File Coverage

blib/lib/Authen/U2F.pm
Criterion Covered Total %
statement 47 110 42.7
branch 0 28 0.0
condition n/a
subroutine 16 28 57.1
pod 4 6 66.6
total 67 172 38.9


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