1package OpenXPKI::Server::Workflow::Activity::Tools::EvaluateSignerTrust;
2
3use strict;
4use warnings;
5
6use base qw( OpenXPKI::Server::Workflow::Activity );
7use OpenXPKI::Server::Context qw( CTX );
8use OpenXPKI::Debug;
9use OpenXPKI::Exception;
10use OpenXPKI::Crypt::X509;
11use Data::Dumper;
12use English;
13
14sub execute {
15    ##! 16: 'start'
16    my ( $self, $workflow ) = @_;
17
18    my $context = $workflow->context();
19    my $config = CTX('config');
20
21    # reset the context flags
22    $context->param('signer_trusted' => 0);
23    $context->param('signer_authorized' => undef);
24    $context->param('signer_revoked' => 0);
25    $context->param('signer_validity_ok' => 0);
26    $context->param('signer_chain_ok' => undef);
27    $context->param('signer_in_current_realm' => 0);
28    $context->param('signer_cert_identifier' => undef );
29
30    my $current_realm = CTX('session')->data->pki_realm;
31
32    my $signer_cert = $context->param('signer_cert');
33
34    if (!$signer_cert) {
35        ##! 16: 'No signer certificate in context'
36        CTX('log')->application()->debug("Trusted Signer validation skipped, no certificate found");
37        return 1;
38    }
39
40    my $x509 = OpenXPKI::Crypt::X509->new( $signer_cert );
41
42    my $signer_identifier = $x509->get_cert_identifier();
43    ##! 32: 'Signer identifier ' .$signer_identifier
44
45    # Get realm and issuer for signer certificate
46    my $cert_hash = CTX('dbi')->select_one(
47        from    => 'certificate',
48        columns => ['pki_realm', 'issuer_identifier', 'req_key', 'status' ],
49        where   => { identifier => $signer_identifier },
50    );
51
52    if (!$cert_hash && $x509->is_selfsigned() && $self->param('allow_surrogate_certificate')) {
53        my $db_results = CTX('dbi')->select_hashes(
54            from    => 'certificate',
55            columns => ['pki_realm', 'identifier', 'issuer_identifier', 'req_key', 'status', 'data' ],
56            where   => {
57                subject_key_identifier => $x509->get_subject_key_id(),
58                req_key => { "!=" => undef }
59            },
60            limit => 2,
61        );
62
63        if ($db_results && scalar @{$db_results}) {
64            if (scalar @{$db_results} > 1) {
65                CTX('log')->application()->warn("Use of surrogate requested but result is not unique!");
66            } else {
67                $cert_hash = $db_results->[0];
68                $signer_identifier = $cert_hash->{identifier};
69                CTX('log')->application()->info("Using surrogate certificate");
70                ##! 32: 'Got surrogate ' . $signer_identifier
71                $x509 = OpenXPKI::Crypt::X509->new( $cert_hash->{data} );
72            }
73        }
74
75    }
76
77    # Check if the certificate is valid
78    my $now = DateTime->now();
79    my $notbefore = $x509->get_notbefore();
80    my $notafter = $x509->get_notafter();
81
82    if ( ( DateTime->compare( $notbefore, $now ) <= 0)  && ( DateTime->compare( $now,  $notafter) < 0) ) {
83        $context->param('signer_validity_ok' => '1');
84    } else {
85        $context->param('signer_validity_ok' => '0');
86    }
87
88    # populate some details on the cert if requested
89    my $signer_subject = $x509->get_subject();
90    if ($self->param('export_subject')) {
91        $context->param( 'signer_subject' => $signer_subject );
92    }
93
94    if ($self->param('export_key_identifier')) {
95        if ($self->param('export_key_identifier') eq 'hash') {
96            $context->param( 'signer_subject_key_identifier' => $x509->get_public_key_hash() );
97        } elsif ($self->param('export_key_identifier') eq 'both') {
98            $context->param( 'signer_public_key_hash' => $x509->get_public_key_hash());
99            $context->param( 'signer_subject_key_identifier' => $x509->get_subject_key_id() );
100        } else {
101            $context->param( 'signer_subject_key_identifier' => $x509->get_subject_key_id() );
102        }
103    }
104
105    # Check the chain
106    # set from either db query or from chain validation
107    my ($signer_issuer, $signer_root, $signer_revoked, @signer_chain);
108    my $signer_realm = 'unknown';
109    my $signer_profile = 'unknown';
110
111    if ($cert_hash) {
112        ##! 16: 'certificate found in database'
113        # certificate was found in local database
114        $context->param('signer_cert_identifier' => $signer_identifier);
115        $signer_realm = $cert_hash->{pki_realm} || '_global';
116        $signer_issuer = $cert_hash->{issuer_identifier};
117        $signer_revoked = ($cert_hash->{status} ne 'ISSUED');
118
119        # Get the profile of the certificate, if it was issued from this CA
120        if ($cert_hash->{req_key}) {
121            my $profile = CTX('api2')->get_profile_for_cert( identifier => $signer_identifier );
122            $signer_profile = $profile if ($profile);
123            if ( $current_realm eq $signer_realm ) {
124                $context->param('signer_in_current_realm' => 1 );
125            }
126        }
127
128        if ($signer_issuer) {
129            my $signer_chain = CTX('api2')->get_chain( start_with => $signer_issuer );
130            @signer_chain = @{$signer_chain->{identifiers}};
131            if ($signer_chain->{complete}) {
132                $signer_root = pop @{$signer_chain->{identifiers}};
133            }
134        }
135    } elsif (!$x509->is_selfsigned() && $self->param('allow_external_signer')) {
136        ##! 16: 'external certificate - try to validate'
137        # use validate to build the chain
138        $signer_realm = 'external';
139
140        my $crl_check = $self->param('crl_check') || 'none';
141
142        my $chain_validate = CTX('api2')->validate_certificate(
143            pem => $signer_cert,
144            crl_check => $crl_check,
145        );
146
147        my $cert_status = $chain_validate->{status};
148        ##! 32: 'chain validation status ' . $cert_status
149        if ($cert_status =~ m{(VALID|REVOKED|NOROOT)}) {
150
151            @signer_chain = @{$chain_validate->{chain}};
152            # remove the entity from the chain
153            shift @signer_chain;
154            if ($signer_chain[0]) {
155                $signer_issuer = CTX('api2')->get_cert_identifier( cert => $signer_chain[0] );
156            }
157
158            if ($cert_status ne 'NOROOT') {
159                my $signer_root_pem = pop @{$chain_validate->{chain}};
160                $signer_root = CTX('api2')->get_cert_identifier( cert => $signer_root_pem );
161                $context->param('signer_chain_ok' => 1);
162            }
163            # not implemented for remote certificates yet!
164            $signer_revoked = ($cert_status eq 'REVOKED');
165
166            CTX('log')->application()->debug("Chain validation result is $cert_status");
167
168        }  else {
169
170            ##! 32: 'chain broken or untrusted'
171            $context->param('signer_chain_ok' => 0);
172            CTX('log')->application()->warn("Chain validation was not successful");
173        }
174
175    }
176
177    ##! 32: 'Signer profile ' .$signer_profile
178    ##! 32: 'Signer realm ' .  $signer_realm
179    ##! 32: 'Signer issuer ' . ($signer_issuer || 'unknown')
180    ##! 32: 'Signer root ' . ($signer_root || 'unknown')
181
182    if ($signer_revoked) {
183        ##! 64: 'Signer is revoked'
184        CTX('log')->application()->warn("Trusted Signer certificate is revoked");
185        $context->param('signer_revoked' => 1);
186
187    } elsif ($signer_root) {
188        ##! 64: 'Signer is trusted'
189        $context->param('signer_trusted' => 1);
190        CTX('log')->application()->info("Trusted Signer chain validated - trusted root is $signer_root");
191
192    } elsif ($x509->is_selfsigned()) {
193        ##! 64: 'Signer is selfsigned'
194        $context->param('signer_cert_identifier' => '');
195        CTX('log')->application()->info("Trusted Signer chain - certificate is self signed");
196
197    # something went really wrong, the certificate might be forged or is
198    # not from a trusted source so it does - usually - not make sense to
199    # continue but sometimes you might want to....
200    } elsif ($self->param('allow_untrusted_signer')) {
201        ##! 64: 'continue with untrusted signer'
202        CTX('log')->application()->info("Chain validation failed but allow_untrusted_signer is set");
203
204    } else {
205        ##! 64: 'untrusted signer, aborting'
206        CTX('log')->application()->warn("Trusted Signer chain validation FAILED - aborting");
207        return;
208    }
209
210    # End chain validation, now check the authorization
211
212    ##! 32: 'Check signer '.$signer_subject.' against trustlist'
213    my $rules = $self->param('rules');
214    # explicit declaration as action parameter
215    my @rules;
216    if (ref $rules eq 'HASH') {
217        ##! 128: $rules
218        @rules = sort keys %{$rules};
219        CTX('log')->application()->debug("SignerTrust explicit rules: ". join(",", @rules));
220    } else {
221        @rules = $config->get_keys( $rules );
222        CTX('log')->application()->debug("SignerTrust loading rules from $rules");
223    }
224
225    if (!@rules) {
226        CTX('log')->application()->info("No rules were found - skip signer authorization check.");
227        return 1;
228    }
229
230    my $matched = 0;
231
232    CTX('log')->application()->debug("Trusted Signer Authorization $signer_profile / $signer_realm / $signer_subject / $signer_identifier");
233
234    TRUST_RULE:
235    foreach my $rule (@rules) {
236        ##! 32: 'Testing rule ' . $rule
237        my $trustrule = (ref $rules eq 'HASH') ? $rules->{$rule}
238            : $config->get_hash("$rules.$rule");
239
240        # as we expect the idenifier to be uniq we do not need a realm
241        $trustrule->{realm} = $current_realm
242            if (!$trustrule->{realm} && !$trustrule->{identifier});
243
244        ##! 64: $trustrule
245        my $matched = CTX('api2')->evaluate_trust_rule(
246            signer_subject  => $signer_subject,
247            signer_identifier   => $signer_identifier,
248            signer_realm    => $signer_realm,
249            ($signer_profile ? (signer_profile  => $signer_profile) : ()),
250            ($signer_issuer  ? (signer_issuer   => $signer_issuer) : ()),
251            ($signer_root    ? (signer_root     => $signer_root) : ()),
252            rule            => $trustrule,
253        );
254
255        if ($matched) {
256            ##! 16: 'Passed validation rule #'.$rule,
257            CTX('log')->application()->info("Trusted Signer Authorization matched rule $rule");
258            $context->param('signer_authorized' => 1);
259            return 1;
260        }
261    }
262
263    CTX('log')->application()->info("Trusted Signer not found in trust list ($signer_subject).");
264
265    $context->param('signer_authorized' => 0);
266    return 1;
267}
268
2691;
270
271=head1 NAME
272
273OpenXPKI::Server::Workflow::Activity::Tools::EvaluateSignerTrust
274
275=head1 SYNOPSIS
276
277    class: OpenXPKI::Server::Workflow::Activity::Tools::EvaluateSignerTrust
278    param:
279      _map_rules: scep.[% context.server %].authorized_signer_on_behalf
280
281=head1 DESCRIPTION
282
283Evaluate if the signer certificate can be trusted. Populates the result
284into several context items, all values are boolean. Checks are by default
285done based on the contents of the certificate database and work only for
286certificates which are found there. If you want to validate certificates
287from an external CA you must import the full issuer chain and either set
288the I<allow_external_signer> flag or import the used signer certificates
289themselves.
290
291=over
292
293=item signer_trusted
294
295true if the complete chain is available and certificate status is not
296revoked. Does B<NOT> check for expiration.
297
298=item signer_authorized
299
300true if the signer matches one of the given rules. This does B<NOT> depend
301on the trust status of the certificate, so you need to check both flags or
302delegate the chain validation to another component (e.g. tls config of
303webserver). Will be I<undef> in case the certificate chain could not be
304validated at all or no trust rules have been found.
305
306=item signer_validity_ok
307
308true if the current date is within the notbefore/notafter window
309
310=item signer_revoked
311
312true if the certificate is marked revoked.
313
314=item signer_chain_ok
315
316only available with external signers, true if the certificate chain
317was successfully build, false otherwise.
318
319=item signer_cert_identifier
320
321the identifier of the signer certificate
322
323=item signer_subject
324
325the subject of the signer certificate,
326only exported if export_subject parameter is set
327
328=item signer_subject_key_identifier, signer_public_key_hash
329
330the signer_key_identifier / public_key_hash of the signer
331certificate, see export_key_identifier parameter for details.
332
333=item signer_in_current_realm
334
335Boolean, weather the signer is an entity in the current realm
336
337=back
338
339=head1 Configuration
340
341The check for authorization uses a list of rules. Those can be either
342given explicitly to the I<rules> parameter or as a pointer to the realm
343config. A common pattern used in OpenXPKI is to build the path for the
344rules from the server properties, e.g. the SCEP workflow uses
345I<scep.[% context.server ].authorized_signer>..
346
347If I<rules> is a scalar, it is considered to be a config path, if it is
348a hash it is taken as explicitly defined ruleset.
349
350The ruleset structure is a hash of hashes, were each entry is a combination
351of one or more matching rules. The name of the rule is just used for logging
352purpose:
353
354  rule1:
355    subject: CN=scep-signer.*,dc=OpenXPKI,dc=org
356    identifier: AhElV5GzgFhKalmF_yQq-b1TnWg
357    profile: client_scep
358    realm: democa
359
360=head2 Rules
361
362The rules in one entry are ANDed together, values are full string match,
363except the subject rule. If you want to provide alternatives, add multiple
364list items.
365
366=over
367
368=item subject
369
370Evaluated as a regexp against the signers full subject, therefore any
371characters with a special meaning in perl regexp need to be escaped!
372
373=item profile
374
375Matches the name of the internal OpenXPKI profile assigned to this
376certificate. This implies that the certificate was issued by us.
377
378=item realm
379
380The name of the realm where the certificate originates from, works also
381for certificates imported into a realm. If not set, the default is the
382current realm. Pass the special value I<_any> to ignore the realm during
383rule evaluation.
384
385Special rules apply when matching on "identifier" or "issuer_alias".
386
387=item identifier
388
389The identifier of the certificate. This works also with external issued or
390self signed certificates. I<realm> is only matched if set explicit, so
391I<realm: _any> is the default.
392
393=item issuer_alias
394
395The name of an alias group as registered in the I<aliases> table. Matches
396if the certificate issuer has an active alias in the given group. The alias
397item is searched in the given realm and the global realm, setting
398I<realm: _any> is ignored (search is done in the global realm only).
399
400Note: The query is done at once for the given and global realm, this might
401cause unexepcted behaviour when the same alias exists in both with different
402validity dates (there will be a positive match if either the local or the
403global realm lists the item as valid).
404
405=item root_alias
406
407Same as issuer_alias but queries for the root certificate
408
409=item meta_*
410
411Load the metadata attributes assigned to the certificate and match against
412the given value.
413
414=back
415
416=head2 Parameters
417
418=over
419
420=item rules
421
422Usually a scalar value, taken as config path to read the rules from. Can
423also be a hash that represents an explicit ruleset (see Rules).
424
425=item export_subject
426
427Boolean, if set the signer_subject is exported to the context.
428
429=item export_key_identifier
430
431Export information about the signers subject_key_identifier. As there is
432an ambiguity on this term, you can switch the behaviour.
433
434The default behaviour on any true value is to write the key identifier
435read from the certificate to  I<signer_subject_key_identifier>. If you
436pass I<hash>, you get the value of the SHA1 hash of the public key as
437defined in RFC5280 in this field. If you pass I<both>, the SHA1 has will
438be written as an additional field to I<signer_public_key_hash>.
439
440B<Note>: If a certificate does not contain an explicit subject key
441identifier, this always falls back to the SHA1 hash.
442
443=item allow_external_signer
444
445Boolean, if set and the signer is not found in the local database the activity
446tries to verify the certificate chain using the validate_certificate API
447method.
448
449=item allow_untrusted_signer
450
451Boolean, if true, the rulesets are processed even if the certificate chain
452could not be built or validated. This is only useful with external signers.
453
454=item crl_check
455
456Only used when the certificate is an external signer. Valid values are
457I<leaf> or I<all>, tries to use CRLs when validating the certificate for
458either the lead certificate or the full chain. The required CRLs must
459exist in the CRL table.
460
461
462=item allow_surrogate_certificate
463
464Boolean, if set and the signer is not found in the local database B<and> is
465self-signed the database is searched for an entity certificate with the same
466subject key id. This is used e.g. in the PoP renewal via EST/RPC.
467
468=back
469