1package OpenXPKI::Crypto::Profile::Base; 2use strict; 3use warnings; 4 5=head1 NAME 6 7OpenXPKI::Crypto::Profile::Base - base class for cryptographic profiles 8for certificates and CRLs. 9 10=head1 DESCRIPTION 11 12Base class for profiles used in the CA. 13 14=cut 15 16# Core modules 17use English; 18use Data::Dumper; 19use MIME::Base64; 20 21# CPAN modules 22use DateTime; 23use Template; 24 25# Project modules 26use OpenXPKI::Debug; 27use OpenXPKI::Exception; 28use OpenXPKI::Crypt::X509; 29use OpenXPKI::Server::Context qw( CTX ); 30 31 32=head1 FUNCTIONS 33 34=head2 load_extension 35 36Load data from the extensions section 37 38=over 39 40=item * PROFILE (certificates only) 41 42Name of the profile to get the extension from. 43 44=item * CA (crl only) 45 46Name of the CA to get the extension from. 47 48=item * EXT 49 50Name of the extension to load. 51 52=back 53 54=cut 55 56sub load_extension 57{ 58 ##! 1: 'start' 59 my $self = shift; 60 my $args = shift; 61 my $profile_path = $args->{PATH}; 62 my $ext = $args->{EXT}; 63 my @values = (); 64 65 ##! 32: Dumper ( $args ) 66 67 my $config = CTX('config'); 68 69 ##! 4: "Profile: $profile_path, Extension: $ext" 70 my @basepath = split /\./, $profile_path; 71 # the netscape stuff is one level down 72 if ($ext =~ m{netscape_(\w+)}) { 73 push @basepath, 'extensions', 'netscape', $1; 74 } else { 75 push @basepath, 'extensions', $ext; 76 } 77 78 ##! 16: 'path: ' . join (".", @basepath) 79 80 ## is the extension used at all? 81 if (!$config->exists(\@basepath)) { 82 # Test for default settings 83 $basepath[1] = 'default'; 84 if ($config->exists(\@basepath)) { 85 ##! 16: 'Using default value for ' . $ext 86 } else { 87 ##! 16: "Extension $ext is not used" 88 return 0; 89 } 90 } 91 92 ## is this a critical extension? 93 94 my $critical; 95 if ($ext !~ m{(oid|ocsp_nocheck)}) { 96 $critical = $config->get([ @basepath, 'critical' ]); 97 if ($critical) { 98 $critical = 'true'; 99 } elsif (defined $critical) { 100 $critical = 'false'; 101 } else { 102 CTX('log')->application()->warn("Critical flag is not set for $ext in profile $profile_path!"); 103 } 104 } 105 106 if ($ext eq "basic_constraints") 107 { 108 109 my $ca = $config->get([ @basepath, 'ca' ]) || '0'; 110 if ($ca !~ /\A(true|1)\z/) { 111 $values[0] = ["CA", 'false']; 112 } else { 113 $values[0] = ["CA", 'true']; 114 115 my $path_length = $config->get([ @basepath, 'path_length']); 116 if (defined $path_length) 117 { 118 $values[1] = ["PATH_LENGTH", $path_length]; 119 } 120 } 121 122 $self->set_extension (NAME => "basic_constraints", 123 CRITICAL => $critical, 124 VALUES => [@values]); 125 } 126 elsif ($ext eq "key_usage") 127 { 128 my @bits = ( "digital_signature", "non_repudiation", "key_encipherment", 129 "data_encipherment", "key_agreement", "key_cert_sign", 130 "crl_sign", "encipher_only", "decipher_only" ); 131 132 foreach my $bit (@bits) { 133 push @values, $bit if ($config->get([ @basepath, $bit ])); 134 } 135 136 $self->set_extension (NAME => "key_usage", 137 CRITICAL => $critical, 138 VALUES => [@values]); 139 } 140 elsif ($ext eq "extended_key_usage") 141 { 142 my $bits_set = $config->get_hash(\@basepath); 143 ##! 16: "ext key usage bits: ". Dumper $bits_set 144 my @bits = ( "client_auth", "server_auth","email_protection","code_signing","time_stamping", "ocsp_signing"); 145 146 foreach my $bit (@bits) { 147 push @values, $bit if ( $bits_set->{$bit} ); 148 } 149 150 # check keys of hash for numeric oids 151 foreach my $oid (keys %{$bits_set}) { 152 if ($oid =~ /^\d+(\.\d+)+$/) { 153 push @values, $oid; 154 } 155 } 156 157 if (scalar @values) 158 { 159 $self->set_extension (NAME => "extended_key_usage", 160 CRITICAL => $critical, 161 VALUES => [@values]); 162 } 163 } 164 elsif ($ext eq "subject_key_identifier") 165 { 166 if ($config->get([ @basepath, 'hash'])) 167 { 168 $self->set_extension (NAME => "subject_key_identifier", 169 CRITICAL => $critical, 170 VALUES => ["hash"]); 171 } 172 } 173 elsif ($ext eq "authority_key_identifier") 174 { 175 176 my @bits = ( "keyid", "issuer" ); 177 foreach my $bit (@bits) { 178 push @values, $bit if ( $config->get([ @basepath, $bit ]) ); 179 } 180 if (scalar @values) 181 { 182 $self->set_extension (NAME => "authority_key_identifier", 183 CRITICAL => $critical, 184 VALUES => [@values]); 185 } 186 } 187 elsif ($ext eq "issuer_alt_name") 188 { 189 if ($config->get([ @basepath, 'copy' ]) ) 190 { 191 $self->set_extension (NAME => "issuer_alt_name", 192 CRITICAL => $critical, 193 VALUES => ["copy"]); 194 } 195 } 196 elsif ($ext eq "crl_distribution_points") 197 { 198 199 my @uri = $config->get_scalar_as_list([ @basepath, 'uri' ]); 200 # Parse using Template Toolkit 201 @values = @{ $self->process_templates(\@uri) }; 202 203 if (scalar @values) 204 { 205 $self->set_extension (NAME => "cdp", 206 CRITICAL => $critical, 207 VALUES => [@values]); 208 } 209 } 210 elsif ($ext eq "authority_info_access") 211 { 212 foreach my $bit (qw(ca_issuers ocsp)) { 213 214 my @template_list = $config->get_scalar_as_list([ @basepath, $bit ]); 215 # Parse using Template Toolkit and push result 216 if (scalar @template_list) { 217 push @values, [uc($bit), $self->process_templates( \@template_list ) ]; 218 } 219 } 220 221 if (scalar @values) 222 { 223 $self->set_extension (NAME => "authority_info_access", 224 CRITICAL => $critical, 225 VALUES => [@values]); 226 } 227 } 228 elsif ($ext eq "policy_identifier") 229 { 230 # OIDs only 231 my @oids = $config->get_scalar_as_list([ @basepath , 'oid' ]); 232 ##! 16: 'short oids ' . Dumper \@oids 233 foreach my $oid (@oids) { 234 next if ($oid !~ /\d+(\.\d+)+/); 235 push @values, $oid; 236 } 237 238 239 # Support the old config format with one oid and 240 # user_notice in a seperate section 241 if (scalar @values == 1) { 242 pop @basepath; 243 my @user_notice = $config->get_scalar_as_list([ @basepath, "user_notice" ]); 244 if (@user_notice) { 245 $values[0] = { 246 oid => $values[0], 247 user_notice => \@user_notice 248 }; 249 } 250 push @basepath, "policy_identifier"; 251 } 252 253 # check remaining keys for policy with extra sections (CPS + Notice) 254 @oids = $config->get_keys(\@basepath); 255 ##! 16: 'full oid sections ' . Dumper \@oids 256 foreach my $name (@oids) { 257 next if ($name !~/\d+(\.\d+)+/); 258 my $attr = { oid => $name }; 259 my @cps = $config->get_scalar_as_list( [ @basepath, $name, 'cps' ] ); 260 if (@cps) { 261 $attr->{cps} = \@cps; 262 } 263 my @notice = $config->get_scalar_as_list( [ @basepath, $name, 'user_notice' ] ); 264 if (@notice) { 265 $attr->{user_notice} = \@notice; 266 } 267 ##! 32: 'Policy Attribute ' . Dumper $attr 268 push @values, $attr; 269 } 270 271 if (scalar @values) { 272 $self->set_extension (NAME => "policy_identifier", 273 CRITICAL => $critical, 274 VALUES => [@values]); 275 } 276 277 } 278 elsif ($ext eq "oid") 279 { 280 281 my @oids = $config->get_keys(\@basepath); 282 foreach my $name (@oids) { 283 284 my $attr = $config->get_hash( [ @basepath, $name ] ); 285 ##! 32: 'oid attributes, name ' . $name. ', attr: ' . Dumper $attr 286 287 if (!$attr->{value}) { 288 next; 289 } 290 291 # OID can be either the name or given as attribute "oid" 292 if ($attr->{oid}) { 293 $name = $attr->{oid}; 294 } 295 296 # Special case, Sequences needs to be written to a new section 297 if ($attr->{encoding} eq 'SEQUENCE') { 298 $self->set_oid_extension_sequence( 299 NAME => $name, 300 CRITICAL => ($attr->{critical} ? 'true' : 'false'), 301 VALUES => $attr->{value} 302 ); 303 } else { 304 305 my $val = ''; 306 # format and encoding can be given as extra parameters but 307 # finally just end up concatenated with the value 308 if ($attr->{format}) { 309 $val .= $attr->{format}.':'; 310 } 311 if ($attr->{encoding}) { 312 $val .= $attr->{encoding}.':'; 313 } 314 $val .= $attr->{value}; 315 @values = ( $val ); 316 317 $self->set_extension(NAME => $name, 318 CRITICAL => ($attr->{critical} ? 'true' : 'false'), 319 VALUES => [@values]); 320 } 321 } 322 } 323 elsif ($ext eq "ocsp_nocheck") 324 { 325 if ($config->get([ @basepath ])) 326 { 327 $self->set_extension ( 328 NAME => "1.3.6.1.5.5.7.48.1.5", 329 CRITICAL => "false", 330 VALUES => [ "ASN1:NULL" ], 331 ); 332 } 333 } 334 elsif ($ext eq "netscape_comment") 335 { 336 my $comment = $config->get([ @basepath , 'text' ]); 337 if ($comment) 338 { 339 $self->set_extension (NAME => "netscape_comment", 340 CRITICAL => $critical, 341 VALUES => [ $comment ]); 342 } 343 } 344 elsif ($ext eq "netscape_certificate_type") 345 { 346 my @bits = ( "ssl_client", "smime_client", "object_signing", 347 "ssl_client_ca", "smime_client_ca", "object_signing_ca", "ssl_server", "reserved" ); 348 349 foreach my $bit (@bits) { 350 push @values, $bit if ( $config->get([ @basepath , $bit ]) ); 351 } 352 353 if (scalar @values) { 354 $self->set_extension (NAME => "netscape_certificate_type", 355 CRITICAL => $critical, 356 VALUES => [@values]); 357 } 358 359 } 360 elsif ($ext eq "netscape_cdp") 361 { 362 363 my $cdp = $config->get([ @basepath , 'uri' ]); 364 if ($cdp) { 365 $self->set_extension (NAME => "netscape_cdp", 366 CRITICAL => $critical, 367 VALUES => [$cdp]); 368 } 369 370 my $ca_cdp = $config->get([ @basepath , 'ca_uri' ]); 371 if ($ca_cdp) { 372 $self->set_extension (NAME => "netscape_ca_cdp", 373 CRITICAL => $critical, 374 VALUES => [$ca_cdp]); 375 } 376 } 377 else 378 { 379 OpenXPKI::Exception->throw ( 380 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_LOAD_EXTENSION_UNKNOWN_NAME", 381 params => {NAME => $ext, PATH => join(".", @basepath) } 382 ); 383 } 384 385 return 1; 386} 387 388sub set_extension 389{ 390 ##! 1: 'start' 391 my $self = shift; 392 my $keys = { @_ }; 393 my $name = $keys->{NAME}; 394 my $critical = $keys->{CRITICAL}; 395 my $value = $keys->{VALUES}; 396 my $force = $keys->{FORCE}; 397 398 399 if (! defined $name) { 400 OpenXPKI::Exception->throw( 401 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_SET_EXTENSION_NAME_NOT_SPECIFIED", 402 ); 403 } 404 405 if ($self->{PROFILE}->{EXTENSIONS}->{$name}) { 406 if (!$force) { 407 OpenXPKI::Exception->throw ( 408 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_SET_EXTENSION_ALREADY_SET", 409 params => { NAME => $name } 410 ); 411 } 412 $self->{PROFILE}->{EXTENSIONS}->{$name} = {}; 413 } 414 415 if (! defined $value) { 416 OpenXPKI::Exception->throw ( 417 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_SET_EXTENSION_VALUE_NOT_SPECIFIED", 418 ); 419 } 420 if (! defined $critical) { 421 OpenXPKI::Exception->throw ( 422 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_SET_EXTENSION_CRITICALITY_NOT_SPECIFIED", 423 params => { 424 NAME => $name, 425 VALUE => $value, 426 }); 427 } 428 if ($critical !~ m{ \A (?:true|false) }xms) { 429 OpenXPKI::Exception->throw ( 430 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_SET_EXTENSION_INVALID_CRITICALITY", 431 params => { 432 NAME => $name, 433 VALUE => $value, 434 CRITICALITY => $critical, 435 }); 436 } 437 438 ##! 16: 'name: ' . $name 439 ##! 16: 'critical: ' . $critical 440 ##! 16: 'value: ' . Dumper ( $value ) 441 442 $critical = 0 if ($critical eq "false"); 443 $critical = 1 if ($critical eq "true"); 444 $self->{PROFILE}->{EXTENSIONS}->{$name}->{CRITICAL} = $critical; 445 446 447 if (!ref $value) { 448 $self->{PROFILE}->{EXTENSIONS}->{$name}->{VALUE} = [ $value ]; 449 } else { 450 ## copy by value (normal array) 451 $self->{PROFILE}->{EXTENSIONS}->{$name}->{VALUE} = [ @{$value} ]; 452 } 453 454 return 1; 455} 456 457=head2 generate_oid_extension_section 458 459Wrapper around set_extension to prepare oid extensions with sequence 460 461=cut 462 463sub set_oid_extension_sequence { 464 465 my $self = shift; 466 my $keys = { @_ }; 467 my $name = $keys->{NAME}; 468 my $critical = $keys->{CRITICAL}; 469 my $value = $keys->{VALUES}; 470 471 my $section = 'oid_section_'.$name; 472 $section =~ s/\./_/g; 473 my @values = ( 'ASN1:SEQUENCE:'.$section, "[ $section ]" ); 474 my @section = split /\r?\n/, $value; 475 push @values, @section; 476 477 return $self->set_extension(NAME => $name, 478 CRITICAL => $critical ? 'true' : 'false', 479 VALUES => [@values]); 480 481} 482 483sub is_critical_extension 484{ 485 my $self = shift; 486 my $ext = shift; 487 488 if (not exists $self->{PROFILE}->{EXTENSIONS}->{$ext}) 489 { 490 OpenXPKI::Exception->throw ( 491 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_IS_CIRITICAL_EXTENSION_NOT_FOUND"); 492 } 493 494 return $self->{PROFILE}->{EXTENSIONS}->{$ext}->{CRITICAL}; 495} 496 497sub get_extension 498{ 499 my $self = shift; 500 my $ext = shift; 501 502 if (not exists $self->{PROFILE}->{EXTENSIONS}->{$ext}) 503 { 504 OpenXPKI::Exception->throw ( 505 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_GET_EXTENSION_NOT_FOUND", 506 params => { 507 "EXTENSION" => $ext, 508 }, 509 ); 510 } 511 512 if (not defined $self->{PROFILE}->{EXTENSIONS}->{$ext}->{VALUE}) 513 { 514 OpenXPKI::Exception->throw ( 515 message => "I18N_OPENXPKI_CRYPTO_PROFILE_CERTIFICATE_GET_EXTENSION_NO_VALUE", 516 params => { 517 "EXTENSION" => $ext, 518 }, 519 ); 520 } 521 522 return $self->{PROFILE}->{EXTENSIONS}->{$ext}->{VALUE}; 523 524} 525 526 527sub has_extension 528{ 529 my $self = shift; 530 my $ext = shift; 531 532 return ((exists $self->{PROFILE}->{EXTENSIONS}->{$ext}) 533 && (defined $self->{PROFILE}->{EXTENSIONS}->{$ext}->{VALUE})); 534 535} 536 537sub set_serial 538{ 539 my $self = shift; 540 $self->{PROFILE}->{SERIAL} = shift; 541 return 1; 542} 543 544sub get_serial 545{ 546 my $self = shift; 547 return $self->{PROFILE}->{SERIAL}; 548} 549 550sub get_oid_extensions 551{ 552 my $self = shift; 553 return grep /\d+\./, keys %{$self->{PROFILE}->{EXTENSIONS}}; 554} 555 556sub get_named_extensions 557{ 558 my $self = shift; 559 return grep /[^(\d+\.)]/, keys %{$self->{PROFILE}->{EXTENSIONS}}; 560} 561 562# string_mask has no effect in CRL but is required to properly build the config 563sub get_string_mask 564{ 565 my $self = shift; 566 return $self->{PROFILE}->{STRING_MASK} || 'utf8only'; 567} 568 569=head2 create_random_serial 570 571Generate a random serial number (ID) and return it as a L<Math::BigInt> object. 572 573B<Parameters> 574 575=over 576 577=item PREFIX - High order bits to prepend to the generated serial number (optional) 578 579=item RANDOM_LENGTH - The desired byte length of the random part 580 581=back 582 583=cut 584sub create_random_serial { 585 my ($self, %args) = @_; 586 587 OpenXPKI::Exception->throw(message => 'Mandatory parameter RANDOM_LENGTH missing') 588 unless defined $args{'RANDOM_LENGTH'}; 589 590 my $serial = Math::BigInt->new( $args{'PREFIX'} // 0); 591 my $rand_length = $args{'RANDOM_LENGTH'}; 592 ##! 16: "create_random_serial({ RANDOM_LENGTH => $rand_length, PREFIX => " . $serial->as_hex . " })" 593 594 if ($rand_length > 0) { 595 my $rand_hex = CTX('api2')->get_random( 596 length => $rand_length, 597 format => 'hex', 598 ); 599 600 ##! 16: 'random part: ' . $rand_hex 601 # left shift the existing serial by the size of the random part and 602 # add it to the right 603 $serial->blsft($rand_length * 8); 604 ##! 16: 'bit shifted serial: ' . $serial->as_hex 605 $serial->bior(Math::BigInt->new('0x' . $rand_hex)); 606 } 607 ##! 16: 'returning: ' . $serial->as_hex 608 return $serial->bstr; # return serial as "decimal notation, possibly zero padded" 609} 610 611=head2 process_templates 612 613Helper method to parse profile items through template toolkit. 614Expects an array of strings containing one TT Template per line. 615Available variables for substitution are 616 617=over 618 619=item ISSUER.x Hash with the subject parts of the issuing certificate. 620 621Note that each key is an array itself, even if there is only a single value in it. 622Therefore you need to write e.g. ISSUER.OU.0 for the (first) OU entry. Its wise 623to do urlescaping on the output, e.g. [- ISSUER.OU.0 | uri -]. 624 625The hash also has ISSUER.DN set with the full dn. 626 627=item CAALIAS Hash holding information about the used ca token. 628 629Offers the keys ALIAS, GROUP, GENERATION as given in the alias table. 630 631=item PKI_REALM 632 633The internal name of the realm (e.g. "democa"). 634 635=back 636 637=cut 638 639sub process_templates { 640 641 my $self = shift; 642 my $values = shift; 643 644 # Add ability to use template toolkit - check if there are tags inside 645 646 ##! 32: ' Test for TT ' . Dumper ( $values ) 647 if (! scalar(grep /\[.*\]/, @$values) ) { 648 return $values; 649 } 650 651 ##! 16: 'Tags found - init TT' 652 my $tt = Template->new(); 653 654 655 if (not $self->{CACERTIFICATE}) { 656 $self->{CACERTIFICATE} = CTX('api2')->get_certificate_for_alias( 'alias' => $self->{CA} ); 657 } 658 659 my $ca_cert; 660 if ($self->{CACERTIFICATE}->{data}) { 661 $ca_cert = $self->{CACERTIFICATE}->{data}; 662 # old format 663 } elsif ($self->{CACERTIFICATE}->{DATA}) { 664 $ca_cert = $self->{CACERTIFICATE}->{DATA}; 665 } else { 666 OpenXPKI::Exception->throw( 667 message => 'Unable to load CA Certificate', 668 ); 669 } 670 671 my $x509 = OpenXPKI::Crypt::X509->new( $ca_cert ); 672 673 # Get Issuer Info from selected ca 674 my $issuer_info = $x509->subject_hash(); 675 $issuer_info->{DN} = $x509->get_subject(); 676 677 # Split alias into generation and group name 678 $self->{CA} =~ /^(.*)-(\d+)$/; 679 my $group = $1; 680 my $generation = $2; 681 682 my %template_vars = ( 683 'ISSUER' => $issuer_info, 684 'CAALIAS' => { 685 'ALIAS' => $self->{CA}, 686 'GROUP' => $group, 687 'GENERATION' => $generation, 688 }, 689 'PKI_REALM' => CTX('api2')->get_pki_realm(), 690 ); 691 ##! 32: ' Template Vars ' . Dumper ( %template_vars ) 692 693 my @newvalues; 694 while (my $template = shift @$values) { 695 if ($template =~ /\[.+\]/) { 696 my $output; 697 #$template = '[% TAGS [- -] -%]' . $template; 698 if (!$tt->process(\$template, \%template_vars, \$output)) { 699 OpenXPKI::Exception->throw( 700 message => 'I18N_OPENXPKI_CRYPTO_PROFILE_BASE_ERROR_PARSING_TEMPLATE', 701 params => { 702 'TEMPLATE' => $template, 703 'ERROR' => $tt->error() 704 } 705 ); 706 } 707 708 ##! 32: ' Tags found - ' . $template . ' -> '. $output 709 if($output) { 710 push @newvalues, $output; 711 } 712 } else { 713 push @newvalues, $template; 714 } 715 } 716 717 ##! 64: ' Processed CRL DP ' . Dumper ( @newvalues ) 718 return \@newvalues; 719} 720 721our $AUTOLOAD; 722sub AUTOLOAD { 723 my $self = shift; 724 return if ($AUTOLOAD =~ m{ \A .*::DESTROY \z}xms); 725 return "" if ($AUTOLOAD =~ s/^.*:get_//); 726 OpenXPKI::Exception->throw ( 727 message => "I18N_OPENXPKI_CRYPTO_PROFILE_BASE_AUTOLOAD_ILLEGAL_FUNCTION", 728 params => {"FUNCTION" => $AUTOLOAD}); 729} 730 7311; 732__END__ 733