File Coverage

blib/lib/Authen/U2F.pm
Criterion Covered Total %
statement 52 108 48.1
branch 0 28 0.0
condition 1 9 11.1
subroutine 17 27 62.9
pod 4 6 66.6
total 74 178 41.5


line stmt bran cond sub pod time code
1             package Authen::U2F;
2             $Authen::U2F::VERSION = '0.003';
3             # ABSTRACT: FIDO U2F library
4              
5 2     2   73277 use warnings;
  2         185  
  2         89  
6 2     2   12 use strict;
  2         4  
  2         53  
7              
8 2     2   640 use namespace::autoclean;
  2         27729  
  2         8  
9              
10 2     2   905 use Types::Standard -types, qw(slurpy);
  2         123246  
  2         27  
11 2     2   9922 use Type::Params qw(compile);
  2         20400  
  2         22  
12 2     2   455 use Try::Tiny;
  2         5  
  2         159  
13 2     2   13 use Carp qw(croak);
  2         4  
  2         113  
14              
15 2     2   994 use Math::Random::Secure qw(irand);
  2         150403  
  2         240  
16 2     2   952 use MIME::Base64 3.11 qw(encode_base64url decode_base64url);
  2         2395  
  2         176  
17 2     2   779 use Crypt::OpenSSL::X509 1.806;
  2         8973  
  2         204  
18 2     2   831 use CryptX 0.034;
  2         7498  
  2         124  
19 2     2   974 use Crypt::PK::ECC;
  2         36760  
  2         204  
20 2     2   1010 use Digest::SHA qw(sha256);
  2         7376  
  2         236  
21 2     2   1091 use JSON qw(decode_json);
  2         17783  
  2         19  
22              
23 2     2   1121 use parent 'Exporter::Tiny';
  2         678  
  2         33  
24             our @EXPORT_OK = qw(u2f_challenge u2f_registration_verify u2f_signature_verify);
25              
26 1     1 1 88 sub u2f_challenge { __PACKAGE__->challenge(@_) }
27 0     0 1 0 sub u2f_registration_verify { __PACKAGE__->registration_verify(@_) }
28 0     0 1 0 sub u2f_signature_verify { __PACKAGE__->signature_verify(@_) }
29              
30             # Param checks
31             my $challenge_check;
32             my $registration_check;
33             my $signature_check;
34              
35             sub challenge {
36 1   33 1 1 10 $challenge_check ||= compile(
37             ClassName,
38             );
39 1         798 my ($class) = $challenge_check->(@_);
40              
41 1         26 my $raw = pack "L*", map { irand } 1..8;
  8         38515  
42 1         84 my $challenge = encode_base64url($raw);
43 1         20 return $challenge;
44             }
45              
46             sub registration_verify {
47 0   0 0 0   $registration_check ||= compile(
48             ClassName,
49             slurpy Dict[
50             challenge => Str,
51             app_id => Str,
52             origin => Str,
53             registration_data => Str,
54             client_data => Str,
55             ],
56             );
57 0           my ($class, $args) = $registration_check->(@_);
58              
59 0           my $client_data = decode_base64url($args->{client_data});
60 0 0         croak "couldn't decode client data; not valid Base64-URL?"
61             unless $client_data;
62              
63             {
64 0           my $data = decode_json($client_data);
  0            
65             croak "invalid client data (challenge doesn't match)"
66 0 0         unless $data->{challenge} eq $args->{challenge};
67             croak "invalid client data (origin doesn't match)"
68 0 0         unless $data->{origin} eq $args->{origin};
69             }
70              
71 0           my $reg_data = decode_base64url($args->{registration_data});
72 0 0         croak "couldn't decode registration data; not valid Base64-URL?"
73             unless $reg_data;
74              
75             # $reg_data is packed like so:
76             #
77             # 1-byte reserved (0x05)
78             # 65-byte public key
79             # 1-byte key handle length
80             # key handle
81             # attestation cert
82             # 2-byte DER type
83             # 2-byte DER length
84             # DER payload
85             # signature
86              
87 0           my ($reserved, $key, $handle, $certtype, $certlen, $certsig) = unpack 'a a65 C/a n n a*', $reg_data;
88              
89 0 0         croak "invalid registration data (reserved byte != 0x05)"
90             unless $reserved eq chr(0x05);
91              
92 0 0         croak "invalid registration data (key length != 65)"
93             unless length($key) == 65;
94              
95             # extract the cert payload from the trailing data and repack
96 0           my $certraw = substr $certsig, 0, $certlen;
97 0 0         croak "invalid registration data (incorrect cert length)"
98             unless length($certraw) == $certlen;
99 0           my $cert = pack "n n a*", $certtype, $certlen, $certraw;
100              
101             # signature at end of the trailing data
102 0           my $sig = substr $certsig, $certlen;
103              
104             my $x509 = try {
105 0     0     Crypt::OpenSSL::X509->new_from_string($cert, Crypt::OpenSSL::X509::FORMAT_ASN1);
106             }
107             catch {
108 0     0     croak "invalid registration data (certificate parse failure: $_)";
109 0           };
110              
111             my $pkec = try {
112 0     0     Crypt::PK::ECC->new(\$x509->pubkey);
113             }
114             catch {
115 0     0     croak "invalid registration data (certificate public key parse failure: $_)";
116 0           };
117              
118             # signature data. sha256 of:
119             #
120             # 1-byte reserved (0x00)
121             # 32-byte sha256(app ID) (application parameter)
122             # 32-byte sha256(client data (JSON-encoded)) (challenge parameter)
123             # key handle
124             # 65-byte key
125              
126 0           my $app_id_sha = sha256($args->{app_id});
127 0           my $challenge_sha = sha256($client_data);
128              
129 0           my $sigdata = pack "x a32 a32 a* a65", $app_id_sha, $challenge_sha, $handle, $key;
130 0           my $sigdata_sha = sha256($sigdata);
131              
132 0 0         $pkec->verify_hash($sig, $sigdata_sha)
133             or croak "invalid registration data (signature verification failed)";
134              
135 0           my $enc_key = encode_base64url($key);
136 0           my $enc_handle = encode_base64url($handle);
137              
138 0           return ($enc_handle, $enc_key);
139             }
140              
141             sub signature_verify {
142 0   0 0 0   $signature_check ||= compile(
143             ClassName,
144             slurpy Dict[
145             challenge => Str,
146             app_id => Str,
147             origin => Str,
148             key_handle => Str,
149             key => Str,
150             signature_data => Str,
151             client_data => Str,
152             ],
153             );
154 0           my ($class, $args) = $signature_check->(@_);
155              
156 0           my $key = decode_base64url($args->{key});
157 0 0         croak "couldn't decode key; not valid Base64-URL?"
158             unless $key;
159              
160 0           my $pkec = Crypt::PK::ECC->new;
161             try {
162 0     0     $pkec->import_key_raw($key, "nistp256");
163             }
164             catch {
165 0     0     croak "invalid key argument (parse failure: $_)";
166 0           };
167              
168 0           my $client_data = decode_base64url($args->{client_data});
169 0 0         croak "couldn't decode client data; not valid Base64-URL?"
170             unless $client_data;
171              
172             {
173 0           my $data = decode_json($client_data);
  0            
174             croak "invalid client data (challenge doesn't match)"
175 0 0         unless $data->{challenge} eq $args->{challenge};
176             croak "invalid client data (origin doesn't match)"
177 0 0         unless $data->{origin} eq $args->{origin};
178             }
179              
180 0           my $sign_data = decode_base64url($args->{signature_data});
181 0 0         croak "couldn't decode signature data; not valid Base64-URL?"
182             unless $sign_data;
183              
184             # $sig_data is packed like so
185             #
186             # 1-byte user presence
187             # 4-byte counter (big-endian)
188             # signature
189              
190 0           my ($presence, $counter, $sig) = unpack 'a N a*', $sign_data;
191              
192             # XXX presence check
193              
194             # XXX counter check
195              
196             # signature data. sha256 of:
197             #
198             # 32-byte sha256(app ID) (application parameter)
199             # 1-byte user presence
200             # 4-byte counter (big endian)
201             # 32-byte sha256(client data (JSON-encoded)) (challenge parameter)
202              
203 0           my $app_id_sha = sha256($args->{app_id});
204 0           my $challenge_sha = sha256($client_data);
205              
206 0           my $sigdata = pack "a32 a N a32", $app_id_sha, $presence, $counter, $challenge_sha;
207 0           my $sigdata_sha = sha256($sigdata);
208              
209 0 0         $pkec->verify_hash($sig, $sigdata_sha)
210             or croak "invalid signature data (signature verification failed)";
211              
212 0           return;
213             }
214              
215             1;
216             __END__