Source

p5-web-id / lib / Web / ID / Certificate.pm

package Web::ID::Certificate;

use 5.010;
use utf8;

BEGIN {
	$Web::ID::Certificate::AUTHORITY = 'cpan:TOBYINK';
	$Web::ID::Certificate::VERSION   = '1.922';
}

use Crypt::X509 0.50 ();  # why the hell does this export anything?!
use DateTime 0;
use MooseX::Types::Moose -all;
use Digest::SHA qw(sha1_hex);
use MIME::Base64 0 qw(decode_base64);
use Web::ID::Types -all;
use Web::ID::SAN;
use Web::ID::SAN::Email;
use Web::ID::SAN::URI;
use Web::ID::Util qw(:default part);

# Partly sorts a list of Web::ID::SAN objects,
# prioritising URIs and Email addresses.
#
# Note: placing this deliberately before namespace::clean.
#
sub _sort_san
{
	map  { ref($_) eq 'ARRAY' ? (@$_) : () }
	part {
		if ($_->isa('Web::ID::SAN::URI'))       { 0 }
		elsif ($_->isa('Web::ID::SAN::Email'))  { 1 }
		else                                    { 2 }
	}
	@_;
}

use Any::Moose;
use namespace::clean -except => 'meta';

has pem => (
	is          => read_only,
	isa         => Str,
	required    => true,
	coerce      => false,
);

has _der => (
	is          => read_only,
	isa         => Str,
	required    => true,
	lazy_build  => true,
);

has _x509 => (
	is          => read_only,
	isa         => 'Crypt::X509',
	lazy_build  => true,
);

has public_key => (
	is          => read_only,
	isa         => Rsakey,
	lazy_build  => true,
	handles     => [qw(modulus exponent)],
);

has subject_alt_names => (
	is          => read_only,
	isa         => ArrayRef,
	lazy_build  => true,
);

has $_ => (
	is          => read_only,
	isa         => Datetime,
	lazy_build  => true,
	coerce      => true,
) for qw( not_before not_after );

has san_factory => (
	is          => read_only,
	isa         => CodeRef,
	lazy_build  => true,
);

has fingerprint => (
	is          => read_only,
	isa         => Str,
	lazy_build  => true,
);

sub _build_fingerprint
{
	lc sha1_hex( shift->_der );
}

sub _build__der
{
	my @lines = split /\n/, shift->pem;
	decode_base64(join "\n", grep { !/--(BEGIN|END) CERTIFICATE--/ } @lines);
}

sub _build__x509
{
	return Crypt::X509->new(cert => shift->_der);
}

sub _build_public_key
{
	my ($self) = @_;
	Web::ID::RSAKey->new($self->_x509->pubkey_components);
}

sub _build_subject_alt_names
{
	my ($self) = @_;
	my $factory = $self->san_factory;

	[_sort_san(
		map {
			my ($type, $value) = split /=/, $_, 2;
			$factory->(type => $type, value => $value);
		}
		@{ $self->_x509->SubjectAltName }
	)];
}

sub _build_not_before
{
	my ($self) = @_;
	return $self->_x509->not_before;
}

sub _build_not_after
{
	my ($self) = @_;
	return $self->_x509->not_after;
}

my $default_san_factory = sub
{
	my (%args) = @_;
	my $class = {
			uniformResourceIdentifier  => 'Web::ID::SAN::URI',
			rfc822Name                 => 'Web::ID::SAN::Email',
		}->{ $args{type} }
		// 'Web::ID::SAN';
	$class->new(%args);
};

sub _build_san_factory
{
	return $default_san_factory;
}

sub timely
{
	my ($self, $now) = @_;
	$now //= DateTime->now;
	
	return if $now > $self->not_after;
	return if $now < $self->not_before;
	
	return $self;
}

__PACKAGE__
__END__

=head1 NAME

Web::ID::Certificate - an x509 certificate

=head1 SYNOPSIS

 my $cert = Web::ID::Certificate->new(pem => $pem_encoded_x509);
 foreach (@{ $cert->subject_alt_names })
 {
   say "SAN: ", $_->type, " = ", $_->value;
 }

=head1 DESCRIPTION

=head2 Constructor

=over

=item C<< new >>

Standard Moose-style constructor. (This class uses L<Any::Moose>.)

=back

=head2 Attributes

=over

=item C<< pem >>

A PEM-encoded string for the certificate.

This is usually the only attribute you want to pass to the constructor.
Allow the others to be built automatically.

=item C<< public_key >>

A L<Web::ID::RSAKey> object.

=item C<< fingerprint >>

A string identifier for the certificate. It is the lower-cased
hexadecimal SHA1 hash of the DER-encoded certificate.

This is not used in WebID authentication, but may be used as an
identifier for the certificate if you need to keep it in a cache.

=item C<< not_before >>

L<DateTime> object indicating when the certificate started (or will
start) to be valid.

=item C<< not_after >>

L<DateTime> object indicating when the certificate will cease (or
has ceased) to be valid.

=item C<< subject_alt_names >>

An arrayref containing a list of subject alt names (L<Web::ID::SAN>
objects) associated with the certificate. These are sorted in the order
they'll be tried for WebID authentication. 

=item C<< san_factory >>

A coderef used for building L<Web::ID::SAN> objects. It's very unlikely
you need to play with this - the default is probably OK. But changing this
is "supported" (in so much as any of this is supported).

The coderef is passed a hash (not hashref) along the lines of:

 (
   type  => 'uniformResourceIdentifier',
   value => 'http://example.com/id/alice',
 )

=back

=head2 Methods

=over

=item C<< timely >>

Checks C<not_before> and C<not_after> against the current system time to
indicate whether the certifixate is temporally valid. Returns a boolean.

You can optionally pass it a L<DateTime> object to use instead of the
current system time.

=item C<< exponent >>

Delegated to the C<public_key> attribute.

=item C<< modulus >>

Delegated to the C<public_key> attribute.

=back

=head1 BUGS

Please report any bugs to
L<http://rt.cpan.org/Dist/Display.html?Queue=Web-ID>.

=head1 SEE ALSO

L<Web::ID>,
L<Web::ID::SAN>,
L<Web::ID::RSAKey>.

L<Web::ID::Certificate::Generator> - augments this class to add the
ability to generate new WebID certificates.

L<Crypt::X509> provides a pure Perl X.509 certificate parser, and is
used internally by this module.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2012 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.