package IO::Socket::SSL::Utils; use strict; use warnings; use Carp 'croak'; use Net::SSLeay; # old versions of Exporter do not export 'import' yet require Exporter; *import = \&Exporter::import; our $VERSION = '2.015'; our @EXPORT = qw( PEM_file2cert PEM_file2certs PEM_string2cert PEM_cert2file PEM_certs2file PEM_cert2string PEM_file2key PEM_string2key PEM_key2file PEM_key2string KEY_free CERT_free KEY_create_rsa CERT_asHash CERT_create ); sub PEM_file2cert { my $file = shift; my $bio = Net::SSLeay::BIO_new_file($file,'r') or croak "cannot read $file: $!"; my $cert = Net::SSLeay::PEM_read_bio_X509($bio); Net::SSLeay::BIO_free($bio); $cert or croak "cannot parse $file as PEM X509 cert: ". Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); return $cert; } sub PEM_cert2file { my ($cert,$file) = @_; my $string = Net::SSLeay::PEM_get_string_X509($cert) or croak("cannot get string from cert"); open( my $fh,'>',$file ) or croak("cannot write $file: $!"); print $fh $string; } use constant PEM_R_NO_START_LINE => 108; sub PEM_file2certs { my $file = shift; my $bio = Net::SSLeay::BIO_new_file($file,'r') or croak "cannot read $file: $!"; my @certs; while (1) { if (my $cert = Net::SSLeay::PEM_read_bio_X509($bio)) { push @certs, $cert; } else { Net::SSLeay::BIO_free($bio); my $error = Net::SSLeay::ERR_get_error(); last if ($error & 0xfff) == PEM_R_NO_START_LINE && @certs; croak "cannot parse $file as PEM X509 cert: " . Net::SSLeay::ERR_error_string($error); } } return @certs; } sub PEM_certs2file { my $file = shift; open( my $fh,'>',$file ) or croak("cannot write $file: $!"); for my $cert (@_) { my $string = Net::SSLeay::PEM_get_string_X509($cert) or croak("cannot get string from cert"); print $fh $string; } } sub PEM_string2cert { my $string = shift; my $bio = Net::SSLeay::BIO_new( Net::SSLeay::BIO_s_mem()); Net::SSLeay::BIO_write($bio,$string); my $cert = Net::SSLeay::PEM_read_bio_X509($bio); Net::SSLeay::BIO_free($bio); $cert or croak "cannot parse string as PEM X509 cert: ". Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); return $cert; } sub PEM_cert2string { my $cert = shift; return Net::SSLeay::PEM_get_string_X509($cert) || croak("cannot get string from cert"); } sub PEM_file2key { my $file = shift; my $bio = Net::SSLeay::BIO_new_file($file,'r') or croak "cannot read $file: $!"; my $key = Net::SSLeay::PEM_read_bio_PrivateKey($bio); Net::SSLeay::BIO_free($bio); $key or croak "cannot parse $file as PEM private key: ". Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); return $key; } sub PEM_key2file { my ($key,$file) = @_; my $string = Net::SSLeay::PEM_get_string_PrivateKey($key) or croak("cannot get string from key"); open( my $fh,'>',$file ) or croak("cannot write $file: $!"); print $fh $string; } sub PEM_string2key { my $string = shift; my $bio = Net::SSLeay::BIO_new( Net::SSLeay::BIO_s_mem()); Net::SSLeay::BIO_write($bio,$string); my $key = Net::SSLeay::PEM_read_bio_PrivateKey($bio); Net::SSLeay::BIO_free($bio); $key or croak "cannot parse string as PEM private key: ". Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); return $key; } sub PEM_key2string { my $key = shift; return Net::SSLeay::PEM_get_string_PrivateKey($key) || croak("cannot get string from key"); } sub CERT_free { Net::SSLeay::X509_free($_) for @_; } sub KEY_free { Net::SSLeay::EVP_PKEY_free($_) for @_; } sub KEY_create_rsa { my $bits = shift || 2048; my $key = Net::SSLeay::EVP_PKEY_new(); my $rsa = Net::SSLeay::RSA_generate_key($bits, 0x10001); # 0x10001 = RSA_F4 Net::SSLeay::EVP_PKEY_assign_RSA($key,$rsa); return $key; } if (defined &Net::SSLeay::EC_KEY_generate_key) { push @EXPORT,'KEY_create_ec'; *KEY_create_ec = sub { my $curve = shift || 'prime256v1'; my $key = Net::SSLeay::EVP_PKEY_new(); my $ec = Net::SSLeay::EC_KEY_generate_key($curve); Net::SSLeay::EVP_PKEY_assign_EC_KEY($key,$ec); return $key; } } # extract information from cert my %gen2i = qw( OTHERNAME 0 EMAIL 1 DNS 2 X400 3 DIRNAME 4 EDIPARTY 5 URI 6 IP 7 RID 8 ); my %i2gen = reverse %gen2i; sub CERT_asHash { my $cert = shift; my $digest_name = shift || 'sha256'; my %hash = ( version => Net::SSLeay::X509_get_version($cert), not_before => _asn1t2t(Net::SSLeay::X509_get_notBefore($cert)), not_after => _asn1t2t(Net::SSLeay::X509_get_notAfter($cert)), serial => Net::SSLeay::P_ASN1_INTEGER_get_dec( Net::SSLeay::X509_get_serialNumber($cert)), signature_alg => Net::SSLeay::OBJ_obj2txt ( Net::SSLeay::P_X509_get_signature_alg($cert)), crl_uri => [ Net::SSLeay::P_X509_get_crl_distribution_points($cert) ], keyusage => [ Net::SSLeay::P_X509_get_key_usage($cert) ], extkeyusage => { oid => [ Net::SSLeay::P_X509_get_ext_key_usage($cert,0) ], nid => [ Net::SSLeay::P_X509_get_ext_key_usage($cert,1) ], sn => [ Net::SSLeay::P_X509_get_ext_key_usage($cert,2) ], ln => [ Net::SSLeay::P_X509_get_ext_key_usage($cert,3) ], }, "pubkey_digest_$digest_name" => Net::SSLeay::X509_pubkey_digest( $cert,_digest($digest_name)), "x509_digest_$digest_name" => Net::SSLeay::X509_digest( $cert,_digest($digest_name)), "fingerprint_$digest_name" => Net::SSLeay::X509_get_fingerprint( $cert,$digest_name), ); for([ subject => Net::SSLeay::X509_get_subject_name($cert) ], [ issuer => Net::SSLeay::X509_get_issuer_name($cert) ]) { my ($what,$subj) = @$_; my %subj; for ( 0..Net::SSLeay::X509_NAME_entry_count($subj)-1 ) { my $e = Net::SSLeay::X509_NAME_get_entry($subj,$_); my $k = Net::SSLeay::OBJ_obj2txt( Net::SSLeay::X509_NAME_ENTRY_get_object($e)); my $v = Net::SSLeay::P_ASN1_STRING_get( Net::SSLeay::X509_NAME_ENTRY_get_data($e)); if (!exists $subj{$k}) { $subj{$k} = $v; } elsif (!ref $subj{$k}) { $subj{$k} = [ $subj{$k}, $v ]; } else { push @{$subj{$k}}, $v; } } $hash{$what} = \%subj; } if ( my @names = Net::SSLeay::X509_get_subjectAltNames($cert) ) { my $alt = $hash{subjectAltNames} = []; while (my ($t,$v) = splice(@names,0,2)) { $t = $i2gen{$t} || die "unknown type $t in subjectAltName"; if ( $t eq 'IP' ) { if (length($v) == 4) { $v = join('.',unpack("CCCC",$v)); } elsif ( length($v) == 16 ) { my @v = unpack("nnnnnnnn",$v); my ($best0,$last0); for(my $i=0;$i<@v;$i++) { if ($v[$i] == 0) { if ($last0) { $last0->[1] = $i; $last0->[2]++; $best0 = $last0 if ++$last0->[2]>$best0->[2]; } else { $last0 = [ $i,$i,0 ]; $best0 ||= $last0; } } else { $last0 = undef; } } if ($best0) { $v = ''; $v .= join(':', map { sprintf( "%x",$_) } @v[0..$best0->[0]-1]) if $best0->[0]>0; $v .= '::'; $v .= join(':', map { sprintf( "%x",$_) } @v[$best0->[1]+1..$#v]) if $best0->[1]<$#v; } else { $v = join(':', map { sprintf( "%x",$_) } @v); } } } push @$alt,[$t,$v] } } my @ext; for( 0..Net::SSLeay::X509_get_ext_count($cert)-1 ) { my $e = Net::SSLeay::X509_get_ext($cert,$_); my $o = Net::SSLeay::X509_EXTENSION_get_object($e); my $nid = Net::SSLeay::OBJ_obj2nid($o); push @ext, { oid => Net::SSLeay::OBJ_obj2txt($o), nid => ( $nid > 0 ) ? $nid : undef, sn => ( $nid > 0 ) ? Net::SSLeay::OBJ_nid2sn($nid) : undef, critical => Net::SSLeay::X509_EXTENSION_get_critical($e), data => Net::SSLeay::X509V3_EXT_print($e), } } $hash{ext} = \@ext; if ( defined(&Net::SSLeay::P_X509_get_ocsp_uri)) { $hash{ocsp_uri} = [ Net::SSLeay::P_X509_get_ocsp_uri($cert) ]; } else { $hash{ocsp_uri} = []; for( @ext ) { $_->{sn} or next; $_->{sn} eq 'authorityInfoAccess' or next; push @{ $hash{ocsp_uri}}, $_->{data} =~m{\bOCSP - URI:(\S+)}g; } } return \%hash; } sub CERT_create { my %args = @_%2 ? %{ shift() } : @_; my $cert = Net::SSLeay::X509_new(); my $digest_name = delete $args{digest} || 'sha256'; Net::SSLeay::ASN1_INTEGER_set( Net::SSLeay::X509_get_serialNumber($cert), delete $args{serial} || rand(2**32), ); # version default to 2 (V3) Net::SSLeay::X509_set_version($cert, delete $args{version} || 2 ); # not_before default to now Net::SSLeay::ASN1_TIME_set( Net::SSLeay::X509_get_notBefore($cert), delete $args{not_before} || time() ); # not_after default to now+365 days Net::SSLeay::ASN1_TIME_set( Net::SSLeay::X509_get_notAfter($cert), delete $args{not_after} || time() + 365*86400 ); # set subject my $subj_e = Net::SSLeay::X509_get_subject_name($cert); my $subj = delete $args{subject} || { organizationName => 'IO::Socket::SSL', commonName => 'IO::Socket::SSL Test' }; while ( my ($k,$v) = each %$subj ) { # Not everything we get is nice - try with MBSTRING_UTF8 first and if it # fails try V_ASN1_T61STRING and finally V_ASN1_OCTET_STRING for (ref($v) ? @$v : ($v)) { Net::SSLeay::X509_NAME_add_entry_by_txt($subj_e,$k,0x1000,$_,-1,0) or Net::SSLeay::X509_NAME_add_entry_by_txt($subj_e,$k,20,$_,-1,0) or Net::SSLeay::X509_NAME_add_entry_by_txt($subj_e,$k,4,$_,-1,0) or croak("failed to add entry for $k - ". Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error())); } } my @ext = ( &Net::SSLeay::NID_subject_key_identifier => 'hash', &Net::SSLeay::NID_authority_key_identifier => 'keyid', ); if ( my $altsubj = delete $args{subjectAltNames} ) { push @ext, &Net::SSLeay::NID_subject_alt_name => join(',', map { "$_->[0]:$_->[1]" } @$altsubj) } my $key = delete $args{key} || KEY_create_rsa(); Net::SSLeay::X509_set_pubkey($cert,$key); my $is = delete $args{issuer}; my $issuer_cert = delete $args{issuer_cert} || $is && $is->[0] || $cert; my $issuer_key = delete $args{issuer_key} || $is && $is->[1] || $key; my %purpose; if (my $p = delete $args{purpose}) { if (!ref($p)) { $purpose{lc($2)} = (!$1 || $1 eq '+') ? 1:0 while $p =~m{([+-]?)(\w+)}g; } elsif (ref($p) eq 'ARRAY') { for(@$p) { m{^([+-]?)(\w+)$} or die "invalid entry in purpose: $_"; $purpose{lc($2)} = (!$1 || $1 eq '+') ? 1:0 } } else { while( my ($k,$v) = each %$p) { $purpose{lc($k)} = ($v && $v ne '-')?1:0; } } } if (delete $args{CA}) { # add defaults for CA %purpose = ( ca => 1, sslca => 1, emailca => 1, objca => 1, %purpose ); } if (!%purpose) { %purpose = (server => 1, client => 1); } my (%key_usage,%ext_key_usage,%cert_type,%basic_constraints); my %dS = ( digitalSignature => \%key_usage ); my %kE = ( keyEncipherment => \%key_usage ); my %CA = ( 'CA:TRUE' => \%basic_constraints, %dS, keyCertSign => \%key_usage ); my @disable; for( [ client => { %dS, %kE, clientAuth => \%ext_key_usage, client => \%cert_type } ], [ server => { %dS, %kE, serverAuth => \%ext_key_usage, server => \%cert_type } ], [ email => { %dS, %kE, emailProtection => \%ext_key_usage, email => \%cert_type } ], [ objsign => { %dS, %kE, codeSigning => \%ext_key_usage, objsign => \%cert_type } ], [ CA => { %CA }], [ sslCA => { %CA, sslCA => \%cert_type }], [ emailCA => { %CA, emailCA => \%cert_type }], [ objCA => { %CA, objCA => \%cert_type }], [ emailProtection => { %dS, %kE, emailProtection => \%ext_key_usage, email => \%cert_type } ], [ codeSigning => { %dS, %kE, codeSigning => \%ext_key_usage, objsign => \%cert_type } ], [ timeStamping => { timeStamping => \%ext_key_usage } ], [ digitalSignature => { digitalSignature => \%key_usage } ], [ nonRepudiation => { nonRepudiation => \%key_usage } ], [ keyEncipherment => { keyEncipherment => \%key_usage } ], [ dataEncipherment => { dataEncipherment => \%key_usage } ], [ keyAgreement => { keyAgreement => \%key_usage } ], [ keyCertSign => { keyCertSign => \%key_usage } ], [ cRLSign => { cRLSign => \%key_usage } ], [ encipherOnly => { encipherOnly => \%key_usage } ], [ decipherOnly => { decipherOnly => \%key_usage } ], [ clientAuth => { clientAuth => \%ext_key_usage } ], [ serverAuth => { serverAuth => \%ext_key_usage } ], ) { exists $purpose{lc($_->[0])} or next; if (delete $purpose{lc($_->[0])}) { while (my($k,$h) = each %{$_->[1]}) { $h->{$k} = 1; } } else { push @disable, $_->[1]; } } die "unknown purpose ".join(",",keys %purpose) if %purpose; for(@disable) { while (my($k,$h) = each %$_) { delete $h->{$k}; } } if (%basic_constraints) { push @ext,&Net::SSLeay::NID_basic_constraints, => join(",",'critical', sort keys %basic_constraints); } else { push @ext, &Net::SSLeay::NID_basic_constraints => 'critical,CA:FALSE'; } push @ext,&Net::SSLeay::NID_key_usage => join(",",'critical', sort keys %key_usage) if %key_usage; push @ext,&Net::SSLeay::NID_netscape_cert_type => join(",",sort keys %cert_type) if %cert_type; push @ext,&Net::SSLeay::NID_ext_key_usage => join(",",sort keys %ext_key_usage) if %ext_key_usage; Net::SSLeay::P_X509_add_extensions($cert, $issuer_cert, @ext); my %have_ext; for(my $i=0;$i<@ext;$i+=2) { $have_ext{ $ext[$i] }++ } for my $ext (@{ delete $args{ext} || [] }) { my $nid = $ext->{nid} || $ext->{sn} && Net::SSLeay::OBJ_sn2nid($ext->{sn}) || croak "cannot determine NID of extension"; $have_ext{$nid} and next; my $val = $ext->{data}; if ($nid == 177) { # authorityInfoAccess: # OpenSSL i2v does not output the same way as expected by i2v :( for (split(/\n/,$val)) { s{ - }{;}; # "OCSP - URI:..." -> "OCSP;URI:..." $_ = "critical,$_" if $ext->{critical}; Net::SSLeay::P_X509_add_extensions($cert,$issuer_cert,$nid,$_); } } else { $val = "critical,$val" if $ext->{critical}; Net::SSLeay::P_X509_add_extensions($cert, $issuer_cert, $nid, $val); } } die "unknown arguments: ". join(" ", sort keys %args) if !delete $args{ignore_invalid_args} && %args; Net::SSLeay::X509_set_issuer_name($cert, Net::SSLeay::X509_get_subject_name($issuer_cert)); Net::SSLeay::X509_sign($cert,$issuer_key,_digest($digest_name)); return ($cert,$key); } if ( defined &Net::SSLeay::ASN1_TIME_timet ) { *_asn1t2t = \&Net::SSLeay::ASN1_TIME_timet } else { require Time::Local; my %mon2i = qw( Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11 ); *_asn1t2t = sub { my $t = Net::SSLeay::P_ASN1_TIME_put2string( shift ); my ($mon,$d,$h,$m,$s,$y,$tz) = split(/[\s:]+/,$t); defined( $mon = $mon2i{$mon} ) or die "invalid month in $t"; $tz ||= $y =~s{^(\d+)([A-Z]\S*)}{$1} && $2; if ( ! $tz ) { return Time::Local::timelocal($s,$m,$h,$d,$mon,$y) } elsif ( $tz eq 'GMT' ) { return Time::Local::timegm($s,$m,$h,$d,$mon,$y) } else { die "unexpected TZ $tz from ASN1_TIME_print"; } } } { my %digest; sub _digest { my $digest_name = shift; return $digest{$digest_name} ||= do { Net::SSLeay::SSLeay_add_ssl_algorithms(); Net::SSLeay::EVP_get_digestbyname($digest_name) or die "Digest algorithm $digest_name is not available"; }; } } 1; __END__ =head1 NAME IO::Socket::SSL::Utils -- loading, storing, creating certificates and keys =head1 SYNOPSIS use IO::Socket::SSL::Utils; $cert = PEM_file2cert('cert.pem'); # load certificate from file my $hash = CERT_asHash($cert); # get details from certificate PEM_cert2file($cert,'cert.pem'); # write certificate to file CERT_free($cert); # free memory within OpenSSL @certs = PEM_file2certs('chain.pem'); # load multiple certificates from file PEM_certs2file('chain.pem', @certs); # write multiple certificates to file CERT_free(@certs); # free memory for all within OpenSSL my $cert = PEM_string2cert($pem); # load certificate from PEM string $pem = PEM_cert2string($cert); # convert certificate to PEM string $key = KEY_create_rsa(2048); # create new 2048-bit RSA key PEM_key2file($key,"key.pem"); # and write it to file KEY_free($key); # free memory within OpenSSL =head1 DESCRIPTION This module provides various utility functions to work with certificates and private keys, shielding some of the complexity of the underlying Net::SSLeay and OpenSSL. =head1 FUNCTIONS =over 4 =item * Functions converting between string or file and certificates and keys. They croak if the operation cannot be completed. =over 8 =item PEM_file2cert(file) -> cert =item PEM_cert2file(cert,file) =item PEM_file2certs(file) -> @certs =item PEM_certs2file(file,@certs) =item PEM_string2cert(string) -> cert =item PEM_cert2string(cert) -> string =item PEM_file2key(file) -> key =item PEM_key2file(key,file) =item PEM_string2key(string) -> key =item PEM_key2string(key) -> string =back =item * Functions for cleaning up. Each loaded or created cert and key must be freed to not leak memory. =over 8 =item CERT_free(@certs) =item KEY_free(@keys) =back =item * KEY_create_rsa(bits) -> key Creates an RSA key pair, bits defaults to 2048. =item * KEY_create_ec(curve) -> key Creates an EC key, curve defaults to C. =item * CERT_asHash(cert,[digest_algo]) -> hash Extracts the information from the certificate into a hash and uses the given digest_algo (default: SHA-256) to determine digest of pubkey and cert. The resulting hash contains: =over 8 =item subject Hash with the parts of the subject, e.g. commonName, countryName, organizationName, stateOrProvinceName, localityName. If there are multiple values for any of these parts the hash value will be an array ref with the values in order instead of just a scalar. =item subjectAltNames Array with list of alternative names. Each entry in the list is of C<[type,value]>, where C can be OTHERNAME, EMAIL, DNS, X400, DIRNAME, EDIPARTY, URI, IP or RID. =item issuer Hash with the parts of the issuer, e.g. commonName, countryName, organizationName, stateOrProvinceName, localityName. If there are multiple values for any of these parts the hash value will be an array ref with the values in order instead of just a scalar. =item not_before, not_after The time frame, where the certificate is valid, as time_t, e.g. can be converted with localtime or similar functions. =item serial The serial number =item crl_uri List of URIs for CRL distribution. =item ocsp_uri List of URIs for revocation checking using OCSP. =item keyusage List of keyUsage information in the certificate. =item extkeyusage List of extended key usage information from the certificate. Each entry in this list consists of a hash with oid, nid, ln and sn. =item pubkey_digest_xxx Binary digest of the pubkey using the given digest algorithm, e.g. pubkey_digest_sha256 if (the default) SHA-256 was used. =item x509_digest_xxx Binary digest of the X.509 certificate using the given digest algorithm, e.g. x509_digest_sha256 if (the default) SHA-256 was used. =item fingerprint_xxx Fingerprint of the certificate using the given digest algorithm, e.g. fingerprint_sha256 if (the default) SHA-256 was used. Contrary to digest_* this is an ASCII string with a list if hexadecimal numbers, e.g. "73:59:75:5C:6D...". =item signature_alg Algorithm used to sign certificate, e.g. C. =item ext List of extensions. Each entry in the list is a hash with oid, nid, sn, critical flag (boolean) and data (string representation given by X509V3_EXT_print). =item version Certificate version, usually 2 (x509v3) =back =item * CERT_create(hash) -> (cert,key) Creates a certificate based on the given hash. If the issuer is not specified the certificate will be self-signed. The following keys can be given: =over 8 =item subject Hash with the parts of the subject, e.g. commonName, countryName, ... as described in C. Default points to IO::Socket::SSL. =item not_before A time_t value when the certificate starts to be valid. Defaults to current time. =item not_after A time_t value when the certificate ends to be valid. Defaults to current time plus one 365 days. =item serial The serial number. If not given a random number will be used. =item version The version of the certificate, default 2 (x509v3). =item CA true|false If true declare certificate as CA, defaults to false. =item purpose string|array|hash Set the purpose of the certificate. The different purposes can be given as a string separated by non-word character, as array or hash. With string or array each purpose can be prefixed with '+' (enable) or '-' (disable) and same can be done with the value when given as a hash. By default enabling the purpose is assumed. If the CA option is given and true the defaults "ca,sslca,emailca,objca" are assumed, but can be overridden with explicit purpose. If the CA option is given and false the defaults "server,client" are assumed. If no CA option and no purpose is given it defaults to "server,client". Purpose affects basicConstraints, keyUsage, extKeyUsage and netscapeCertType. The following purposes are defined (case is not important): client server email objsign CA sslCA emailCA objCA emailProtection codeSigning timeStamping digitalSignature nonRepudiation keyEncipherment dataEncipherment keyAgreement keyCertSign cRLSign encipherOnly decipherOnly Examples: # root-CA for SSL certificates purpose => 'sslCA' # or CA => 1 # server certificate and CA (typically self-signed) purpose => 'sslCA,server' # client certificate purpose => 'client', =item ext [{ sn => .., data => ... }, ... ] List of extensions. The type of the extension can be specified as name with C or as NID with C and the data with C. These data must be in the same syntax as expected within openssl.cnf, e.g. something like C. Additionally the critical flag can be set with C 1>. =item key key use given key as key for certificate, otherwise a new one will be generated and returned =item issuer_cert cert set issuer for new certificate =item issuer_key key sign new certificate with given key =item issuer [ cert, key ] Instead of giving issuer_key and issuer_cert as separate arguments they can be given both together. =item digest algorithm specify the algorithm used to sign the certificate, default SHA-256. =item ignore_invalid_args ignore any unknown arguments which might be in the argument list (which might be in the arguments for example as result from CERT_asHash) =back =back =head1 AUTHOR Steffen Ullrich