Skip to content

Commit c013d5b

Browse files
committed
initial checkin
1 parent 5dc6d1a commit c013d5b

File tree

8 files changed

+452
-0
lines changed

8 files changed

+452
-0
lines changed

Changes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Release history for URI-Signature-Tiny
2+
3+
1.000 Wed 28 Oct 2020
4+
- Initial release

Makefile.PL

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use 5.006; use strict; use warnings;
2+
3+
my $u = 'github.com/ap/URI-Signature-Tiny';
4+
5+
my %META = (
6+
name => 'URI-Signature-Tiny',
7+
author => 'Aristotle Pagaltzis <[email protected]>',
8+
license => 'perl_5',
9+
x_copyright => { holder => 'Aristotle Pagaltzis', year => 2020 },
10+
prereqs => {
11+
runtime => { requires => {qw(
12+
perl 5.006
13+
Digest::SHA 0
14+
Scalar::Util 0
15+
Carp 0
16+
)} },
17+
test => { requires => {qw(
18+
Test::More 0
19+
URI 0
20+
URI::WithBase 0
21+
)} },
22+
},
23+
dynamic_config => 0,
24+
resources => {
25+
repository => { type => 'git', url => "git://$u.git", web => "https://$u" },
26+
bugtracker => { web => "https://$u/issues" },
27+
},
28+
);
29+
30+
sub MY::postamble { -f 'META.yml' ? return : <<'' }
31+
create_distdir : MANIFEST
32+
MANIFEST :
33+
( git ls-files ':!README.pod' . ; echo MANIFEST ) > MANIFEST
34+
distdir : boilerplate
35+
.PHONY : boilerplate
36+
boilerplate : distmeta
37+
$(PERL) -Ilib boilerplate.pl $(DISTVNAME)
38+
39+
## BOILERPLATE ###############################################################
40+
require ExtUtils::MakeMaker;
41+
42+
my %MM_ARGS;
43+
44+
# have to do this since old EUMM dev releases miss the eval $VERSION line
45+
my $eumm_version = eval $ExtUtils::MakeMaker::VERSION;
46+
my $mymeta = $eumm_version >= 6.57_02;
47+
my $mymeta_broken = $mymeta && $eumm_version < 6.57_07;
48+
49+
($MM_ARGS{NAME} = $META{name}) =~ s/-/::/g;
50+
($MM_ARGS{VERSION_FROM} = "lib/$MM_ARGS{NAME}.pm") =~ s{::}{/}g;
51+
($MM_ARGS{ABSTRACT_FROM} = $MM_ARGS{VERSION_FROM}) =~ s{\.pm\z}{.pod};
52+
$META{license} = [ $META{license} ]
53+
if $META{license} && !ref $META{license};
54+
$MM_ARGS{LICENSE} = $META{license}[0]
55+
if $META{license} && $eumm_version >= 6.30;
56+
$MM_ARGS{NO_MYMETA} = 1
57+
if $mymeta_broken;
58+
$MM_ARGS{META_ADD} = { 'meta-spec' => { version => 2 }, %META }
59+
unless -f 'META.yml';
60+
$MM_ARGS{PL_FILES} ||= {};
61+
$MM_ARGS{NORECURS} = 1
62+
if not exists $MM_ARGS{NORECURS};
63+
64+
for (qw(configure build test runtime)) {
65+
my $key = $_ eq 'runtime' ? 'PREREQ_PM' : uc $_.'_REQUIRES';
66+
my $r = $MM_ARGS{$key} = {
67+
%{$META{prereqs}{$_}{requires} || {}},
68+
%{delete $MM_ARGS{$key} || {}},
69+
};
70+
defined $r->{$_} or delete $r->{$_} for keys %$r;
71+
}
72+
73+
$MM_ARGS{MIN_PERL_VERSION} = delete $MM_ARGS{PREREQ_PM}{perl} || 0;
74+
75+
delete $MM_ARGS{MIN_PERL_VERSION}
76+
if $eumm_version < 6.47_01;
77+
$MM_ARGS{BUILD_REQUIRES} = {%{$MM_ARGS{BUILD_REQUIRES}}, %{delete $MM_ARGS{TEST_REQUIRES}}}
78+
if $eumm_version < 6.63_03;
79+
$MM_ARGS{PREREQ_PM} = {%{$MM_ARGS{PREREQ_PM}}, %{delete $MM_ARGS{BUILD_REQUIRES}}}
80+
if $eumm_version < 6.55_01;
81+
delete $MM_ARGS{CONFIGURE_REQUIRES}
82+
if $eumm_version < 6.51_03;
83+
84+
ExtUtils::MakeMaker::WriteMakefile(%MM_ARGS);
85+
## END BOILERPLATE ###########################################################

README.pod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lib/URI/Signature/Tiny.pod

boilerplate.pl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use strict; use warnings;
2+
3+
use CPAN::Meta;
4+
use Software::LicenseUtils;
5+
use Pod::Readme::Brief;
6+
7+
sub slurp { open my $fh, '<', $_[0] or die "Couldn't open $_[0] to read: $!\n"; readline $fh }
8+
9+
chdir $ARGV[0] or die "Cannot chdir to $ARGV[0]: $!\n";
10+
11+
my %file;
12+
13+
my $meta = CPAN::Meta->load_file( 'META.json' );
14+
15+
my $license = do {
16+
my @key = ( $meta->license, $meta->meta_spec_version );
17+
my ( $class, @ambiguous ) = Software::LicenseUtils->guess_license_from_meta_key( @key );
18+
die if @ambiguous;
19+
$class->new( $meta->custom( 'x_copyright' ) );
20+
};
21+
22+
$file{'LICENSE'} = $license->fulltext;
23+
24+
my @source = slurp 'lib/URI/Signature/Tiny.pod';
25+
splice @source, -2, 0, map "$_\n", '', '=head1 AUTHOR', '', $meta->authors;
26+
splice @source, -2, 0, split /(?<=\n)/, "\n=head1 COPYRIGHT AND LICENSE\n\n" . $license->notice;
27+
$file{'lib/URI/Signature/Tiny.pod'} = join '', @source;
28+
29+
die unless -e 'Makefile.PL';
30+
$file{'README'} = Pod::Readme::Brief->new( @source )->render( installer => 'eumm' );
31+
32+
my @manifest = slurp 'MANIFEST';
33+
my %manifest = map /\A([^\s#]+)()/, @manifest;
34+
$file{'MANIFEST'} = join '', sort @manifest, map "$_\n", grep !exists $manifest{ $_ }, keys %file;
35+
36+
for my $fn ( sort keys %file ) {
37+
unlink $fn if -e $fn;
38+
open my $fh, '>', $fn or die "Couldn't open $fn to write: $!\n";
39+
print $fh $file{ $fn };
40+
close $fh or die "Couldn't close $fn after writing: $!\n";
41+
}

lib/URI/Signature/Tiny.pm

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use strict; use warnings;
2+
3+
package URI::Signature::Tiny;
4+
5+
our $VERSION = '1.000';
6+
7+
use Digest::SHA ();
8+
use Carp ();
9+
use Scalar::Util ();
10+
11+
my @defaults = (
12+
sort_params => 1,
13+
recode_base64 => 1,
14+
after_sign => sub { Carp::croak( 'No after_sign callback specified' ) },
15+
before_verify => sub { Carp::croak( 'No before_verify callback specified' ) },
16+
function => \&Digest::SHA::hmac_sha256_base64,
17+
);
18+
19+
sub new {
20+
my $class = shift;
21+
my $self = bless { @defaults, @_ }, $class;
22+
Carp::croak( "Missing secret for $class" ) unless defined $self->{'secret'};
23+
$self;
24+
}
25+
26+
sub signature {
27+
my ( $self, $uri ) = ( shift, @_ );
28+
29+
Carp::croak( 'Cannot compute the signature of an undefined value' ) unless defined $uri;
30+
31+
$uri = $uri->isa( 'URI::WithBase' ) ? $uri->abs->as_string : $uri->as_string
32+
if Scalar::Util::blessed( $uri );
33+
34+
$uri =~ m[ \A [^?#]* \? ]xgc and $uri =~ s[ \G ([^#]+) ]{
35+
my @qp = split /[&;]/, "$1";
36+
join ';', $self->{'sort_params'} ? sort @qp : @qp;
37+
}xe;
38+
39+
my $sig = $self->{'function'}->( $uri, $self->{'secret'} );
40+
41+
$sig =~ s/=+\z//, $sig =~ y{+/}{-_} if $self->{'recode_base64'};
42+
43+
$sig;
44+
}
45+
46+
sub sign {
47+
my ( $self, $uri ) = ( shift, @_ );
48+
$self->{'after_sign'}->( $uri, $self->signature( $uri ) );
49+
}
50+
51+
sub verify {
52+
my $self = shift;
53+
my ( $uri, $sig ) = $self->{'before_verify'}->( @_ );
54+
$sig eq $self->signature( $uri );
55+
}
56+
57+
1;

lib/URI/Signature/Tiny.pod

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
=pod
2+
3+
=encoding UTF-8
4+
5+
=head1 NAME
6+
7+
URI::Signature::Tiny - Mint and verify server-signed URIs
8+
9+
=head1 SYNOPSIS
10+
11+
use URI;
12+
use URI::Signature::Tiny;
13+
14+
my $notary = URI::Signature::Tiny->new(
15+
secret => $secret,
16+
after_sign => sub {
17+
my ( $uri, $sig ) = @_;
18+
$uri->query_form({ $uri->query_form, s => $sig });
19+
$uri;
20+
},
21+
before_verify => sub {
22+
my ( $uri ) = @_;
23+
my %f = $uri->query_form;
24+
my $sig = delete $f{'s'};
25+
$uri = $uri->clone; # important
26+
$uri->query_form( \%f );
27+
( $uri, ref $sig ? '' : $sig );
28+
},
29+
);
30+
31+
my $signed_uri = $notary->sign( URI->new( 'http://example.com/foo?bar=baz#pagetop' ) );
32+
33+
my $ok = $notary->verify( $signed_uri );
34+
35+
=head1 DESCRIPTION
36+
37+
This is a minimal helper to generate URLs that you can later verify to not have
38+
been modified, so that you can trust security-relevant values such as user IDs.
39+
This is useful e.g. for a passwort reset link that the user should not be able
40+
to edit to log in as someone else.
41+
42+
=head1 METHODS
43+
44+
=over 2
45+
46+
=item C<new>
47+
48+
Construct and return an instance of this class.
49+
Takes a list of key/value pairs specifying configuration options:
50+
51+
=over 2
52+
53+
=item C<secret>
54+
55+
A message authentication code (MAC) value,
56+
which needs to have cryptographically sufficient entropy.
57+
58+
B<Required>.
59+
60+
=item C<after_sign>
61+
62+
A callback that defines how to incorporate the signature into a fresh URI.
63+
See L</C<sign>> for details.
64+
65+
Defaults to a placeholder that croaks.
66+
67+
=item C<before_verify>
68+
69+
A callback that defines how to remove the signature from a signed URI.
70+
See L</C<verify>> for details.
71+
72+
Defaults to a placeholder that croaks.
73+
74+
=item C<sort_params>
75+
76+
Whether to sort query parameters (if any) before computing the signature.
77+
78+
Defaults to true.
79+
80+
=item C<function>
81+
82+
The function that will be called to compute the signature,
83+
which should have the same signature as the HMAC functions from L<Digest::SHA>:
84+
the (normalised) URI and the secret will be its first and second arguments.
85+
86+
Defaults to
87+
L<C<\&Digest::SHA::hmac_sha256_base64>|Digest::SHA/hmac_sha256_base64>.
88+
89+
You might also use this just to post-process the HMAC value, any way you wish:
90+
91+
sub { substr &Digest::SHA::hmac_sha512224_base64, 0, 10 }
92+
93+
=item C<recode_base64>
94+
95+
Whether to apply substitutions to turn the return value of the L</C<function>>
96+
from regular C<base64> encoding into C<base64url>.
97+
98+
Defaults to true.
99+
100+
=back
101+
102+
=item C<signature>
103+
104+
Compute and return the signature for the URI
105+
which is passed as the only argument.
106+
107+
The only way that the URI value might be modified here is
108+
to sort the query parameters if requested by L</C<sort_params>>.
109+
110+
=item C<sign>
111+
112+
Takes a fresh URI and returns the same URI with the signature added to it.
113+
Specifically it returns whatever the L</C<after_sign>> callback returns,
114+
which gets called with the fresh URI and its signature as arguments.
115+
116+
=item C<verify>
117+
118+
Takes a signed URI and checks whether it matches its signature.
119+
It passes its arguments to the L</C<after_sign>> callback,
120+
which must return two values:
121+
the bare URI with the signature stripped off, and the signature.
122+
123+
=back
124+
125+
=head1 SEE ALSO
126+
127+
=over 2
128+
129+
=item *
130+
131+
L<URL::Signature>
132+
133+
=item *
134+
135+
L<RFCE<nbsp>2104, I<HMAC: Keyed-Hashing for Message Authentication>|https://tools.ietf.org/html/rfc2104>
136+
137+
=item *
138+
139+
L<RFCE<nbsp>4648, I<The Base16, Base32, and Base64 Data Encodings>, section 5., I<Base 64 Encoding with URL and Filename Safe Alphabet>|https://tools.ietf.org/html/rfc4648#section-5>
140+
141+
=back
142+
143+
=cut
144+
145+
vim: et sw=2 ts=2 sts=2

0 commit comments

Comments
 (0)