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__ |