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