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 15 15 100.0
pod 0 3 0.0
total 124 145 85.5


line stmt bran cond sub pod time code
1             package Crypt::Perl::X509v3;
2              
3 2     2   859 use strict;
  2         4  
  2         47  
4 2     2   14 use warnings;
  2         3  
  2         63  
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   9 use parent qw( Crypt::Perl::ASN1::Encodee );
  2         4  
  2         22  
60              
61 2     2   1031 use Digest::SHA ();
  2         4592  
  2         44  
62              
63 2     2   864 use Crypt::Perl::ASN1::Signatures ();
  2         5  
  2         41  
64 2     2   762 use Crypt::Perl::X509::Extensions ();
  2         4  
  2         33  
65 2     2   769 use Crypt::Perl::X509::Name ();
  2         4  
  2         34  
66              
67 2     2   9 use Crypt::Perl::X ();
  2         3  
  2         179  
68              
69             #TODO: refactor
70             *to_der = __PACKAGE__->can('encode');
71              
72             sub to_pem {
73 7     7 0 48 my ($self) = @_;
74              
75 7         77 require Crypt::Format;
76 7         40 return Crypt::Format::der2pem( $self->to_der(), 'CERTIFICATE' );
77             }
78              
79 2     2   11 use constant ASN1 => <
  2         2  
  2         1596  
80             X509v3 ::= SEQUENCE {
81             tbsCertificate ANY,
82             signatureAlgorithm SigIdentifier,
83             signature BIT STRING
84             }
85              
86             SigIdentifier ::= SEQUENCE {
87             algorithm OBJECT IDENTIFIER,
88             parameters ANY OPTIONAL
89             }
90              
91             TBSCertificate ::= SEQUENCE {
92             version [0] Version,
93             serialNumber INTEGER,
94             signature SigIdentifier,
95             issuer ANY, -- Name
96             validity Validity,
97             subject ANY, -- Name
98             subjectPublicKeyInfo ANY,
99             issuerUniqueID [1] IMPLICIT BIT STRING OPTIONAL,
100             -- If present, version MUST be v2 or v3
101             subjectUniqueID [2] IMPLICIT BIT STRING OPTIONAL,
102             -- If present, version MUST be v2 or v3
103             extensions [3] Extensions OPTIONAL
104             -- If present, version MUST be v3 --
105             }
106              
107             Version ::= SEQUENCE {
108             version INTEGER
109             }
110              
111             Validity ::= SEQUENCE {
112             notBefore Time,
113             notAfter Time
114             }
115              
116             Time ::= CHOICE {
117             -- utcTime UTCTime, -- Y2K problem … wtf?!?
118             generalTime GeneralizedTime
119             }
120              
121             Extensions ::= SEQUENCE {
122             extensions ANY
123             }
124             END
125              
126             sub new {
127 7     7 0 435 my ($class, %opts) = @_;
128              
129 7         25 my @missing = grep { !$opts{$_} } qw( subject key not_after );
  21         72  
130              
131 7 50       25 if (@missing) {
132 0         0 die Crypt::Perl::X::create('Generic', "Missing: @missing");
133             }
134              
135 7   66     45 $opts{'extensions'} &&= Crypt::Perl::X509::Extensions->new(@{ $opts{'extensions'} });
  6         134  
136              
137 7         17 my $subj = Crypt::Perl::X509::Name->new( @{ $opts{'subject'} } );
  7         122  
138              
139 7         13 my $issuer;
140 7 50       24 if ($opts{'issuer'}) {
141 7         18 $issuer = Crypt::Perl::X509::Name->new( @{ $opts{'issuer'} } );
  7         28  
142             }
143             else {
144 0         0 $issuer = $subj; #self-signed
145             }
146              
147 7   50     84 $opts{'serial_number'} ||= 0;
148              
149             my %self = (
150             _subject => $subj,
151             _issuer => $issuer,
152             _not_before => $opts{'not_before'} || time,
153              
154 7   66     51 ( map { ( "_$_" => $opts{$_} ) } qw(
  42         156  
155             key
156             not_after
157             extensions
158             serial_number
159             subject_unique_id
160             issuer_unique_id
161             ) ),
162             );
163              
164 7         45 return bless \%self, $class;
165             }
166              
167             sub sign {
168 7     7 0 61 my ($self, $signer_key, $digest_algorithm) = @_;
169              
170 7         83 my ( $tbs, $digest_length ) = $self->_encode_tbs_certificate($signer_key, $digest_algorithm);
171              
172 7         3909 my ($sig_alg, $sig_func, $signature);
173              
174 7 100       147 if ($signer_key->isa('Crypt::Perl::ECDSA::PrivateKey')) {
    100          
    50          
175 3         9 $sig_alg = "ecdsa-with-SHA$digest_length";
176              
177 3         145 $signature = $signer_key->sign( Digest::SHA->can($digest_algorithm)->($tbs) );
178             }
179             elsif ($signer_key->isa('Crypt::Perl::RSA::PrivateKey')) {
180 3         11 $sig_alg = "sha${digest_length}WithRSAEncryption";
181              
182 3 50       25 my $sign_cr = $signer_key->can("sign_RS$digest_length") or do {
183 0         0 die "Unsupported digest for RSA: $digest_algorithm";
184             };
185              
186 3         18 $signature = $sign_cr->($signer_key, $tbs);
187             }
188             elsif ($signer_key->isa('Crypt::Perl::Ed25519::PrivateKey')) {
189 1         2 $sig_alg = 'ed25519';
190 1         8 $signature = $signer_key->sign($tbs);
191             }
192             else {
193 0         0 die "Key ($signer_key) is not a recognized private key object!";
194             }
195              
196             $sig_alg = {
197 7         103581 algorithm => $Crypt::Perl::ASN1::Signatures::OID{$sig_alg},
198             };
199              
200 7         61 $self->{'_signed'} = {
201             tbsCertificate => $tbs,
202             signatureAlgorithm => $sig_alg,
203             signature => $signature,
204             };
205              
206 7         32 return $self;
207             }
208              
209             sub _get_digest_length {
210 6 50   6   75 $_[0] =~ m<\Asha(224|256|384|512)\z> or do {
211 0         0 die Crypt::Perl::X::create('Generic', "Unknown digest algorithm: “$_[0]”");
212             };
213              
214 6         38 return $1;
215             }
216              
217             sub _encode_params {
218 7     7   21 my ($self) = @_;
219              
220 7 50       30 if (!$self->{'_signed'}) {
221 0         0 die Crypt::Perl::X::create('Generic', 'Call sign() first!');
222             }
223              
224 7         32 return $self->{'_signed'};
225             }
226              
227             sub _encode_tbs_certificate {
228 7     7   30 my ($self, $signing_key, $digest_algorithm) = @_;
229              
230 7   66     58 my $digest_length = $digest_algorithm && _get_digest_length($digest_algorithm);
231              
232 7         19 my $sig_alg;
233              
234             my $pubkey_der;
235              
236 7 100       132 if ($self->{'_key'}->isa('Crypt::Perl::ECDSA::PublicKey')) {
    100          
    50          
237 3         48 $pubkey_der = $self->{'_key'}->to_der_with_curve_name();
238 3         874 $sig_alg = "ecdsa-with-SHA$digest_length";
239             }
240             elsif ($self->{'_key'}->isa('Crypt::Perl::RSA::PublicKey')) {
241 3         27 $pubkey_der = $self->{'_key'}->to_subject_der();
242 3         839 $sig_alg = "sha${digest_length}WithRSAEncryption";
243             }
244             elsif ($self->{'_key'}->isa('Crypt::Perl::Ed25519::PublicKey')) {
245 1         2 $sig_alg = 'ed25519';
246 1         8 $pubkey_der = $self->{'_key'}->to_der();
247             }
248             else {
249 0         0 die "Key ($self->{'_key'}) is not a recognized public key object!";
250             }
251              
252 7         163 my $extns_bin;
253 7 100       53 if ($self->{'_extensions'}) {
254 6         47 $extns_bin = $self->{'_extensions'}->encode();
255             }
256              
257             my $params_hr = {
258             version => { version => 2 },
259              
260             serialNumber => $self->{'_serial_number'},
261              
262             issuerUniqueID => $self->{'_issuer_unique_id'},
263              
264             subjectUniqueID => $self->{'_subject_unique_id'},
265              
266             subject => $self->{'_subject'}->encode(),
267             issuer => $self->{'_issuer'}->encode(),
268              
269             validity => {
270             notBefore => { generalTime => $self->{'_not_before'} },
271             notAfter => { generalTime => $self->{'_not_after'} },
272             },
273              
274             subjectPublicKeyInfo => $pubkey_der,
275              
276             signature => {
277 7 100       10159 algorithm => $Crypt::Perl::ASN1::Signatures::OID{$sig_alg},
278             },
279              
280             ( $extns_bin ? ( extensions => { extensions => $extns_bin } ) : () ),
281             };
282              
283 7         927 my $asn1 = Crypt::Perl::ASN1->new()->prepare($self->ASN1());
284 7         39 $asn1 = $asn1->find('TBSCertificate');
285 7         213 $asn1->configure( encode => { time => 'utctime' } );
286              
287 7         269 return ( $asn1->encode($params_hr), $digest_length );
288             }
289              
290             #sub _get_GeneralizedTime {
291             # my ($epoch) = @_;
292             #
293             # my @smhdmy = (gmtime $epoch)[0 .. 5];
294             # $smhdmy[4]++; #month
295             # $smhdmy[5] += 1900; #year
296             #
297             # return sprintf '%04d%02d%02d%02d%02d%02dZ', reverse @smhdmy;
298             #}
299              
300             1;