Authen-WebAuthn/lib/Authen/WebAuthn/Test.pm
package Authen::WebAuthn::Test;
$Authen::WebAuthn::Test::VERSION = '0.005';
use Mouse;
use CBOR::XS;
use MIME::Base64 qw(encode_base64url decode_base64url);
use Digest::SHA qw(sha256);
use Crypt::PK::ECC;
use Hash::Merge::Simple qw/merge/;
use JSON qw(encode_json decode_json);
use Authen::WebAuthn;
use utf8;
has origin => ( is => 'rw' );
has rp_id => ( is => 'rw', lazy => 1, default => sub { $_[0]->origin } );
has credential_id => ( is => 'rw' );
has aaguid => ( is => 'rw' );
has key => ( is => 'rw' );
has sign_count => ( is => 'rw', default => 0 );
use constant {
FLAG_UP => 1,
FLAG_UV => 4,
FLAG_AT => 64,
FLAG_ED => 128,
};
sub makeAttestedCredentialData {
my ($acd) = @_;
return '' unless ($acd);
my $aaguid = exportAaguid( $acd->{aaguid} );
my $credentialIdLength = pack( 'n', length( $acd->{credentialId} ) );
my $credentialId = $acd->{credentialId};
my $credentialPublicKey = $acd->{credentialPublicKey};
my $attestedCredentialData =
( $aaguid . $credentialIdLength . $credentialId . $credentialPublicKey );
return $attestedCredentialData;
}
sub makeAuthData {
my ($authdata) = @_;
my $rpIdHash = $authdata->{rpIdHash};
my $flags = pack( 'C', makeFlags( $authdata->{flags} ) );
my $signCount = pack( 'N', $authdata->{signCount} );
my $acd = makeAttestedCredentialData( $authdata->{attestedCredentialData} );
my $ed =
$authdata->{extensions} ? encode_cbor( $authdata->{extensions} ) : '';
my $res = $rpIdHash . $flags . $signCount . $acd . $ed;
return $res;
}
sub makeAttestationObject {
my ($dat) = @_;
my $attestationObject = {
'fmt' => $dat->{fmt},
'authData' => makeAuthData( $dat->{authData} ),
'attStmt' => {} # TODO
};
my $cbor = CBOR::XS->new;
$cbor->text_keys(1);
return $cbor->encode($attestationObject);
}
sub makeFlags {
my ($flags) = @_;
my $up = $flags->{userPresent} ? 1 : 0;
my $uv = $flags->{userVerified} ? 1 : 0;
my $at = $flags->{atIncluded} ? 1 : 0;
my $ed = $flags->{edIncluded} ? 1 : 0;
return ( FLAG_UP * $up + FLAG_UV * $uv + FLAG_AT * $at + FLAG_ED * $ed );
}
# Transform string GUID into binary
sub exportAaguid {
my ($aaguid) = @_;
$aaguid =~ s/-//g;
if ( length($aaguid) == 32 ) {
return pack( 'H*', $aaguid );
}
else {
die "Invalid AAGUID length";
}
}
sub encode_credential {
my ( $self, $credential ) = @_;
# Encode clientDataJSON
if ( $credential->{response}->{clientDataJSON} ) {
$credential->{response}->{clientDataJSON} =
encode_base64url( $credential->{response}->{clientDataJSON} );
}
# Encode attestationObject
if ( $credential->{response}->{attestationObject} ) {
$credential->{response}->{attestationObject} =
encode_base64url( $credential->{response}->{attestationObject} );
}
# Encode authenticatorData
if ( $credential->{response}->{authenticatorData} ) {
$credential->{response}->{authenticatorData} =
encode_base64url( $credential->{response}->{authenticatorData} );
}
# Encode signature
if ( $credential->{response}->{signature} ) {
$credential->{response}->{signature} =
encode_base64url( $credential->{response}->{signature} );
}
# Encode rawId
if ( $credential->{rawId} ) {
$credential->{rawId} = encode_base64url( $credential->{rawId} );
}
return JSON->new->utf8->pretty->encode($credential);
}
sub encode_cosekey {
my ($self) = @_;
my $key_str = $self->key;
my $key = Crypt::PK::ECC->new( \$key_str );
return Authen::WebAuthn::_ecc_obj_to_cose($key);
}
sub sign {
my ( $self, $message ) = @_;
my $key_str = $self->key;
my $key = Crypt::PK::ECC->new( \$key_str );
return $key->sign_message( $message, 'SHA256' );
}
sub get_credential_response {
my ( $self, $input, $override ) = @_;
my $challenge = $input->{request}->{challenge};
my $uv = $input->{request}->{authenticatorSelection}->{userVerification}
|| "preferred";
# Everything is build from this array, you can override it for testing
# various scenarios
my $credential = merge {
response => {
type => "public-key",
rawId => $self->credential_id,
id => encode_base64url( $self->credential_id ),
clientDataJSON => {
type => "webauthn.create",
challenge => "$challenge",
origin => $self->origin,
crossOrigin => JSON::false,
},
attestationObject => {
'fmt' => 'none',
'authData' => {
rpIdHash => sha256( $self->rp_id ),
flags => {
userPresent => 1,
( $uv eq "required" ? ( userVerified => 1 ) : () ),
atIncluded => 1,
},
signCount => $self->sign_count,
attestedCredentialData => {
aaguid => $self->aaguid,
credentialId => $self->credential_id,
credentialPublicKey => $self->encode_cosekey,
},
},
'attStmt' => {}
}
}
}, $override;
$credential->{response}->{clientDataJSON} =
encode_json $credential->{response}->{clientDataJSON};
$credential->{response}->{attestationObject} =
makeAttestationObject $credential->{response}->{attestationObject};
return $credential;
}
sub get_assertion_response {
my ( $self, $input, $override ) = @_;
my $challenge = $input->{request}->{challenge};
my $uv = $input->{request}->{userVerification} || "preferred";
# Everything is build from this array, you can override it for testing
# various scenarios
my $credential = merge {
type => "public-key",
rawId => $self->credential_id,
id => encode_base64url( $self->credential_id ),
response => {
clientDataJSON => {
type => "webauthn.get",
challenge => "$challenge",
origin => $self->origin,
crossOrigin => JSON::false,
},
authenticatorData => {
rpIdHash => sha256( $self->rp_id ),
flags => {
userPresent => 1,
( $uv eq "required" ? ( userVerified => 1 ) : () ),
},
signCount => $self->sign_count,
},
userHandle => "", #TODO
}
}, $override;
my $clientData = {
type => "webauthn.get",
challenge => "$challenge",
origin => $self->origin,
crossOrigin => JSON::false,
};
$credential->{response}->{clientDataJSON} =
encode_json $credential->{response}->{clientDataJSON};
$credential->{response}->{authenticatorData} =
makeAuthData $credential->{response}->{authenticatorData};
my $hash = sha256( $credential->{response}->{clientDataJSON} );
my $authData = $credential->{response}->{authenticatorData};
my $signature = $self->sign( $authData . $hash );
# Add signature to hash unless we have overriden it
unless ( $credential->{response}->{signature} ) {
$credential->{response}->{signature} = $signature;
}
return $credential;
}
1;