File Coverage

lib/Crypt/Perl/X509v3.pm
Criterion Covered Total %
statement 83 90 92.2
branch 19 26 73.0
condition 7 11 63.6
subroutine 14 14 100.0
pod 0 3 0.0
total 123 144 85.4


line stmt bran cond sub pod time code
1             package Crypt::Perl::X509v3;
2              
3 2     2   1019 use strict;
  2         4  
  2         71  
4 2     2   11 use warnings;
  2         4  
  2         83  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             Crypt::Perl::X509v3 - TLS/SSL Certificates
11              
12             =head1 SYNOPSIS
13              
14             my $cert = Crypt::Perl::X509v3->new(
15             key => $crypt_perl_public_key_obj,
16             issuer => [
17             [ commonName => 'Foo', surname => 'theIssuer' ],
18             [ givenName => 'separate RDNs' ],
19             ],
20             subject => \@subject, #same format as issuer
21              
22             not_before => $unixtime,
23             not_after => $unixtime,
24              
25             # The same structure as in Crypt::Perl::PKCS10 …
26             extensions => [
27             [ keyUsage => 'keyCertSign', 'keyEncipherment' ],
28             [ $extn_name => @extn_args ],
29             # ..
30             ],
31              
32             serial_number => 12345,
33              
34             issuer_unique_id => '..',
35             subject_unique_id => '..',
36             );
37              
38             # The signature algorithm (2nd argument) is not needed
39             # when the signing key is Ed25519.
40             $cert->sign( $crypt_perl_private_key_obj, 'sha256' );
41              
42             my $pem = $cert->to_pem();
43              
44             =head1 STATUS
45              
46             This module is B! The API may change between versions.
47             If you’re going to build something off of it, ensure that you check
48             Crypt::Perl’s changelog before updating this module.
49              
50             =head1 DESCRIPTION
51              
52             This module can create TLS/SSL certificates. The caller has full control
53             over all certificate components, and anything not specified is not assumed.
54              
55             There currently is not a parsing interface. Hopefully that can be remedied.
56              
57             =cut
58              
59 2     2   11 use parent qw( Crypt::Perl::ASN1::Encodee );
  2         4  
  2         25  
60              
61 2     2   1077 use Crypt::Perl::ASN1::Signatures ();
  2         44  
  2         43  
62 2     2   899 use Crypt::Perl::X509::Extensions ();
  2         6  
  2         44  
63 2     2   924 use Crypt::Perl::X509::Name ();
  2         6  
  2         39  
64              
65 2     2   18 use Crypt::Perl::X ();
  2         4  
  2         257  
66              
67             #TODO: refactor
68             *to_der = __PACKAGE__->can('encode');
69              
70             sub to_pem {
71 7     7 0 55 my ($self) = @_;
72              
73 7         64 require Crypt::Format;
74 7         58 return Crypt::Format::der2pem( $self->to_der(), 'CERTIFICATE' );
75             }
76              
77 2     2   14 use constant ASN1 => <
  2         4  
  2         2121  
78             X509v3 ::= SEQUENCE {
79             tbsCertificate ANY,
80             signatureAlgorithm SigIdentifier,
81             signature BIT STRING
82             }
83              
84             SigIdentifier ::= SEQUENCE {
85             algorithm OBJECT IDENTIFIER,
86             parameters ANY OPTIONAL
87             }
88              
89             TBSCertificate ::= SEQUENCE {
90             version [0] Version,
91             serialNumber INTEGER,
92             signature SigIdentifier,
93             issuer ANY, -- Name
94             validity Validity,
95             subject ANY, -- Name
96             subjectPublicKeyInfo ANY,
97             issuerUniqueID [1] IMPLICIT BIT STRING OPTIONAL,
98             -- If present, version MUST be v2 or v3
99             subjectUniqueID [2] IMPLICIT BIT STRING OPTIONAL,
100             -- If present, version MUST be v2 or v3
101             extensions [3] Extensions OPTIONAL
102             -- If present, version MUST be v3 --
103             }
104              
105             Version ::= SEQUENCE {
106             version INTEGER
107             }
108              
109             Validity ::= SEQUENCE {
110             notBefore Time,
111             notAfter Time
112             }
113              
114             Time ::= CHOICE {
115             -- utcTime UTCTime, -- Y2K problem … wtf?!?
116             generalTime GeneralizedTime
117             }
118              
119             Extensions ::= SEQUENCE {
120             extensions ANY
121             }
122             END
123              
124             sub new {
125 7     7 0 559 my ($class, %opts) = @_;
126              
127 7         37 my @missing = grep { !$opts{$_} } qw( subject key not_after );
  21         98  
128              
129 7 50       31 if (@missing) {
130 0         0 die Crypt::Perl::X::create('Generic', "Missing: @missing");
131             }
132              
133 7   66     72 $opts{'extensions'} &&= Crypt::Perl::X509::Extensions->new(@{ $opts{'extensions'} });
  6         172  
134              
135 7         24 my $subj = Crypt::Perl::X509::Name->new( @{ $opts{'subject'} } );
  7         137  
136              
137 7         24 my $issuer;
138 7 50       35 if ($opts{'issuer'}) {
139 7         25 $issuer = Crypt::Perl::X509::Name->new( @{ $opts{'issuer'} } );
  7         32  
140             }
141             else {
142 0         0 $issuer = $subj; #self-signed
143             }
144              
145 7   50     135 $opts{'serial_number'} ||= 0;
146              
147             my %self = (
148             _subject => $subj,
149             _issuer => $issuer,
150             _not_before => $opts{'not_before'} || time,
151              
152 7   66     76 ( map { ( "_$_" => $opts{$_} ) } qw(
  42         186  
153             key
154             not_after
155             extensions
156             serial_number
157             subject_unique_id
158             issuer_unique_id
159             ) ),
160             );
161              
162 7         55 return bless \%self, $class;
163             }
164              
165             sub sign {
166 7     7 0 80 my ($self, $signer_key, $digest_algorithm) = @_;
167              
168 7         76 my ( $tbs, $digest_length ) = $self->_encode_tbs_certificate($signer_key, $digest_algorithm);
169              
170 7         4853 my ($sig_alg, $sig_func, $signature);
171              
172 7 100       175 if ($signer_key->isa('Crypt::Perl::ECDSA::PrivateKey')) {
    100          
    50          
173 3         78 require Digest::SHA;
174              
175 3         15 $sig_alg = "ecdsa-with-SHA$digest_length";
176              
177 3         12 my $fn = "sign_sha$digest_length";
178              
179 3         26 $signature = $signer_key->$fn($tbs);
180             }
181             elsif ($signer_key->isa('Crypt::Perl::RSA::PrivateKey')) {
182 3         692 require Digest::SHA;
183              
184 3         3639 $sig_alg = "sha${digest_length}WithRSAEncryption";
185              
186 3 50       42 my $sign_cr = $signer_key->can("sign_RS$digest_length") or do {
187 0         0 die "Unsupported digest for RSA: $digest_algorithm";
188             };
189              
190 3         35 $signature = $sign_cr->($signer_key, $tbs);
191             }
192             elsif ($signer_key->isa('Crypt::Perl::Ed25519::PrivateKey')) {
193 1         3 $sig_alg = 'ed25519';
194 1         19 $signature = $signer_key->sign($tbs);
195             }
196             else {
197 0         0 die "Key ($signer_key) is not a recognized private key object!";
198             }
199              
200             $sig_alg = {
201 7         115424 algorithm => $Crypt::Perl::ASN1::Signatures::OID{$sig_alg},
202             };
203              
204 7         66 $self->{'_signed'} = {
205             tbsCertificate => $tbs,
206             signatureAlgorithm => $sig_alg,
207             signature => $signature,
208             };
209              
210 7         46 return $self;
211             }
212              
213             sub _get_digest_length {
214 6 50   6   86 $_[0] =~ m<\Asha(224|256|384|512)\z> or do {
215 0         0 die Crypt::Perl::X::create('Generic', "Unknown digest algorithm: “$_[0]”");
216             };
217              
218 6         60 return $1;
219             }
220              
221             sub _encode_params {
222 7     7   29 my ($self) = @_;
223              
224 7 50       37 if (!$self->{'_signed'}) {
225 0         0 die Crypt::Perl::X::create('Generic', 'Call sign() first!');
226             }
227              
228 7         41 return $self->{'_signed'};
229             }
230              
231             sub _encode_tbs_certificate {
232 7     7   44 my ($self, $signing_key, $digest_algorithm) = @_;
233              
234 7   66     97 my $digest_length = $digest_algorithm && _get_digest_length($digest_algorithm);
235              
236 7         31 my $sig_alg;
237              
238             my $pubkey_der;
239              
240 7 100       187 if ($self->{'_key'}->isa('Crypt::Perl::ECDSA::PublicKey')) {
    100          
    50          
241 3         37 $pubkey_der = $self->{'_key'}->to_der_with_curve_name();
242 3         1071 $sig_alg = "ecdsa-with-SHA$digest_length";
243             }
244             elsif ($self->{'_key'}->isa('Crypt::Perl::RSA::PublicKey')) {
245 3         44 $pubkey_der = $self->{'_key'}->to_subject_der();
246 3         1020 $sig_alg = "sha${digest_length}WithRSAEncryption";
247             }
248             elsif ($self->{'_key'}->isa('Crypt::Perl::Ed25519::PublicKey')) {
249 1         4 $sig_alg = 'ed25519';
250 1         9 $pubkey_der = $self->{'_key'}->to_der();
251             }
252             else {
253 0         0 die "Key ($self->{'_key'}) is not a recognized public key object!";
254             }
255              
256 7         241 my $extns_bin;
257 7 100       57 if ($self->{'_extensions'}) {
258 6         48 $extns_bin = $self->{'_extensions'}->encode();
259             }
260              
261             my $params_hr = {
262             version => { version => 2 },
263              
264             serialNumber => $self->{'_serial_number'},
265              
266             issuerUniqueID => $self->{'_issuer_unique_id'},
267              
268             subjectUniqueID => $self->{'_subject_unique_id'},
269              
270             subject => $self->{'_subject'}->encode(),
271             issuer => $self->{'_issuer'}->encode(),
272              
273             validity => {
274             notBefore => { generalTime => $self->{'_not_before'} },
275             notAfter => { generalTime => $self->{'_not_after'} },
276             },
277              
278             subjectPublicKeyInfo => $pubkey_der,
279              
280             signature => {
281 7 100       12443 algorithm => $Crypt::Perl::ASN1::Signatures::OID{$sig_alg},
282             },
283              
284             ( $extns_bin ? ( extensions => { extensions => $extns_bin } ) : () ),
285             };
286              
287 7         1201 my $asn1 = Crypt::Perl::ASN1->new()->prepare($self->ASN1());
288 7         61 $asn1 = $asn1->find('TBSCertificate');
289 7         255 $asn1->configure( encode => { time => 'utctime' } );
290              
291 7         274 return ( $asn1->encode($params_hr), $digest_length );
292             }
293              
294             #sub _get_GeneralizedTime {
295             # my ($epoch) = @_;
296             #
297             # my @smhdmy = (gmtime $epoch)[0 .. 5];
298             # $smhdmy[4]++; #month
299             # $smhdmy[5] += 1900; #year
300             #
301             # return sprintf '%04d%02d%02d%02d%02d%02dZ', reverse @smhdmy;
302             #}
303              
304             1;