File Coverage

blib/lib/Net/ACME/Crypt.pm
Criterion Covered Total %
statement 56 64 87.5
branch 5 12 41.6
condition n/a
subroutine 15 17 88.2
pod 0 2 0.0
total 76 95 80.0


line stmt bran cond sub pod time code
1             package Net::ACME::Crypt;
2              
3             #----------------------------------------------------------------------
4             # This module exists because of a desire to do these computations
5             # in environments where a compiler may not be available.
6             # (Otherwise, CryptX would be ideal.)
7             #----------------------------------------------------------------------
8              
9 14     14   146822 use strict;
  14         16  
  14         287  
10 14     14   43 use warnings;
  14         19  
  14         230  
11              
12 14     14   3276 use JSON ();
  14         40444  
  14         178  
13 14     14   2556 use MIME::Base64 ();
  14         2947  
  14         222  
14              
15 14     14   4330 use Crypt::Perl::PK ();
  14         22837  
  14         189  
16              
17 14     14   4176 use Net::ACME::X ();
  14         20  
  14         230  
18              
19             #As per the ACME spec
20 14     14   44 use constant JWK_THUMBPRINT_DIGEST => 'sha256';
  14         16  
  14         618  
21              
22 14     14   43 use constant JWT_RSA_SIG => 'RS256';
  14         28  
  14         6939  
23              
24             *parse_key = \&Crypt::Perl::PK::parse_key;
25              
26             sub get_jwk_thumbprint {
27 5     5 0 3522342 my ($jwk_hr) = @_;
28              
29             #We could generate the thumbprint directly from the JWK,
30             #but there’d be more code to maintain. For now the speed hit
31             #seems acceptable … ?
32              
33 5         25 my $key_obj = Crypt::Perl::PK::parse_jwk($jwk_hr);
34              
35 5         59690 return $key_obj->get_jwk_thumbprint(JWK_THUMBPRINT_DIGEST());
36             }
37              
38             *_encode_b64u = \&MIME::Base64::encode_base64url;
39              
40             #expects:
41             # key - object
42             # payload
43             # extra_headers (optional, hashref)
44             sub create_jwt {
45 9     9 0 75245721 my (%args) = @_;
46              
47 9 50       89 if ($args{'key'}->isa('Crypt::Perl::RSA::PrivateKey')) {
    0          
48 9         70 return _create_rs256_jwt(%args);
49             }
50             elsif ($args{'key'}->isa('Crypt::Perl::ECDSA::PrivateKey')) {
51 0         0 return _create_ecc_jwt(%args);
52             }
53              
54 0         0 die "Unrecognized “key”: “$args{'key'}”";
55             }
56              
57             #----------------------------------------------------------------------
58              
59             #Based on Crypt::JWT::encode_jwt(), but focused on this particular
60             #protocol’s needs. Note that UTF-8 might get mangled in here,
61             #but that’s not a problem since ACME shouldn’t require sending raw UTF-8.
62             #(Maybe with registration??)
63             sub _create_rs256_jwt {
64 9     9   26 my ( %args ) = @_;
65              
66 9         20 my $alg = JWT_RSA_SIG();
67              
68 9         17 my $key = $args{'key'};
69              
70             my $signer_cr = sub {
71 9     9   97 return $key->can("sign_$alg")->($key, @_);
72 9         51 };
73              
74 9         39 return _create_jwt(
75             %args,
76             alg => $alg,
77             signer_cr => $signer_cr,
78             );
79             }
80              
81             sub _create_ecc_jwt {
82 0     0   0 my (%args) = @_;
83              
84 0         0 my $key = $args{'key'};
85              
86             my $signer_cr = sub {
87 0     0   0 return $key->sign_jwa(@_);
88 0         0 };
89              
90 0         0 return _create_jwt(
91             %args,
92             alg => $key->get_jwa_alg(),
93             signer_cr => $signer_cr,
94             );
95             }
96              
97             sub _create_jwt {
98 9     9   32 my ( %args ) = @_;
99              
100             # key
101 9 50       33 die "JWS: missing 'key'" if !$args{key};
102              
103 9         16 my $payload = $args{payload};
104 9         13 my $alg = $args{'alg'};
105              
106 9 50       29 my $header = $args{extra_headers} ? { %{$args{extra_headers}} } : {};
  9         30  
107              
108             # serialize payload
109 9         33 $payload = _payload_enc($payload);
110              
111             # encode payload
112 9         27 my $b64u_payload = _encode_b64u($payload);
113              
114             # prepare header
115 9         90 $header->{alg} = $alg;
116              
117             # encode header
118 9         24 my $json_header = _encode_json($header);
119 9         49 my $b64u_header = _encode_b64u($json_header);
120              
121 9         98 my $signer_cr = $args{'signer_cr'};
122              
123 9         51 my $b64u_signature = _encode_b64u( $signer_cr->("$b64u_header.$b64u_payload", $args{key}) );
124              
125 8         40354450 return join('.', $b64u_header, $b64u_payload, $b64u_signature);
126             }
127              
128             sub _encode_json {
129 9     9   39 my ($payload) = @_;
130              
131             #Always do a canonical encode so that we can test more easily.
132             #Note that JWS itself does NOT require this.
133 9         173 return JSON->new()->canonical(1)->encode($payload);
134             }
135              
136             #Taken from Crypt::JWT
137             sub _payload_enc {
138 9     9   17 my ($payload) = @_;
139              
140 9 50       27 if (ref($payload) =~ /^(?:HASH|ARRAY)$/) {
141 0         0 $payload = _encode_json($payload);
142             }
143             else {
144 9 50       56 utf8::downgrade($payload, 1) or die "JWT: payload cannot contain wide character";
145             }
146              
147 9         16 return $payload;
148             }
149              
150             1;