File Coverage

blib/lib/Authen/WebAuthn/Test.pm
Criterion Covered Total %
statement 89 102 87.2
branch 15 30 50.0
condition 4 4 100.0
subroutine 19 20 95.0
pod 0 10 0.0
total 127 166 76.5


line stmt bran cond sub pod time code
1             package Authen::WebAuthn::Test;
2             $Authen::WebAuthn::Test::VERSION = '0.001';
3 2     2   1563 use Mouse;
  2         6  
  2         22  
4 2     2   1114 use CBOR::XS;
  2         8  
  2         223  
5 2     2   16 use MIME::Base64 qw(encode_base64url decode_base64url);
  2         6  
  2         120  
6 2     2   19 use Digest::SHA qw(sha256);
  2         4  
  2         91  
7 2     2   12 use Crypt::PK::ECC;
  2         4  
  2         161  
8 2     2   732 use Hash::Merge::Simple qw/merge/;
  2         640  
  2         137  
9 2     2   12 use JSON qw(encode_json decode_json);
  2         2  
  2         19  
10 2     2   277 use Authen::WebAuthn;
  2         9  
  2         146  
11 2     2   15 use utf8;
  2         3  
  2         23  
12              
13             has origin => ( is => 'rw' );
14             has rp_id => ( is => 'rw', lazy => 1, default => sub { $_[0]->origin } );
15             has credential_id => ( is => 'rw' );
16             has aaguid => ( is => 'rw' );
17             has key => ( is => 'rw' );
18             has sign_count => ( is => 'rw', default => 0 );
19              
20             use constant {
21 2         3200 FLAG_UP => 1,
22             FLAG_UV => 4,
23             FLAG_AT => 64,
24             FLAG_ED => 128,
25 2     2   292 };
  2         4  
26              
27             sub makeAttestedCredentialData {
28 21     21 0 53 my ($acd) = @_;
29 21 100       78 return '' unless ($acd);
30              
31 6         11 my $aaguid = exportAaguid( $acd->{aaguid} );
32 6         14 my $credentialIdLength = pack( 'n', length( $acd->{credentialId} ) );
33 6         8 my $credentialId = $acd->{credentialId};
34 6         9 my $credentialPublicKey = $acd->{credentialPublicKey};
35 6         12 my $attestedCredentialData =
36             ( $aaguid . $credentialIdLength . $credentialId . $credentialPublicKey );
37              
38 6         9 return $attestedCredentialData;
39             }
40              
41             sub makeAuthData {
42 21     21 0 54 my ($authdata) = @_;
43              
44 21         52 my $rpIdHash = $authdata->{rpIdHash};
45 21         94 my $flags = pack( 'C', makeFlags( $authdata->{flags} ) );
46 21         67 my $signCount = pack( 'N', $authdata->{signCount} );
47 21         81 my $acd = makeAttestedCredentialData( $authdata->{attestedCredentialData} );
48             my $ed =
49 21 50       68 $authdata->{extensions} ? encode_cbor( $authdata->{extensions} ) : '';
50 21         56 my $res = $rpIdHash . $flags . $signCount . $acd . $ed;
51              
52 21         70 return $res;
53             }
54              
55             sub makeAttestationObject {
56 6     6 0 9 my ($dat) = @_;
57              
58             my $attestationObject = {
59             'fmt' => $dat->{fmt},
60 6         15 'authData' => makeAuthData( $dat->{authData} ),
61             'attStmt' => {} # TODO
62             };
63              
64 6         26 my $cbor = CBOR::XS->new;
65 6         17 $cbor->text_keys(1);
66 6         44 return $cbor->encode($attestationObject);
67             }
68              
69             sub makeFlags {
70 21     21 0 46 my ($flags) = @_;
71 21 50       83 my $up = $flags->{userPresent} ? 1 : 0;
72 21 100       53 my $uv = $flags->{userVerified} ? 1 : 0;
73 21 100       54 my $at = $flags->{atIncluded} ? 1 : 0;
74 21 50       58 my $ed = $flags->{edIncluded} ? 1 : 0;
75 21         146 return ( FLAG_UP * $up + FLAG_UV * $uv + FLAG_AT * $at + FLAG_ED * $ed );
76             }
77              
78             # Transform string GUID into binary
79             sub exportAaguid {
80 6     6 0 8 my ($aaguid) = @_;
81 6         28 $aaguid =~ s/-//g;
82              
83 6 50       14 if ( length($aaguid) == 32 ) {
84 6         19 return pack( 'H*', $aaguid );
85             }
86             else {
87 0         0 die "Invalid AAGUID length";
88             }
89             }
90              
91             sub encode_credential {
92 0     0 0 0 my ( $self, $credential ) = @_;
93              
94             # Encode clientDataJSON
95 0 0       0 if ( $credential->{response}->{clientDataJSON} ) {
96             $credential->{response}->{clientDataJSON} =
97 0         0 encode_base64url( $credential->{response}->{clientDataJSON} );
98             }
99              
100             # Encode attestationObject
101 0 0       0 if ( $credential->{response}->{attestationObject} ) {
102             $credential->{response}->{attestationObject} =
103 0         0 encode_base64url( $credential->{response}->{attestationObject} );
104             }
105              
106             # Encode authenticatorData
107 0 0       0 if ( $credential->{response}->{authenticatorData} ) {
108             $credential->{response}->{authenticatorData} =
109 0         0 encode_base64url( $credential->{response}->{authenticatorData} );
110             }
111              
112             # Encode signature
113 0 0       0 if ( $credential->{response}->{signature} ) {
114             $credential->{response}->{signature} =
115 0         0 encode_base64url( $credential->{response}->{signature} );
116             }
117              
118             # Encode rawId
119 0 0       0 if ( $credential->{rawId} ) {
120 0         0 $credential->{rawId} = encode_base64url( $credential->{rawId} );
121             }
122              
123 0         0 return JSON->new->utf8->pretty->encode($credential);
124             }
125              
126             sub encode_cosekey {
127              
128 21     21 0 370 my ($self) = @_;
129              
130 21         141 my $key_str = $self->key;
131 21         168 my $key = Crypt::PK::ECC->new( \$key_str );
132              
133 21         104990 return Authen::WebAuthn::_ecc_obj_to_cose($key);
134             }
135              
136             sub sign {
137 15     15 0 40 my ( $self, $message ) = @_;
138 15         63 my $key_str = $self->key;
139 15         134 my $key = Crypt::PK::ECC->new( \$key_str );
140              
141 15         112091 return $key->sign_message( $message, 'SHA256' );
142             }
143              
144             sub get_credential_response {
145 6     6 0 13563 my ( $self, $input, $override ) = @_;
146              
147 6         13 my $challenge = $input->{request}->{challenge};
148             my $uv = $input->{request}->{authenticatorSelection}->{userVerification}
149 6   100     24 || "preferred";
150              
151             # Everything is build from this array, you can override it for testing
152             # various scenarios
153 6 100       23 my $credential = merge {
154             response => {
155             type => "public-key",
156             rawId => $self->credential_id,
157             id => encode_base64url( $self->credential_id ),
158             clientDataJSON => {
159             type => "webauthn.create",
160             challenge => "$challenge",
161             origin => $self->origin,
162             crossOrigin => JSON::false,
163             },
164             attestationObject => {
165             'fmt' => 'none',
166             'authData' => {
167             rpIdHash => sha256( $self->rp_id ),
168             flags => {
169             userPresent => 1,
170             ( $uv eq "required" ? ( userVerified => 1 ) : () ),
171             atIncluded => 1,
172             },
173             signCount => $self->sign_count,
174             attestedCredentialData => {
175             aaguid => $self->aaguid,
176             credentialId => $self->credential_id,
177             credentialPublicKey => $self->encode_cosekey,
178             },
179             },
180             'attStmt' => {}
181             }
182             }
183             }, $override;
184              
185             $credential->{response}->{clientDataJSON} =
186 6         304 encode_json $credential->{response}->{clientDataJSON};
187             $credential->{response}->{attestationObject} =
188 6         18 makeAttestationObject $credential->{response}->{attestationObject};
189              
190 6         24 return $credential;
191             }
192              
193             sub get_assertion_response {
194 15     15 0 15732 my ( $self, $input, $override ) = @_;
195              
196 15         75 my $challenge = $input->{request}->{challenge};
197 15   100     102 my $uv = $input->{request}->{userVerification} || "preferred";
198              
199             # Everything is build from this array, you can override it for testing
200             # various scenarios
201 15 100       103 my $credential = merge {
202             type => "public-key",
203             rawId => $self->credential_id,
204             id => encode_base64url( $self->credential_id ),
205             response => {
206             clientDataJSON => {
207             type => "webauthn.get",
208             challenge => "$challenge",
209             origin => $self->origin,
210             crossOrigin => JSON::false,
211             },
212             authenticatorData => {
213             rpIdHash => sha256( $self->rp_id ),
214             flags => {
215             userPresent => 1,
216             ( $uv eq "required" ? ( userVerified => 1 ) : () ),
217             },
218             signCount => $self->sign_count,
219             },
220             userHandle => "", #TODO
221             }
222             }, $override;
223              
224 15         1247 my $clientData = {
225             type => "webauthn.get",
226             challenge => "$challenge",
227             origin => $self->origin,
228             crossOrigin => JSON::false,
229             };
230             $credential->{response}->{clientDataJSON} =
231 15         209 encode_json $credential->{response}->{clientDataJSON};
232             $credential->{response}->{authenticatorData} =
233 15         87 makeAuthData $credential->{response}->{authenticatorData};
234              
235 15         112 my $hash = sha256( $credential->{response}->{clientDataJSON} );
236 15         36 my $authData = $credential->{response}->{authenticatorData};
237 15         81 my $signature = $self->sign( $authData . $hash );
238              
239             # Add signature to hash unless we have overriden it
240 15 50       234 unless ( $credential->{response}->{signature} ) {
241 15         72 $credential->{response}->{signature} = $signature;
242             }
243              
244 15         193 return $credential;
245             }
246              
247             1;
248              
249             __END__