File Coverage

blib/lib/Crypt/OpenSSL/Cloner.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Crypt::OpenSSL::Cloner;
2 1     1   75713 use strict;
  1         3  
  1         40  
3 1     1   5 use warnings;
  1         1  
  1         30  
4 1     1   449 use Crypt::OpenSSL::CA;
  0            
  0            
5             use Crypt::OpenSSL::RSA;
6             use File::Slurp qw(read_file write_file);
7             use File::Path qw(mkpath);
8             use Hash::Util qw(lock_keys);
9             use Data::Dumper;
10             use Time::HiRes;
11             use MIME::Base64 qw(decode_base64);
12             use Convert::ASN1;
13             #use Encoding::BER::DER;
14              
15             use Crypt::OpenSSL::Cloner::x509asn1;
16              
17             our $PREFERRED_ALG = "sha1";
18             our $PREFERRED_KEYLENGTH = 1024;
19             our $CA_BASENAME = "CA";
20             our $VERSION = 0.04;
21              
22             my $ASN = Convert::ASN1->new();
23             $ASN->prepare($Crypt::OpenSSL::Cloner::x509asn1::ASN_DEF,
24             encoding => "DER") or die "GRRR";
25              
26             my %PARSERS = map { $_, $ASN->find($_) } qw(
27             SubjectKeyIdentifier
28             BasicConstraints
29             KeyUsage
30             CertificatePolicies
31             SubjectAltName
32             );
33              
34             my %oid_2_ext = (
35             '2.5.29.14' => "SubjectKeyIdentifier",
36             '2.5.29.17' => "SubjectAltName",
37             '2.5.29.37' => "KeyUsage",
38             '2.5.29.32' => 'CertificatePolicies',
39             '2.5.29.19' => "BasicConstraints",
40            
41             );
42              
43             my @FIELDS = qw(
44             PATH
45             CA_OBJ
46             PRIVKEY_OBJ
47             AUTH_KEY_ID
48             );
49              
50             #To make sure we don't get a timestamp serial collision (unlikely but possible)
51             my %TS_SERIALS = ();
52              
53             sub load_ca {
54             my ($self) = @_;
55             my $path = $self->{PATH};
56             my ($privkey_obj,$privkey_string,$ca_obj,$pem);
57             eval {
58             $privkey_string = read_file($path . "/$CA_BASENAME.key");
59             $pem = read_file($path."/$CA_BASENAME.pem");
60             };
61             return if $@;
62            
63             $privkey_obj = Crypt::OpenSSL::CA::PrivateKey->parse($privkey_string);
64             $ca_obj = Crypt::OpenSSL::CA::X509->parse($pem);
65            
66             return unless ($privkey_obj && $ca_obj);
67             return [$ca_obj,$privkey_obj];
68             }
69              
70             sub _gen_new_ca {
71             my ($self,$dn_hash) = @_;
72             my $rsa = Crypt::OpenSSL::RSA->generate_key($PREFERRED_KEYLENGTH);
73             my $privkey = Crypt::OpenSSL::CA::PrivateKey->parse(
74             $rsa->get_private_key_string
75             );
76             my $ca = Crypt::OpenSSL::CA::X509->new($privkey->get_public_key);
77             my $dn = Crypt::OpenSSL::CA::X509_NAME->new(%$dn_hash);
78             my $keyid = $privkey->get_public_key->get_openssl_keyid();
79             die "Need Distinguished Name for CA" if !$dn_hash;
80             $ca->set_serial("0x1");
81             $ca->set_notBefore("20080204101500Z");
82             $ca->set_notAfter("20220204101500Z");
83             $ca->set_subject_DN($dn);
84             $ca->set_issuer_DN($dn);
85             $ca->set_extension("subjectKeyIdentifier", $keyid);
86             $ca->set_extension("authorityKeyIdentifier", {
87             keyid => $keyid,
88             issuer => $dn,
89             serial => "0x1"
90             });
91            
92             $ca->set_extension("basicConstraints", "CA:TRUE", -critical => 1);
93             #$ca->set_extension("keyUsage" =>
94             # "digitalSignature, nonRepudiation,".
95             # "keyEncipherment, dataEncipherment, keyAgreement,".
96             # "keyCertSign, cRLSign");
97             my $crt_text = $ca->sign($privkey, $PREFERRED_ALG);
98             return [$ca,$privkey,$crt_text,$rsa->get_private_key_string];
99             }
100              
101             sub new {
102             my ($cls,%opts) = @_;
103             my $self = {};
104             bless ($self, $cls);
105             lock_keys(%$self, @FIELDS);
106             my $path = delete $opts{path} or die "Must have CA path";
107             my $dn_hash = delete $opts{dn};
108             $dn_hash ||= {
109             C => 'GB',
110             O => 'CertOnTheFly',
111             OU => "CertOnTheFly Certificate Generation",
112             CN => 'CertOnTheFly Root'
113             };
114            
115             mkpath($path);
116             $self->{PATH} = $path;
117            
118             my ($ca_obj,$privkey_obj);
119             my $res = $self->load_ca();
120             if ($res) {
121             ($ca_obj,$privkey_obj) = @$res;
122             } else {
123             my ($pem,$keytxt);
124             ($ca_obj,$privkey_obj,$pem,$keytxt) = @{ $self->_gen_new_ca($dn_hash) };
125             write_file($path . "/$CA_BASENAME.pem", $pem);
126             write_file($path . "/$CA_BASENAME.key", $keytxt);
127             }
128             $self->{CA_OBJ} = $ca_obj;
129             $self->{PRIVKEY_OBJ} = $privkey_obj;
130             return $self;
131             }
132              
133              
134             sub clone_cert {
135             my ($self,$pem,$domain_name) = @_;
136             my $keystr = Crypt::OpenSSL::RSA->generate_key(1024)->get_private_key_string();
137             my $privkey = Crypt::OpenSSL::CA::PrivateKey->parse($keystr);
138             my $new_cert = Crypt::OpenSSL::CA::X509->new($privkey->get_public_key);
139            
140             my $alt_name_string = ($domain_name) ? "DNS:$domain_name" : "";
141            
142             $new_cert->set_subject_DN(Crypt::OpenSSL::CA::X509->parse($pem)->get_subject_DN);
143             $new_cert->set_issuer_DN($self->{CA_OBJ}->get_issuer_DN);
144             $new_cert->set_notBefore("20080204114600Z");
145             $new_cert->set_notAfter("20220204114600Z");
146             $new_cert->set_extension("authorityKeyIdentifier",
147             { keyid => $self->{CA_OBJ}->get_subject_keyid });
148             my $serial = time();
149             $serial .= $TS_SERIALS{$serial}++;
150             $serial = "0x$serial";
151             $new_cert->set_serial($serial);
152             my %extracted;
153             my $blob = $pem;
154             $blob =~ s/-----(BEGIN|END)\sCERTIFICATE-----//msg;
155             $blob = decode_base64($blob);
156             my $rootparse = $ASN->find("Certificate");
157             my $extensions = $rootparse->decode($blob);
158             $extensions = $extensions->{tbsCertificate}->{extensions};
159             foreach my $ext (@$extensions) {
160             my $oid = $ext->{extnID};
161             my $extname = $oid_2_ext{$oid};
162             next if !$extname;
163             my $der = $ext->{extnValue};
164             my $parser = $PARSERS{$extname};
165             my $decoded = $parser->decode($der);
166             if ($extname eq 'SubjectKeyIdentifier') {
167             $new_cert->set_extension(
168             "subjectKeyIdentifier", unpack('H*', $decoded));
169             } elsif ($extname eq 'KeyUsage') {
170             #Then try to figure that out, too..
171             #Apparently this module has a different way of doing things...
172             # Our sample cert doesn't seem to conform to this.. and using
173             # A parse of another module, seems to be using OIDs for
174             # ExtendedKeyUsage?
175             } elsif ($extname eq 'SubjectAltName') {
176             #ASN:
177             #
178             #GeneralName ::= CHOICE {
179             #otherName [0] AnotherName,
180             #rfc822Name [1] IA5String,
181             #dNSName [2] IA5String,
182             #x400Address [3] ANY, --ORAddress,
183             #directoryName [4] Name,
184             #ediPartyName [5] EDIPartyName,
185             #uniformResourceIdentifier [6] IA5String,
186             #iPAddress [7] OCTET STRING,
187             #registeredID [8] OBJECT IDENTIFIER }
188              
189             my %asn2openssl = (
190             otherName => "otherName",
191             rfc822name => "email",
192             dNSName => "DNS",
193             x400Address => "dirName",
194             #ediPartyName => "what's this?",
195             directoryName => "dirName",
196             uniformResourceIdentifier => "URI",
197             iPAddress => "IP",
198             registeredID => "RID",
199             );
200             my @altnames;
201             my $altname = "";
202             foreach my $h (@$decoded) {
203             my ($k,$v) = (%$h);
204             my $new_k = $asn2openssl{$k};
205             if (!$new_k) {
206             warn "Found ASN.1 X509 field $k which doesn't have an OpenSSL mapping";
207             next;
208             }
209             $k = $new_k;
210             push @altnames, "$k:$v";
211             }
212             $altname = join(",", @altnames);
213             if ($alt_name_string) {
214             $alt_name_string .= ",$altname";
215             } else {
216             $alt_name_string = $altname;
217             }
218             }
219             }
220             $new_cert->set_extension("subjectAltName", $alt_name_string) if $alt_name_string;
221             my $new_pem = $new_cert->sign($self->{PRIVKEY_OBJ}, $PREFERRED_ALG);
222             return ($new_pem, $keystr);
223             }
224              
225             1;
226              
227             __END__