IO-Socket-SSL

 view release on metacpan or  search on metacpan

lib/IO/Socket/SSL/Utils.pm  view on Meta::CPAN


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 (@_) {

lib/IO/Socket/SSL/Utils.pm  view on Meta::CPAN


    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);

lib/IO/Socket/SSL/Utils.pm  view on Meta::CPAN

	} 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.

lib/IO/Socket/SSL/Utils.pm  view on Meta::CPAN

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<sha256WithRSAEncryption>.

=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<CERT_asHash>.
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



( run in 1.125 second using v1.01-cache-2.11-cpan-39bf76dae61 )