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