auth: publish passkey foundations preview
verify / verify (push) Successful in 3m40s

This commit is contained in:
2026-08-21 17:33:00 -04:00
parent fb6bbd0dad
commit bfe6cfd29e
230 changed files with 44547 additions and 17 deletions
+205
View File
@@ -0,0 +1,205 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// The CredentialAssertionResponse is the raw response returned to the Relying Party from an authenticator when we request a
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
// The ParsedCredentialAssertionData is the parsed [CredentialAssertionResponse] that has been marshalled into a format
// that allows us to verify the client and authenticator data inside the response.
type ParsedCredentialAssertionData struct {
ParsedPublicKeyCredential
Response ParsedAssertionResponse
Raw CredentialAssertionResponse
}
// The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
// [ParsedAssertionResponse].
type AuthenticatorAssertionResponse struct {
AuthenticatorResponse
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
Signature URLEncodedBase64 `json:"signature"`
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
}
// ParsedAssertionResponse is the parsed form of [AuthenticatorAssertionResponse].
type ParsedAssertionResponse struct {
CollectedClientData CollectedClientData
AuthenticatorData AuthenticatorData
Signature []byte
UserHandle []byte
}
// ParseCredentialRequestResponse parses a login/assertion response from a [*http.Request]. The request body is
// automatically drained and closed after parsing.
//
// This is the standard entry point when using [net/http]. For implementations that don't use [net/http], see
// [ParseCredentialRequestResponseBody] (accepts an [io.Reader]) or [ParseCredentialRequestResponseBytes] (accepts a
// []byte).
func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) {
if response == nil || response.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
defer func(request *http.Request) {
_, _ = io.Copy(io.Discard, request.Body)
_ = request.Body.Close()
}(response)
return ParseCredentialRequestResponseBody(response.Body)
}
// ParseCredentialRequestResponseBody parses a login/assertion response from an [io.Reader]. The caller is responsible
// for closing the reader if applicable.
//
// This is the framework-agnostic variant of [ParseCredentialRequestResponse]. For a [*http.Request] use
// [ParseCredentialRequestResponse] instead. For raw bytes use [ParseCredentialRequestResponseBytes].
func ParseCredentialRequestResponseBody(body io.Reader) (par *ParsedCredentialAssertionData, err error) {
var car CredentialAssertionResponse
if err = decodeBody(body, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error()).WithError(err)
}
return car.Parse()
}
// ParseCredentialRequestResponseBytes parses a login/assertion response from raw bytes.
//
// See also [ParseCredentialRequestResponse] (for [*http.Request]) and [ParseCredentialRequestResponseBody] (for
// [io.Reader]).
func ParseCredentialRequestResponseBytes(data []byte) (par *ParsedCredentialAssertionData, err error) {
var car CredentialAssertionResponse
if err = decodeBytes(data, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error()).WithError(err)
}
return car.Parse()
}
// Parse validates and parses the [CredentialAssertionResponse] into a [ParsedCredentialAssertionData]. Most
// implementations should use [ParseCredentialRequestResponse], [ParseCredentialRequestResponseBody], or
// [ParseCredentialRequestResponseBytes] instead of calling this method directly.
func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionData, err error) {
if car.ID == "" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
}
if _, err = base64.RawURLEncoding.DecodeString(car.ID); err != nil {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded").WithError(err)
}
if car.Type != string(PublicKeyCredentialType) {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
}
var attachment AuthenticatorAttachment
switch att := AuthenticatorAttachment(car.AuthenticatorAttachment); att {
case Platform, CrossPlatform:
attachment = att
}
par = &ParsedCredentialAssertionData{
ParsedPublicKeyCredential{
ParsedCredential{car.ID, car.Type}, car.RawID, car.ClientExtensionResults, attachment,
},
ParsedAssertionResponse{
Signature: car.AssertionResponse.Signature,
UserHandle: car.AssertionResponse.UserHandle,
},
car,
}
// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// We don't call it cData but this is Step 5 in the spec.
if err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData); err != nil {
return nil, err
}
if err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData); err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data").WithError(err)
}
return par, nil
}
// Verify the remaining elements of the assertion data by following the steps outlined in the referenced specification
// documentation. It's important to note that the credentialBytes field is the CBOR representation of the credential.
//
// Specification: §7.2 Verifying an Authentication Assertion (https://www.w3.org/TR/webauthn/#sctn-verifying-assertion)
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, appID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, allowCrossOrigin, verifyUser, verifyUserPresence bool, credentialBytes []byte) error {
// Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
// "assertive" steps, i.e. "Let JSONtext be the result of running UTF-8 decode on the value of cData."
// We handle these steps in part as we verify but also beforehand
//
// Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
// returned by the authenticator.
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify, allowCrossOrigin)
if validError != nil {
return validError
}
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
var appIDHash [32]byte
if appID != "" {
appIDHash = sha256.Sum256([]byte(appID))
}
// Handle steps 11 through 14, verifying the authenticator data.
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser, verifyUserPresence)
if validError != nil {
return validError
}
// Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON)
// Step 16. Using the credential public key looked up in step 3, verify that sig is
// a valid signature over the binary concatenation of authData and hash.
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...) //nolint:gocritic // This is intentional.
var (
key any
err error
)
// If the Session Data does not contain the appID extension or it wasn't reported as used by the Client/RP then we
// use the standard CTAP2 public key parser.
if appID == "" {
key, err = webauthncose.ParsePublicKey(credentialBytes)
} else {
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
}
if err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error parsing the assertion public key: %+v", err)).WithError(err)
}
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid || err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v", err)).WithError(err)
}
return nil
}
+538
View File
@@ -0,0 +1,538 @@
package protocol
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
)
func TestParseCredentialRequestResponse(t *testing.T) {
byteID, _ := base64.RawURLEncoding.DecodeString("AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng")
byteAAGUID, _ := base64.RawURLEncoding.DecodeString("rc4AAjW8xgpkiwsl8fBVAw")
byteRPIDHash, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")
byteAuthData, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ")
byteSignature, _ := base64.RawURLEncoding.DecodeString("MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc")
byteUserHandle, _ := base64.RawURLEncoding.DecodeString("0ToAAAAAAAAAAA")
byteCredentialPubKey, _ := base64.RawURLEncoding.DecodeString("pQMmIAEhWCAoCF-x0dwEhzQo-ABxHIAgr_5WL6cJceREc81oIwFn7iJYIHEHx8ZhBIE42L26-rSC_3l0ZaWEmsHAKyP9rgslApUdAQI")
byteClientDataJSON, _ := base64.RawURLEncoding.DecodeString("eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9")
type args struct {
responseName string
}
testCases := []struct {
name string
args args
expected *ParsedCredentialAssertionData
err string
errType string
errDetails string
errInfo string
}{
{
name: "ShouldParseCredentialAssertion",
args: args{
"success",
},
expected: &ParsedCredentialAssertionData{
ParsedPublicKeyCredential: ParsedPublicKeyCredential{
ParsedCredential: ParsedCredential{
ID: "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
Type: string(PublicKeyCredentialType),
},
RawID: byteID,
ClientExtensionResults: map[string]any{
"appID": "example.com",
},
},
Response: ParsedAssertionResponse{
CollectedClientData: CollectedClientData{
Type: CeremonyType("webauthn.get"),
Challenge: "E4PTcIH_HfX1pC6Sigk1SC9NAlgeztN0439vi8z_c9k",
Origin: "https://webauthn.io",
Hint: "do not compare clientDataJSON against a template. See https://goo.gl/yabPex",
},
AuthenticatorData: AuthenticatorData{
RPIDHash: byteRPIDHash,
Counter: 1553097241,
Flags: 0x045,
AttData: AttestedCredentialData{
AAGUID: byteAAGUID,
CredentialID: byteID,
CredentialPublicKey: byteCredentialPubKey,
},
},
Signature: byteSignature,
UserHandle: byteUserHandle,
},
Raw: CredentialAssertionResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
Type: string(PublicKeyCredentialType),
ID: "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
},
RawID: byteID,
ClientExtensionResults: map[string]any{
"appID": "example.com",
},
},
AssertionResponse: AuthenticatorAssertionResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: byteClientDataJSON,
},
AuthenticatorData: byteAuthData,
Signature: byteSignature,
UserHandle: byteUserHandle,
},
},
},
err: "",
},
{
name: "ShouldHandleTrailingData",
args: args{
"trailingData",
},
expected: nil,
err: "Parse error for Assertion",
errType: "invalid_request",
errDetails: "Parse error for Assertion",
errInfo: "body contains trailing data",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
body := io.NopCloser(bytes.NewReader([]byte(testAssertionResponses[tc.args.responseName])))
actual, err := ParseCredentialRequestResponseBody(body)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
return
}
require.NoError(t, err)
assert.Equal(t, tc.expected.ClientExtensionResults, actual.ClientExtensionResults)
assert.Equal(t, tc.expected.ID, actual.ID)
assert.Equal(t, tc.expected.ParsedCredential, actual.ParsedCredential)
assert.Equal(t, tc.expected.ParsedPublicKeyCredential, actual.ParsedPublicKeyCredential)
assert.Equal(t, tc.expected.Raw, actual.Raw)
assert.Equal(t, tc.expected.RawID, actual.RawID)
assert.Equal(t, tc.expected.Response.CollectedClientData, actual.Response.CollectedClientData)
var (
pkExpected, pkActual any
)
assert.NoError(t, webauthncbor.Unmarshal(tc.expected.Response.AuthenticatorData.AttData.CredentialPublicKey, &pkExpected))
assert.NoError(t, webauthncbor.Unmarshal(actual.Response.AuthenticatorData.AttData.CredentialPublicKey, &pkActual))
assert.Equal(t, pkExpected, pkActual)
assert.NotEqual(t, nil, pkExpected)
assert.NotEqual(t, nil, pkActual)
})
}
}
func TestParseCredentialRequestResponse_NilRequest(t *testing.T) {
testCases := []struct {
name string
request *http.Request
err string
}{
{
name: "ShouldFailNilRequest",
request: nil,
err: "No response given",
},
{
name: "ShouldFailNilBody",
request: &http.Request{},
err: "No response given",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParseCredentialRequestResponse(tc.request)
assert.Nil(t, result)
assert.EqualError(t, err, tc.err)
})
}
}
func TestParseCredentialRequestResponseBytes(t *testing.T) {
testCases := []struct {
name string
data []byte
err string
errType string
errDetails string
errInfo string
}{
{
name: "ShouldFailInvalidJSON",
data: []byte("not json"),
err: "Parse error for Assertion",
errType: "invalid_request",
errDetails: "Parse error for Assertion",
errInfo: "invalid character 'o' in literal null (expecting 'u')",
},
{
name: "ShouldFailTrailingData",
data: []byte(testAssertionResponses["trailingData"]),
err: "Parse error for Assertion",
errType: "invalid_request",
errDetails: "Parse error for Assertion",
errInfo: "body contains trailing data",
},
{
name: "ShouldSucceed",
data: []byte(testAssertionResponses["success"]),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParseCredentialRequestResponseBytes(tc.data)
if tc.err != "" {
assert.Nil(t, result)
assert.EqualError(t, err, tc.err)
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
} else {
require.NoError(t, err)
require.NotNil(t, result)
}
})
}
}
func TestCredentialAssertionResponse_Parse_Errors(t *testing.T) {
testCases := []struct {
name string
car CredentialAssertionResponse
err string
}{
{
name: "ShouldFailMissingID",
car: CredentialAssertionResponse{},
err: "CredentialAssertionResponse with ID missing",
},
{
name: "ShouldFailIDNotBase64",
car: CredentialAssertionResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "not valid base64 %%%",
},
},
},
err: "CredentialAssertionResponse with ID not base64url encoded",
},
{
name: "ShouldFailBadType",
car: CredentialAssertionResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "dGVzdA",
Type: "bad-type",
},
},
},
err: "CredentialAssertionResponse with bad type",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := tc.car.Parse()
assert.Nil(t, result)
assert.EqualError(t, err, tc.err)
})
}
}
func TestParsedCredentialAssertionData_Verify(t *testing.T) {
par, credPubKey, challenge := testAssertionSpecVectorNoneES256(t)
// Valid but wrong public key (from the packed self ES256 spec test vector).
wrongKey, err := hex.DecodeString("a5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2")
require.NoError(t, err)
testCases := []struct {
name string
challenge string
relyingPartyID string
rpOrigins []string
appID string
credentialBytes []byte
err string
}{
{
name: "ShouldSucceed",
challenge: challenge,
relyingPartyID: "example.org",
rpOrigins: []string{"https://example.org"},
credentialBytes: credPubKey,
},
{
name: "ShouldFailClientDataVerification",
challenge: "wrong-challenge",
relyingPartyID: "example.org",
rpOrigins: []string{"https://example.org"},
credentialBytes: credPubKey,
err: "Error validating challenge",
},
{
name: "ShouldFailAuthDataVerification",
challenge: challenge,
relyingPartyID: "wrong-rp-id.example.com",
rpOrigins: []string{"https://example.org"},
credentialBytes: credPubKey,
err: "Error validating the authenticator response",
},
{
name: "ShouldFailInvalidPublicKey",
challenge: challenge,
relyingPartyID: "example.org",
rpOrigins: []string{"https://example.org"},
credentialBytes: []byte("invalid-key"),
err: "Error parsing the assertion public key: Unsupported Public Key Type",
},
{
name: "ShouldFailSignatureVerification",
challenge: challenge,
relyingPartyID: "example.org",
rpOrigins: []string{"https://example.org"},
credentialBytes: wrongKey,
err: "Error validating the assertion signature: <nil>",
},
{
name: "ShouldFailWithAppID",
challenge: challenge,
relyingPartyID: "example.org",
rpOrigins: []string{"https://example.org"},
appID: "https://example.org",
credentialBytes: credPubKey,
err: "Error parsing the assertion public key: failed to parse FIDO public key: crypto/ecdh: invalid public key",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := par.Verify(tc.challenge, tc.relyingPartyID, tc.appID, tc.rpOrigins, nil, TopOriginExplicitVerificationMode, false, false, true, tc.credentialBytes)
if tc.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
func TestParseCredentialRequestResponse_Success(t *testing.T) {
body := io.NopCloser(bytes.NewReader([]byte(testAssertionResponses["success"])))
req := &http.Request{Body: body}
result, err := ParseCredentialRequestResponse(req)
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng", result.ID)
}
func TestCredentialAssertionResponse_Parse_AuthenticatorAttachment(t *testing.T) {
testCases := []struct {
name string
attachment string
expectedAttachment AuthenticatorAttachment
}{
{
name: "ShouldHandlePlatform",
attachment: "platform",
expectedAttachment: Platform,
},
{
name: "ShouldHandleCrossPlatform",
attachment: "cross-platform",
expectedAttachment: CrossPlatform,
},
{
name: "ShouldHandleEmpty",
attachment: "",
expectedAttachment: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
response := testAssertionResponses["success"]
var raw map[string]any
require.NoError(t, json.Unmarshal([]byte(response), &raw))
if tc.attachment != "" {
raw["authenticatorAttachment"] = tc.attachment
}
data, err := json.Marshal(raw)
require.NoError(t, err)
result, err := ParseCredentialRequestResponseBytes(data)
require.NoError(t, err)
assert.Equal(t, tc.expectedAttachment, result.AuthenticatorAttachment)
})
}
}
func TestCredentialAssertionResponse_Parse_ClientDataJSONError(t *testing.T) {
car := CredentialAssertionResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "dGVzdA",
Type: string(PublicKeyCredentialType),
},
},
AssertionResponse: AuthenticatorAssertionResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: []byte("not valid json"),
},
AuthenticatorData: []byte{
// Minimal valid auth data: 32 bytes rpIdHash + 1 byte flags + 4 bytes counter = 37 bytes.
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0x01, // Flags Value: UP.
0, 0, 0, 0, 0, 0, 0, 0, // Counter Value.
},
},
}
result, err := car.Parse()
assert.Nil(t, result)
require.Error(t, err)
}
func TestCredentialAssertionResponse_Parse_AuthDataError(t *testing.T) {
car := CredentialAssertionResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "dGVzdA",
Type: string(PublicKeyCredentialType),
},
},
AssertionResponse: AuthenticatorAssertionResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: []byte(`{"type":"webauthn.get","challenge":"dGVzdA","origin":"https://example.org"}`),
},
AuthenticatorData: []byte{0x01, 0x02}, // Too short to be valid.
},
}
result, err := car.Parse()
assert.Nil(t, result)
assert.EqualError(t, err, "Error unmarshalling auth data")
}
// testAssertionSpecVectorNoneES256 returns a parsed assertion and credentials for testing.
func testAssertionSpecVectorNoneES256(t *testing.T) (par *ParsedCredentialAssertionData, credPubKey []byte, challenge string) {
t.Helper()
const (
authenticatorDataHex = "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000"
clientDataJSONHex = "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"
signatureHex = "3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87"
credentialIDHex = "f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4" //nolint:gosec
challengeHex = "39c0e7521417ba54d43e8dc95174f423dee9bf3cd804ff6d65c857c9abf4d408"
credentialPubKeyHex = "a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"
)
credentialID, err := hex.DecodeString(credentialIDHex)
require.NoError(t, err)
credPubKey, err = hex.DecodeString(credentialPubKeyHex)
require.NoError(t, err)
challenge = base64.RawURLEncoding.EncodeToString(assertionTestDecodeHex(t, challengeHex))
id := base64.RawURLEncoding.EncodeToString(credentialID)
authenticatorData := base64.RawURLEncoding.EncodeToString(assertionTestDecodeHex(t, authenticatorDataHex))
clientDataJSON := base64.RawURLEncoding.EncodeToString(assertionTestDecodeHex(t, clientDataJSONHex))
signature := base64.RawURLEncoding.EncodeToString(assertionTestDecodeHex(t, signatureHex))
body := map[string]any{
"id": id,
"rawId": id,
"type": "public-key",
"response": map[string]any{
"authenticatorData": authenticatorData,
"clientDataJSON": clientDataJSON,
"signature": signature,
},
}
data, err := json.Marshal(body)
require.NoError(t, err)
par, err = ParseCredentialRequestResponseBytes(data)
require.NoError(t, err)
return par, credPubKey, challenge
}
func assertionTestDecodeHex(t *testing.T, s string) []byte {
t.Helper()
data, err := hex.DecodeString(s)
require.NoError(t, err)
return data
}
var testAssertionResponses = map[string]string{
// None Attestation - MacOS TouchID.
`success`: `{
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
"clientExtensionResults":{"appID":"example.com"},
"type":"public-key",
"response":{
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
"signature":"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
"userHandle":"0ToAAAAAAAAAAA"}
}
`,
`trailingData`: `{
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
"clientExtensionResults":{"appID":"example.com"},
"type":"public-key",
"response":{
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
"signature":"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
"userHandle":"0ToAAAAAAAAAAA"}
}
trailing
`,
}
+253
View File
@@ -0,0 +1,253 @@
package protocol
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// AuthenticatorAttestationResponse is the initial unpacked 'response' object received by the relying party. This
// contains the clientDataJSON object, which will be marshalled into [CollectedClientData], and the 'attestationObject',
// which contains information about the authenticator, and the newly minted public key credential. The information in
// both objects are used to verify the authenticity of the ceremony and new credential.
//
// See: https://www.w3.org/TR/webauthn/#typedefdef-publickeycredentialjson
type AuthenticatorAttestationResponse struct {
// The byte slice of clientDataJSON, which becomes CollectedClientData.
AuthenticatorResponse
Transports []string `json:"transports,omitempty"`
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
PublicKey URLEncodedBase64 `json:"publicKey"`
PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"`
// AttestationObject is the byte slice version of attestationObject.
// This attribute contains an attestation object, which is opaque to, and
// cryptographically protected against tampering by, the client. The
// attestation object contains both authenticator data and an attestation
// statement. The former contains the AAGUID, a unique credential ID, and
// the credential public key. The contents of the attestation statement are
// determined by the attestation statement format used by the authenticator.
// It also contains any additional information that the Relying Party's server
// requires to validate the attestation statement, as well as to decode and
// validate the authenticator data along with the JSON-serialized client data.
AttestationObject URLEncodedBase64 `json:"attestationObject"`
}
// ParsedAttestationResponse is the parsed version of [AuthenticatorAttestationResponse].
type ParsedAttestationResponse struct {
CollectedClientData CollectedClientData
AttestationObject AttestationObject
Transports []AuthenticatorTransport
}
// AttestationObject is the raw attestationObject.
//
// Authenticators SHOULD also provide some form of attestation, if possible. If an authenticator does, the basic
// requirement is that the authenticator can produce, for each credential public key, an attestation statement
// verifiable by the WebAuthn Relying Party. Typically, this attestation statement contains a signature by an
// attestation private key over the attested credential public key and a challenge, as well as a certificate or similar
// data providing provenance information for the attestation public key, enabling the Relying Party to make a trust
// decision. However, if an attestation key pair is not available, then the authenticator MAY either perform self
// attestation of the credential public key with the corresponding credential private key, or otherwise perform no
// attestation. All this information is returned by authenticators any time a new public key credential is generated, in
// the overall form of an attestation object.
//
// Specification: §6.5. Attestation (https://www.w3.org/TR/webauthn/#sctn-attestation)
type AttestationObject struct {
// The authenticator data, including the newly created public key. See [AuthenticatorData] for more info.
AuthData AuthenticatorData
// The byteform version of the authenticator data, used in part for signature validation.
RawAuthData []byte `json:"authData"`
// The format of the Attestation data.
Format string `json:"fmt"`
// The attestation statement data sent back if attestation is requested.
AttStatement map[string]any `json:"attStmt,omitempty"`
// Type is the attestation type as conveyed by the authenticator, one of the values defined by
// [metadata.AuthenticatorAttestationType] (i.e. "basic_full", "basic_surrogate", "attca", "anonca", "none").
// It is populated as a side-effect of a successful [AttestationObject.VerifyAttestation]; before that the field
// is empty. This field is excluded from serialization because the attestation object wire format does not carry
// this value; it is derived by the format-specific verifier.
Type string `json:"-"`
}
// NonCompoundAttestationObject is a subset of [AttestationObject] used within compound attestation statements. Each
// sub-statement in a compound attestation has its own format and attestation statement but shares authenticator data
// with the parent.
//
// Specification: §8.9. Compound Attestation Statement Format (https://www.w3.org/TR/webauthn-3/#sctn-compound-attestation)
type NonCompoundAttestationObject struct {
// The format of the Attestation data.
Format string `json:"fmt"`
// The attestation statement data sent back if attestation is requested.
AttStatement map[string]any `json:"attStmt,omitempty"`
}
type attestationFormatValidationHandler func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error)
var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler)
// RegisterAttestationFormat is a method to register attestation formats with the library. Generally using one of the
// locally registered attestation formats is enough.
func RegisterAttestationFormat(format AttestationFormat, handler attestationFormatValidationHandler) {
attestationRegistry[format] = handler
}
// Parse the values returned in the authenticator response and perform attestation verification
// Step 8. This returns a fully decoded struct with the data put into a format that can be
// used to verify the user and credential that was created.
func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationResponse, err error) {
p = &ParsedAttestationResponse{}
if err = json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData); err != nil {
return nil, ErrParsingData.WithInfo(err.Error()).WithError(err)
}
if err = webauthncbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject); err != nil {
return nil, ErrParsingData.WithInfo(err.Error()).WithError(err)
}
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
// structure to obtain the attestation statement format fmt, the authenticator data authData, and
// the attestation statement attStmt.
if err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData); err != nil {
return nil, err
}
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() {
return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag")
}
for _, t := range ccr.Transports {
if transport, ok := internalRemappedAuthenticatorTransport[t]; ok {
p.Transports = append(p.Transports, transport)
} else {
p.Transports = append(p.Transports, AuthenticatorTransport(t))
}
}
return p, nil
}
// Verify performs Steps 13 through 19 of registration verification.
//
// Steps 13 through 15 are verified against the auth data. These steps are identical to 15 through 18 for assertion so we
// handle them with AuthData.
func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, userPresenceRequired bool, mds metadata.Provider, credParams []CredentialParameter) (err error) {
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
// Begin Step 13 through 15. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
if err = a.AuthData.Verify(rpIDHash[:], nil, userVerificationRequired, userPresenceRequired); err != nil {
return err
}
// Step 16. Verify that the "alg" parameter in the credential public key in
// authData matches the alg attribute of one of the items in options.pubKeyCredParams.
var pk webauthncose.PublicKeyData
if err = webauthncbor.Unmarshal(a.AuthData.AttData.CredentialPublicKey, &pk); err != nil {
return err
}
found := false
for _, credParam := range credParams {
if int(pk.Algorithm) == int(credParam.Algorithm) {
found = true
break
}
}
if !found {
return ErrAttestationFormat.WithInfo("Credential public key algorithm not supported")
}
return a.VerifyAttestation(clientDataHash, mds)
}
// VerifyAttestation only verifies the attestation object excluding the AuthData values. If you wish to also verify the
// AuthData values you should use [Verify].
func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadata.Provider) (err error) {
// Step 18. Determine the attestation statement format by performing a
// USASCII case-sensitive match on fmt against the set of supported
// WebAuthn Attestation Statement Format Identifier values. The up-to-date
// list of registered WebAuthn Attestation Statement Format Identifier
// values is maintained in the IANA registry of the same name
// [WebAuthn-Registries] (https://www.w3.org/TR/webauthn/#biblio-webauthn-registries).
//
// Since there is not an active registry yet, we'll check it against our internal
// Supported types.
//
// But first let's make sure attestation is present. If it isn't, we don't need to handle
// any of the following steps.
if AttestationFormat(a.Format) == AttestationFormatNone {
if len(a.AttStatement) != 0 {
return ErrAttestationFormat.WithInfo("Attestation format none with attestation present")
}
a.Type = string(metadata.None)
return nil
}
var (
handler attestationFormatValidationHandler
valid bool
)
if handler, valid = attestationRegistry[AttestationFormat(a.Format)]; !valid {
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", a.Format))
}
var (
aaguid uuid.UUID
attestationType string
x5cs []any
)
// Step 19. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
// the attestation statement format fmts verification procedure given attStmt, authData and the hash of the serialized
// client data computed in step 7.
if attestationType, x5cs, err = handler(*a, clientDataHash, mds); err != nil {
var e *Error
if errors.As(err, &e) {
return e.WithInfo(attestationType)
}
return ErrInvalidAttestation.WithDetails(err.Error()).WithInfo(attestationType).WithError(err)
}
a.Type = attestationType
if len(a.AuthData.AttData.AAGUID) != 0 {
if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil {
return ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()).WithError(err)
}
}
if mds == nil {
return nil
}
if e := ValidateMetadata(context.Background(), mds, aaguid, a.Type, a.Format, x5cs); e != nil {
return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred validating metadata during attestation validation: %+v", e)).WithDetails(e.DevInfo).WithError(e)
}
return nil
}
@@ -0,0 +1,262 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"time"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// attestationFormatValidationHandlerAndroidKey is the handler for the Android Key Attestation Statement Format.
//
// An Android key attestation statement consists simply of the Android attestation statement, which is a series of DER
// encoded X.509 certificates. See the Android developer documentation. Its syntax is defined as follows:
//
// $$attStmtType //= (
//
// fmt: "android-key",
// attStmt: androidStmtFormat
// )
//
// androidStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
//
// Specification: §8.4. Android Key Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-android-key-attestation
//
//nolint:gocyclo
func attestationFormatValidationHandlerAndroidKey(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
var (
alg int64
sig []byte
ok bool
)
// Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows:
// §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
if alg, ok = att.AttStatement[stmtAlgorithm].(int64); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
// using the public key in the first certificate in x5c with the algorithm specified in alg.
var (
x5c []any
certs []*x509.Certificate
)
if x5c, certs, err = attStatementParseX5CS(att.AttStatement, stmtX5C); err != nil {
return "", nil, err
}
if len(certs) == 0 {
return "", nil, ErrInvalidAttestation.WithDetails("No certificates in x5c")
}
credCert := certs[0]
if _, err = attStatementCertChainVerify(certs, attAndroidKeyHardwareRootsCertPool, true, time.Now().Add(time.Hour*8760).UTC()); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Error validating x5c cert chain").WithError(err)
}
signatureData := append(att.RawAuthData, clientDataHash...) //nolint:gocritic // This is intentional.
if sigAlg := webauthncose.SigAlgFromCOSEAlg(webauthncose.COSEAlgorithmIdentifier(alg)); sigAlg == x509.UnknownSignatureAlgorithm {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", alg))
} else if err = credCert.CheckSignature(sigAlg, signatureData, sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err)).WithError(err)
}
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
var attPublicKeyData webauthncose.EC2PublicKeyData
if attPublicKeyData, err = verifyAttestationECDSAPublicKeyMatch(att, credCert); err != nil {
return "", nil, err
}
var valid bool
if valid, err = attPublicKeyData.Verify(signatureData, sig); err != nil || !valid {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err)).WithError(err)
}
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
// attCert.Extensions.
// As noted in §8.4.1 (https://www.w3.org/TR/webauthn/#key-attstn-cert-requirements) the Android Key Attestation
// certificate's android key attestation certificate extension data is identified by the OID
// "1.3.6.1.4.1.11129.2.1.17".
var attExtBytes []byte
for _, ext := range credCert.Extensions {
if ext.Id.Equal(oidExtensionAndroidKeystore) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
}
decoded := keyDescription{}
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions").WithError(err)
}
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
if !bytes.Equal(decoded.AttestationChallenge, clientDataHash) {
return "", nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
}
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
if decoded.SoftwareEnforced.AllApplications != nil || decoded.TeeEnforced.AllApplications != nil {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
}
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED (which == 0).
if decoded.SoftwareEnforced.Origin != KM_ORIGIN_GENERATED || decoded.TeeEnforced.Origin != KM_ORIGIN_GENERATED {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
}
// The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2).
if !contains(decoded.SoftwareEnforced.Purpose, KM_PURPOSE_SIGN) && !contains(decoded.TeeEnforced.Purpose, KM_PURPOSE_SIGN) {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
}
return string(metadata.BasicFull), x5c, err
}
func contains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
type keyDescription struct {
AttestationVersion int
AttestationSecurityLevel asn1.Enumerated
KeymasterVersion int
KeymasterSecurityLevel asn1.Enumerated
AttestationChallenge []byte
UniqueID []byte
SoftwareEnforced authorizationList
TeeEnforced authorizationList
}
type authorizationList struct {
Purpose []int `asn1:"tag:1,explicit,set,optional"`
Algorithm int `asn1:"tag:2,explicit,optional"`
KeySize int `asn1:"tag:3,explicit,optional"`
Digest []int `asn1:"tag:5,explicit,set,optional"`
Padding []int `asn1:"tag:6,explicit,set,optional"`
EcCurve int `asn1:"tag:10,explicit,optional"`
RsaPublicExponent int `asn1:"tag:200,explicit,optional"`
RollbackResistance any `asn1:"tag:303,explicit,optional"`
ActiveDateTime int `asn1:"tag:400,explicit,optional"`
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"`
UsageExpireDateTime int `asn1:"tag:402,explicit,optional"`
NoAuthRequired any `asn1:"tag:503,explicit,optional"`
UserAuthType int `asn1:"tag:504,explicit,optional"`
AuthTimeout int `asn1:"tag:505,explicit,optional"`
AllowWhileOnBody any `asn1:"tag:506,explicit,optional"`
TrustedUserPresenceRequired any `asn1:"tag:507,explicit,optional"`
TrustedConfirmationRequired any `asn1:"tag:508,explicit,optional"`
UnlockedDeviceRequired any `asn1:"tag:509,explicit,optional"`
AllApplications any `asn1:"tag:600,explicit,optional"`
ApplicationID any `asn1:"tag:601,explicit,optional"`
CreationDateTime int `asn1:"tag:701,explicit,optional"`
Origin int `asn1:"tag:702,explicit,optional"`
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
OsVersion int `asn1:"tag:705,explicit,optional"`
OsPatchLevel int `asn1:"tag:706,explicit,optional"`
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"`
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"`
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"`
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"`
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"`
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"`
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"`
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"`
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"`
VendorPatchLevel int `asn1:"tag:718,explicit,optional"`
BootPatchLevel int `asn1:"tag:719,explicit,optional"`
}
type rootOfTrust struct {
verifiedBootKey []byte //nolint:unused
deviceLocked bool //nolint:unused
verifiedBootState verifiedBootState //nolint:unused
verifiedBootHash []byte //nolint:unused
}
type verifiedBootState int
const (
Verified verifiedBootState = iota
SelfSigned
Unverified
Failed
)
const (
// KM_ORIGIN_GENERATED means generated in keymaster. Should not exist outside the TEE.
KM_ORIGIN_GENERATED = iota
// KM_ORIGIN_DERIVED means derived inside keymaster. Likely exists off-device.
KM_ORIGIN_DERIVED
// KM_ORIGIN_IMPORTED means imported into keymaster. Existed as clear text in Android.
KM_ORIGIN_IMPORTED
// KM_ORIGIN_UNKNOWN means keymaster did not record origin. This value can only be seen on keys in a keymaster0
// implementation. The keymaster0 adapter uses this value to document the fact that it is unknown whether the key
// was generated inside or imported into keymaster.
KM_ORIGIN_UNKNOWN
)
const (
// KM_PURPOSE_ENCRYPT is usable with RSA, EC and AES keys.
KM_PURPOSE_ENCRYPT = iota
// KM_PURPOSE_DECRYPT is usable with RSA, EC and AES keys.
KM_PURPOSE_DECRYPT
// KM_PURPOSE_SIGN is usable with RSA, EC and HMAC keys.
KM_PURPOSE_SIGN
// KM_PURPOSE_VERIFY is usable with RSA, EC and HMAC keys.
KM_PURPOSE_VERIFY
// KM_PURPOSE_DERIVE_KEY is usable with EC keys.
KM_PURPOSE_DERIVE_KEY
// KM_PURPOSE_WRAP is usable with wrapped keys.
KM_PURPOSE_WRAP
)
var (
attAndroidKeyHardwareRootsCertPool *x509.CertPool
)
func init() {
RegisterAttestationFormat(AttestationFormatAndroidKey, attestationFormatValidationHandlerAndroidKey)
}
File diff suppressed because one or more lines are too long
+105
View File
@@ -0,0 +1,105 @@
package protocol
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"time"
"github.com/go-webauthn/webauthn/metadata"
)
// attestationFormatValidationHandlerAppleAnonymous is the handler for the Apple Anonymous Attestation Statement Format.
//
// The syntax of an Apple attestation statement is defined as follows:
//
// $$attStmtType //= (
//
// fmt: "apple",
// attStmt: appleStmtFormat
// )
//
// appleStmtFormat = {
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
//
// Specification: §8.8. Apple Anonymous Attestation Statement Format
//
// See : https://www.w3.org/TR/webauthn/#sctn-apple-anonymous-attestation
func attestationFormatValidationHandlerAppleAnonymous(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it
// to extract the contained fields.
var (
x5c []any
certs []*x509.Certificate
)
if x5c, certs, err = attStatementParseX5CS(att.AttStatement, stmtX5C); err != nil {
return "", nil, err
}
if len(certs) == 0 {
return "", nil, ErrInvalidAttestation.WithDetails("No certificates in x5c")
}
credCert := certs[0]
if _, err = attStatementCertChainVerify(certs, attAppleHardwareRootsCertPool, true, time.Now().Add(time.Hour*8760).UTC()); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Error validating x5c cert chain").WithError(err)
}
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
nonceToHash := append(att.RawAuthData, clientDataHash...) //nolint:gocritic // This is intentional.
// Step 3. Perform SHA-256 hash of nonceToHash to produce nonce.
nonce := sha256.Sum256(nonceToHash)
// Step 4. Verify that nonce equals the value of the extension with OID 1.2.840.113635.100.8.2 in credCert.
var attExtBytes []byte
for _, ext := range credCert.Extensions {
if ext.Id.Equal(oidExtensionAppleAnonymousAttestation) {
attExtBytes = ext.Value
break
}
}
if len(attExtBytes) == 0 {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.2.840.113635.100.8.2")
}
decoded := AppleAnonymousAttestation{}
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions").WithError(err)
}
if !bytes.Equal(decoded.Nonce, nonce[:]) {
return "", nil, ErrInvalidAttestation.WithDetails("Attestation certificate does not contain expected nonce")
}
// Step 5. Verify that the credential public key equals the Subject Public Key of credCert.
if _, err = verifyAttestationECDSAPublicKeyMatch(att, credCert); err != nil {
return "", nil, err
}
// Step 6. If successful, return implementation-specific values representing attestation type Anonymization CA and
// attestation trust path x5c.
return string(metadata.AnonCA), x5c, nil
}
// AppleAnonymousAttestation represents the attestation format for Apple, who have not yet published a schema for the
// extension (as of JULY 2021.)
type AppleAnonymousAttestation struct {
Nonce []byte `asn1:"tag:1,explicit"`
}
var (
attAppleHardwareRootsCertPool *x509.CertPool
)
func init() {
RegisterAttestationFormat(AttestationFormatApple, attestationFormatValidationHandlerAppleAnonymous)
}
@@ -0,0 +1,67 @@
package protocol
import (
"crypto/sha256"
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-webauthn/webauthn/metadata"
)
func Test_VerifyAppleFormat(t *testing.T) {
type args struct {
att AttestationObject
clientDataHash []byte
}
successAttResponse := attestationTestUnpackResponse(t, appleTestResponse["success"]).Response.AttestationObject
successClientDataHash := sha256.Sum256(attestationTestUnpackResponse(t, appleTestResponse["success"]).Raw.AttestationResponse.ClientDataJSON)
testCases := []struct {
name string
args args
attestationType string
x5cs []any
err string
}{
{
"ShouldSuccessfullyParseAppleFormat",
args{
successAttResponse,
successClientDataHash[:],
},
string(metadata.AnonCA),
[]any{
[]byte{0x30, 0x82, 0x2, 0x44, 0x30, 0x82, 0x1, 0xc9, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x6, 0x1, 0x75, 0x2, 0x7d, 0x61, 0x83, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x2, 0x30, 0x48, 0x31, 0x1c, 0x30, 0x1a, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x57, 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x20, 0x43, 0x41, 0x20, 0x31, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0xa, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0x8, 0xc, 0xa, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x30, 0x1e, 0x17, 0xd, 0x32, 0x30, 0x31, 0x30, 0x30, 0x37, 0x30, 0x39, 0x34, 0x36, 0x31, 0x32, 0x5a, 0x17, 0xd, 0x32, 0x30, 0x31, 0x30, 0x30, 0x38, 0x30, 0x39, 0x35, 0x36, 0x31, 0x32, 0x5a, 0x30, 0x81, 0x91, 0x31, 0x49, 0x30, 0x47, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x40, 0x36, 0x31, 0x32, 0x37, 0x36, 0x66, 0x63, 0x30, 0x32, 0x64, 0x33, 0x66, 0x65, 0x38, 0x64, 0x31, 0x36, 0x62, 0x33, 0x33, 0x62, 0x35, 0x35, 0x34, 0x39, 0x64, 0x38, 0x31, 0x39, 0x32, 0x33, 0x36, 0x63, 0x38, 0x31, 0x37, 0x34, 0x36, 0x61, 0x38, 0x33, 0x66, 0x32, 0x65, 0x39, 0x34, 0x61, 0x36, 0x65, 0x34, 0x62, 0x65, 0x65, 0x31, 0x63, 0x37, 0x30, 0x66, 0x38, 0x31, 0x62, 0x35, 0x62, 0x63, 0x31, 0x1a, 0x30, 0x18, 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x11, 0x41, 0x41, 0x41, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0xa, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0x8, 0xc, 0xa, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x30, 0x59, 0x30, 0x13, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xfe, 0x59, 0x8, 0xbb, 0x51, 0x29, 0xc8, 0x9, 0x38, 0xb7, 0x54, 0xc0, 0x4d, 0x2b, 0x34, 0xe, 0xfa, 0x66, 0x15, 0xb9, 0x87, 0x69, 0x8b, 0xf5, 0x9d, 0xa4, 0xe5, 0x3e, 0xa3, 0xe6, 0xfe, 0xfb, 0x3, 0xda, 0xa1, 0x27, 0xd, 0x58, 0x4, 0xe8, 0xab, 0x61, 0xc1, 0x5a, 0xac, 0xa2, 0x43, 0x5c, 0x7d, 0xbf, 0x36, 0x9d, 0x71, 0xca, 0x15, 0xc5, 0x23, 0xb0, 0x0, 0x4a, 0x1b, 0x75, 0xb7, 0xa3, 0x55, 0x30, 0x53, 0x30, 0xc, 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xe, 0x6, 0x3, 0x55, 0x1d, 0xf, 0x1, 0x1, 0xff, 0x4, 0x4, 0x3, 0x2, 0x4, 0xf0, 0x30, 0x33, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x63, 0x64, 0x8, 0x2, 0x4, 0x26, 0x30, 0x24, 0xa1, 0x22, 0x4, 0x20, 0x9c, 0x60, 0x2, 0x15, 0x40, 0xb3, 0xe1, 0x98, 0x34, 0xdf, 0xe3, 0x7e, 0xc6, 0x24, 0x45, 0xc8, 0x9e, 0x1b, 0x29, 0x4f, 0x79, 0x2c, 0xe4, 0x6b, 0x94, 0x13, 0xc3, 0x23, 0xe, 0xf3, 0x86, 0x81, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x2, 0x3, 0x69, 0x0, 0x30, 0x66, 0x2, 0x31, 0x0, 0xda, 0x1c, 0x18, 0xeb, 0x23, 0xbe, 0x71, 0x0, 0x5e, 0xd2, 0x5f, 0x3c, 0x85, 0xe7, 0x34, 0x90, 0x7, 0xf2, 0xe0, 0xf4, 0xf8, 0xd3, 0x77, 0x2c, 0x9e, 0xfb, 0xe, 0xec, 0xb6, 0x2a, 0xb2, 0xf3, 0x82, 0xba, 0x96, 0x6a, 0x3c, 0x77, 0x77, 0xc8, 0xa6, 0xd6, 0x23, 0x2d, 0xc, 0x7c, 0xd5, 0xbb, 0x2, 0x31, 0x0, 0xaf, 0xb, 0xc3, 0x12, 0x37, 0xe6, 0x9e, 0xc2, 0x26, 0x94, 0xd1, 0xb3, 0x2c, 0x77, 0x14, 0x5b, 0x74, 0x37, 0xab, 0x8, 0x92, 0x63, 0xdf, 0x12, 0x5b, 0xdc, 0xa6, 0x70, 0x96, 0x87, 0xaf, 0x27, 0x77, 0x5a, 0xa, 0x60, 0x9c, 0xad, 0x9a, 0xc0, 0x3d, 0x87, 0xcb, 0xa7, 0x69, 0x3, 0x3a, 0xc8},
[]byte{0x30, 0x82, 0x2, 0x34, 0x30, 0x82, 0x1, 0xba, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x10, 0x56, 0x25, 0x53, 0x95, 0xc7, 0xa7, 0xfb, 0x40, 0xeb, 0xe2, 0x28, 0xd8, 0x26, 0x8, 0x53, 0xb6, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x30, 0x4b, 0x31, 0x1f, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x16, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x57, 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0xa, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0x8, 0xc, 0xa, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x30, 0x1e, 0x17, 0xd, 0x32, 0x30, 0x30, 0x33, 0x31, 0x38, 0x31, 0x38, 0x33, 0x38, 0x30, 0x31, 0x5a, 0x17, 0xd, 0x33, 0x30, 0x30, 0x33, 0x31, 0x33, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x48, 0x31, 0x1c, 0x30, 0x1a, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x57, 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x20, 0x43, 0x41, 0x20, 0x31, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0xa, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x13, 0x30, 0x11, 0x6, 0x3, 0x55, 0x4, 0x8, 0xc, 0xa, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x30, 0x76, 0x30, 0x10, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x5, 0x2b, 0x81, 0x4, 0x0, 0x22, 0x3, 0x62, 0x0, 0x4, 0x83, 0x2e, 0x87, 0x2f, 0x26, 0x14, 0x91, 0x81, 0x2, 0x25, 0xb9, 0xf5, 0xfc, 0xd6, 0xbb, 0x63, 0x78, 0xb5, 0xf5, 0x5f, 0x3f, 0xcb, 0x4, 0x5b, 0xc7, 0x35, 0x99, 0x34, 0x75, 0xfd, 0x54, 0x90, 0x44, 0xdf, 0x9b, 0xfe, 0x19, 0x21, 0x17, 0x65, 0xc6, 0x9a, 0x1d, 0xda, 0x5, 0xb, 0x38, 0xd4, 0x50, 0x83, 0x40, 0x1a, 0x43, 0x4f, 0xb2, 0x4d, 0x11, 0x2d, 0x56, 0xc3, 0xe1, 0xcf, 0xbf, 0xcb, 0x98, 0x91, 0xfe, 0xc0, 0x69, 0x60, 0x81, 0xbe, 0xf9, 0x6c, 0xbc, 0x77, 0xc8, 0x8d, 0xdd, 0xaf, 0x46, 0xa5, 0xae, 0xe1, 0xdd, 0x51, 0x5b, 0x5a, 0xfa, 0xab, 0x93, 0xbe, 0x9c, 0xb, 0x26, 0x91, 0xa3, 0x66, 0x30, 0x64, 0x30, 0x12, 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x8, 0x30, 0x6, 0x1, 0x1, 0xff, 0x2, 0x1, 0x0, 0x30, 0x1f, 0x6, 0x3, 0x55, 0x1d, 0x23, 0x4, 0x18, 0x30, 0x16, 0x80, 0x14, 0x26, 0xd7, 0x64, 0xd9, 0xc5, 0x78, 0xc2, 0x5a, 0x67, 0xd1, 0xa7, 0xde, 0x6b, 0x12, 0xd0, 0x1b, 0x63, 0xf1, 0xc6, 0xd7, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0xe, 0x4, 0x16, 0x4, 0x14, 0xeb, 0xae, 0x82, 0xc4, 0xff, 0xa1, 0xac, 0x5b, 0x51, 0xd4, 0xcf, 0x24, 0x61, 0x5, 0x0, 0xbe, 0x63, 0xbd, 0x77, 0x88, 0x30, 0xe, 0x6, 0x3, 0x55, 0x1d, 0xf, 0x1, 0x1, 0xff, 0x4, 0x4, 0x3, 0x2, 0x1, 0x6, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x3, 0x68, 0x0, 0x30, 0x65, 0x2, 0x31, 0x0, 0xdd, 0x8b, 0x1a, 0x34, 0x81, 0xa5, 0xfa, 0xd9, 0xdb, 0xb4, 0xe7, 0x65, 0x7b, 0x84, 0x1e, 0x14, 0x4c, 0x27, 0xb7, 0x5b, 0x87, 0x6a, 0x41, 0x86, 0xc2, 0xb1, 0x47, 0x57, 0x50, 0x33, 0x72, 0x27, 0xef, 0xe5, 0x54, 0x45, 0x7e, 0xf6, 0x48, 0x95, 0xc, 0x63, 0x2e, 0x5c, 0x48, 0x3e, 0x70, 0xc1, 0x2, 0x30, 0x2c, 0x8a, 0x60, 0x44, 0xdc, 0x20, 0x1f, 0xcf, 0xe5, 0x9b, 0xc3, 0x4d, 0x29, 0x30, 0xc1, 0x48, 0x78, 0x51, 0xd9, 0x60, 0xed, 0x6a, 0x75, 0xf1, 0xeb, 0x4a, 0xca, 0xbe, 0x38, 0xcd, 0x25, 0xb8, 0x97, 0xd0, 0xc8, 0x5, 0xbe, 0xf0, 0xc7, 0xf7, 0x8b, 0x7, 0xa5, 0x71, 0xc6, 0xe8, 0xe, 0x7}},
"",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
attestationType, x5cs, err := attestationFormatValidationHandlerAppleAnonymous(tc.args.att, tc.args.clientDataHash, nil)
assert.Equal(t, tc.attestationType, attestationType)
assert.Equal(t, tc.x5cs, x5cs)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
}
})
}
}
var appleTestResponse = map[string]string{
`success`: `{
"rawId": "U5cxFNxLbU9-SAi1K7k9atYwXhghkAMbxpL__VPtBlw",
"id": "U5cxFNxLbU9-SAi1K7k9atYwXhghkAMbxpL__VPtBlw",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia093TXZFMm1RTzZvdTBCMGpqRDBWQSIsIm9yaWdpbiI6Imh0dHBzOi8vNmNjM2M5ZTc5NjdhLm5ncm9rLmlvIn0",
"attestationObject": "o2NmbXRlYXBwbGVnYXR0U3RtdKJjYWxnJmN4NWOCWQJIMIICRDCCAcmgAwIBAgIGAXUCfWGDMAoGCCqGSM49BAMCMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwHhcNMjAxMDA3MDk0NjEyWhcNMjAxMDA4MDk1NjEyWjCBkTFJMEcGA1UEAwxANjEyNzZmYzAyZDNmZThkMTZiMzNiNTU0OWQ4MTkyMzZjODE3NDZhODNmMmU5NGE2ZTRiZWUxYzcwZjgxYjViYzEaMBgGA1UECwwRQUFBIENlcnRpZmljYXRpb24xEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR5_lkIu1EpyAk4t1TATSs0DvpmFbmHaYv1naTlPqPm_vsD2qEnDVgE6KthwVqsokNcfb82nXHKFcUjsABKG3W3o1UwUzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB_wQEAwIE8DAzBgkqhkiG92NkCAIEJjAkoSIEIJxgAhVAs-GYNN_jfsYkRcieGylPeSzka5QTwyMO84aBMAoGCCqGSM49BAMCA2kAMGYCMQDaHBjrI75xAF7SXzyF5zSQB_Lg9PjTdyye-w7stiqy84K6lmo8d3fIptYjLQx81bsCMQCvC8MSN-aewiaU0bMsdxRbdDerCJJj3xJb3KZwloevJ3daCmCcrZrAPYfLp2kDOshZAjgwggI0MIIBuqADAgECAhBWJVOVx6f7QOviKNgmCFO2MAoGCCqGSM49BAMDMEsxHzAdBgNVBAMMFkFwcGxlIFdlYkF1dGhuIFJvb3QgQ0ExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwHhcNMjAwMzE4MTgzODAxWhcNMzAwMzEzMDAwMDAwWjBIMRwwGgYDVQQDDBNBcHBsZSBXZWJBdXRobiBDQSAxMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEgy6HLyYUkYECJbn1_Na7Y3i19V8_ywRbxzWZNHX9VJBE35v-GSEXZcaaHdoFCzjUUINAGkNPsk0RLVbD4c-_y5iR_sBpYIG--Wy8d8iN3a9Gpa7h3VFbWvqrk76cCyaRo2YwZDASBgNVHRMBAf8ECDAGAQH_AgEAMB8GA1UdIwQYMBaAFCbXZNnFeMJaZ9Gn3msS0Btj8cbXMB0GA1UdDgQWBBTrroLE_6GsW1HUzyRhBQC-Y713iDAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIxAN2LGjSBpfrZ27TnZXuEHhRMJ7dbh2pBhsKxR1dQM3In7-VURX72SJUMYy5cSD5wwQIwLIpgRNwgH8_lm8NNKTDBSHhR2WDtanXx60rKvjjNJbiX0MgFvvDH94sHpXHG6A4HaGF1dGhEYXRhWJhWHo8_bWPQzAMKYRIrGXu__PkMUfuqHM4RH7Jea4WDgkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAUomGfdaNI-cYgWrq2klNk97zkcg-lAQIDJiABIVggef5ZCLtRKcgJOLdUwE0rNA76ZhW5h2mL9Z2k5T6j5v4iWCD7A9qhJw1YBOirYcFarKJDXH2_Np1xyhXFI7AASht1tw"},
"type": "public-key"
}`,
}
+117
View File
@@ -0,0 +1,117 @@
package protocol
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/metadata"
)
func init() {
RegisterAttestationFormat(AttestationFormatCompound, attestationFormatValidationHandlerCompound)
}
// attestationFormatValidationHandlerCompound is the handler for the Compound Attestation Statement Format.
//
// The syntax of a Compound Attestation statement is defined by the following CDDL:
//
// $$attStmtType //= (
//
// fmt: "compound",
// attStmt: [2* nonCompoundAttStmt]
// )
//
// nonCompoundAttStmt = { $$attStmtType } .within { fmt: text .ne "compound", * any => any }
//
// Specification: §8.9. Compound Attestation Statement Forma
//
// See: https://www.w3.org/TR/webauthn-3/#sctn-compound-attestation
//
//nolint:gocyclo
func attestationFormatValidationHandlerCompound(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
var (
aaguid uuid.UUID
raw any
ok bool
stmts []any
subStmt map[string]any
attStmts []NonCompoundAttestationObject
)
if len(att.AuthData.AttData.AAGUID) != 0 {
if aaguid, err = uuid.FromBytes(att.AuthData.AttData.AAGUID); err != nil {
return "", nil, ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()).WithError(err)
}
}
if raw, ok = att.AttStatement[stmtAttStmt]; !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement missing attStmt")
}
if stmts, ok = raw.([]any); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement attStmt isn't an array")
}
if len(stmts) < 2 {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement attStmt isn't an array with at least two other statements")
}
for _, stmt := range stmts {
if subStmt, ok = stmt.(map[string]any); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement attStmt contains one or more items that isn't an object")
}
var attStmt NonCompoundAttestationObject
if attStmt.Format, ok = subStmt[stmtFmt].(string); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement does not have a format")
}
if attStmt.AttStatement, ok = subStmt[stmtAttStmt].(map[string]any); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement does not have an attestation statement")
}
switch AttestationFormat(attStmt.Format) {
case AttestationFormatCompound:
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement has a format of compound which is not allowed")
case "":
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement has an empty format which is not allowed")
default:
if _, ok = attestationRegistry[AttestationFormat(attStmt.Format)]; !ok {
return "", nil, ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation sub-statement format %s is unsupported", attStmt.Format))
}
attStmts = append(attStmts, attStmt)
}
}
for _, attStmt := range attStmts {
object := AttestationObject{
Format: attStmt.Format,
AttStatement: attStmt.AttStatement,
AuthData: att.AuthData,
RawAuthData: att.RawAuthData,
}
var (
cx5cs []any
subAttType string
)
if subAttType, cx5cs, err = attestationRegistry[AttestationFormat(object.Format)](object, clientDataHash, mds); err != nil {
return "", nil, err
}
if mds == nil {
continue
}
if e := ValidateMetadata(context.Background(), mds, aaguid, subAttType, object.Format, cx5cs); e != nil {
return "", nil, ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred validating metadata during attestation validation: %+v", e)).WithDetails(e.DevInfo).WithError(e)
}
}
return stmtTypNone, nil, nil
}
@@ -0,0 +1,392 @@
package protocol
import (
"errors"
"reflect"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/testing/mocks"
)
func TestAttestationFormatValidationHandlerCompound(t *testing.T) {
t.Run("ShouldReturnValidationErrors", func(t *testing.T) {
withFreshAttestationRegistry(t)
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return "ok", nil, nil
}
base := AttestationObject{
Format: string(AttestationFormatCompound),
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: make([]byte, 0),
},
},
}
testCases := []struct {
name string
mutate func(a AttestationObject) AttestationObject
expected string
err string
}{
{
name: "ShouldRejectInvalidAaguidBytes",
mutate: func(a AttestationObject) AttestationObject {
a.AuthData.AttData.AAGUID = []byte{0x01}
return a
},
expected: ErrInvalidAttestation.Type,
err: "Error occurred parsing AAGUID",
},
{
name: "ShouldRejectMissingAttStmt",
mutate: func(a AttestationObject) AttestationObject {
delete(a.AttStatement, stmtAttStmt)
return a
},
expected: ErrInvalidAttestation.Type,
err: "Compound statement missing attStmt",
},
{
name: "ShouldRejectAttStmtNotArray",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = "nope"
return a
},
expected: ErrInvalidAttestation.Type,
err: "Compound statement attStmt isn't an array",
},
{
name: "ShouldRejectAttStmtWithLessThanTwoItems",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "at least two",
},
{
name: "ShouldRejectAttStmtContainingNonObject",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
123,
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "isn't an object",
},
{
name: "ShouldRejectSubStatementMissingFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "does not have a format",
},
{
name: "ShouldRejectSubStatementMissingAttStmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatPacked)},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "does not have an attestation statement",
},
{
name: "ShouldRejectSubStatementWithCompoundFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatCompound), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "format of compound",
},
{
name: "ShouldRejectSubStatementWithEmptyFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: "", stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "empty format",
},
{
name: "ShouldRejectUnsupportedSubStatementFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: "definitely-not-registered", stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrAttestationFormat.Type,
err: "unsupported",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
att := tc.mutate(base)
attestationType, x5cs, err := attestationFormatValidationHandlerCompound(att, []byte("clientDataHash"), nil)
require.Error(t, err)
assert.Empty(t, attestationType)
assert.Nil(t, x5cs)
protoErr, ok := err.(*Error)
require.True(t, ok, "expected *Error, got %T: %v", err, err)
if tc.expected != "" {
assert.Equal(t, tc.expected, protoErr.Type)
}
combined := protoErr.Details + " " + protoErr.DevInfo
assert.Contains(t, combined, tc.err)
})
}
})
t.Run("ShouldCallSubHandlersAndReturnCompound", func(t *testing.T) {
withFreshAttestationRegistry(t)
type call struct {
format string
attStmt map[string]any
auth AuthenticatorData
rawAuth []byte
}
var calls []call
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
calls = append(calls, call{
format: att.Format,
attStmt: att.AttStatement,
auth: att.AuthData,
rawAuth: att.RawAuthData,
})
return "packed-type", []any{[]byte("cert1")}, nil
}
attestationRegistry[AttestationFormatApple] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
calls = append(calls, call{
format: att.Format,
attStmt: att.AttStatement,
auth: att.AuthData,
rawAuth: att.RawAuthData,
})
return "apple-type", []any{[]byte("cert2")}, nil
}
auth := AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: make([]byte, 0),
},
}
att := AttestationObject{
Format: string(AttestationFormatCompound),
RawAuthData: []byte{0xAA, 0xBB},
AuthData: auth,
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{
stmtFmt: string(AttestationFormatPacked),
stmtAttStmt: map[string]any{"k1": "v1"},
},
map[string]any{
stmtFmt: string(AttestationFormatApple),
stmtAttStmt: map[string]any{"k2": "v2"},
},
},
},
}
gotType, gotX5Cs, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
require.NoError(t, err)
assert.Equal(t, stmtTypNone, gotType)
assert.Nil(t, gotX5Cs)
require.Len(t, calls, 2)
assert.Equal(t, string(AttestationFormatPacked), calls[0].format)
assert.Equal(t, string(AttestationFormatApple), calls[1].format)
assert.True(t, reflect.DeepEqual(calls[0].auth, auth) && reflect.DeepEqual(calls[1].auth, auth),
"expected auth data to be passed through unchanged, got: %#v", calls)
assert.True(t, reflect.DeepEqual(calls[0].rawAuth, att.RawAuthData) && reflect.DeepEqual(calls[1].rawAuth, att.RawAuthData),
"expected raw auth data to be passed through unchanged, got: %#v", calls)
})
t.Run("ShouldPropagateSubHandlerError", func(t *testing.T) {
withFreshAttestationRegistry(t)
subErr := ErrInvalidAttestation.WithDetails("sub-handler failed")
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return "", nil, subErr
}
att := AttestationObject{
Format: string(AttestationFormatCompound),
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{AAGUID: make([]byte, 0)},
},
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
}
_, _, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
require.Error(t, err)
assert.True(t, errors.Is(err, subErr))
})
t.Run("ShouldWrapMetadataValidationFailure", func(t *testing.T) {
withFreshAttestationRegistry(t)
var handlerCalls int
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
handlerCalls++
return testAttTypeSome, []any{[]byte("cert")}, nil
}
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
u := uuid.New()
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
mds.EXPECT().GetValidateEntry(gomock.Any()).Return(true)
att := AttestationObject{
Format: string(AttestationFormatCompound),
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: u[:],
},
},
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
}
_, _, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), mds)
require.Error(t, err)
protoErr, ok := err.(*Error)
require.True(t, ok)
assert.Equal(t, ErrInvalidAttestation.Type, protoErr.Type)
assert.Contains(t, protoErr.DevInfo, "Error occurred validating metadata")
assert.Equal(t, 1, handlerCalls)
})
t.Run("ShouldNotValidateMetadataWhenMdsIsNil", func(t *testing.T) {
withFreshAttestationRegistry(t)
var handlerCalls int
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
handlerCalls++
return testAttTypeSome, []any{[]byte("cert")}, nil
}
att := AttestationObject{
Format: string(AttestationFormatCompound),
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: make([]byte, 0),
},
},
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
}
gotType, gotX5Cs, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
require.NoError(t, err)
assert.Equal(t, stmtTypNone, gotType)
assert.Nil(t, gotX5Cs)
assert.Equal(t, 2, handlerCalls)
})
}
// Supporting functions.
func withFreshAttestationRegistry(t *testing.T) {
t.Helper()
orig := make(map[AttestationFormat]attestationFormatValidationHandler, len(attestationRegistry))
for k, v := range attestationRegistry {
orig[k] = v
}
t.Cleanup(func() {
for k := range attestationRegistry {
delete(attestationRegistry, k)
}
for k, v := range orig {
attestationRegistry[k] = v
}
})
}
+155
View File
@@ -0,0 +1,155 @@
package protocol
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"fmt"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// attestationFormatValidationHandlerFIDOU2F is the handler for the FIDO U2F Attestation Statement Format.
//
// The syntax of a FIDO U2F attestation statement is defined as follows:
//
// $$attStmtType //= (
//
// fmt: "fido-u2f",
// attStmt: u2fStmtFormat
// )
//
// u2fStmtFormat = {
// x5c: [ attestnCert: bytes ],
// sig: bytes
// }
//
// Specification: §8.6. FIDO U2F Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-fido-u2f-attestation
func attestationFormatValidationHandlerFIDOU2F(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
// Signing procedure. Non-normative verification procedure of expected requirement.
// If the credential public key of the attested credential is not of algorithm -7 ("ES256"), stop and return an error.
var key webauthncose.EC2PublicKeyData
if err = webauthncbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &key); err != nil {
return "", nil, ErrAttestationCertificate.WithDetails("Error parsing public key").WithError(err)
}
if webauthncose.COSEAlgorithmIdentifier(key.Algorithm) != webauthncose.AlgES256 {
return "", nil, ErrUnsupportedAlgorithm.WithDetails("Non-ES256 Public Key algorithm used")
}
var (
sig []byte
raw []byte
x5c []any
ok bool
)
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it
// to extract the contained fields.
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
if x5c, ok = att.AttStatement[stmtX5C].([]any); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data")
}
// Note: Packed Attestation, FIDO U2F Attestation, and Assertion Signatures require ASN.1 DER sig values, but it is
// RECOMMENDED that any new attestation formats defined not use ASN.1 encodings, but instead represent signatures as
// equivalent fixed-length byte arrays without internal structure, using the same representations as used by COSE
// signatures as defined in [RFC9053](https://www.rfc-editor.org/rfc/rfc9053.html) and
// [RFC8230](https://www.rfc-editor.org/rfc/rfc8230.html).
// This is described in §6.5.5 https://www.w3.org/TR/webauthn-3/#sctn-signature-attestation-types.
// Check for "sig" which is The attestation signature. The signature was calculated over the (raw) U2F
// registration response message https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]
// received by the client from the authenticator.
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Missing sig data")
}
// Step 2.
// 1. Check that x5c has exactly one element and let attCert be that element.
// 2. Let certificate public key be the public key conveyed by attCert.
// 3. If certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve, terminate this
// algorithm and return an appropriate error.
// Step 2.1.
if len(x5c) != 1 {
return "", nil, ErrAttestationFormat.WithDetails("x5c must contain exactly one element")
}
// Step 2.2.
if raw, ok = x5c[0].([]byte); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Error decoding ASN.1 data from x5c")
}
attCert, err := x509.ParseCertificate(raw)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate").WithError(err)
}
// Step 2.3.
if attCert.PublicKeyAlgorithm != x509.ECDSA {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate public key algorithm is not ECDSA")
}
// Step 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey
// from authenticatorData.attestedCredentialData.
rpIdHash := att.AuthData.RPIDHash
credentialID := att.AuthData.AttData.CredentialID
// Step 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of RFC8152 [https://www.w3.org/TR/webauthn/#biblio-rfc8152])
// to Raw ANSI X9.62 public key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key
// Representation Formats of
// [FIDO-Registry](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats)).
// Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey, and confirm
// its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm and return an
// appropriate error.
// Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey, and confirm
// its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm and return an
// appropriate error.
credentialPublicKey, ok := attCert.PublicKey.(*ecdsa.PublicKey)
if !ok || credentialPublicKey.Curve != elliptic.P256() {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate does not contain a P-256 ECDSA public key")
}
if len(key.XCoord) != 32 || len(key.YCoord) != 32 {
return "", nil, ErrAttestation.WithDetails("X or Y Coordinate for key is invalid length")
}
// Let publicKeyU2F be the concatenation 0x04 || x || y.
publicKeyU2F := bytes.NewBuffer([]byte{0x04})
publicKeyU2F.Write(key.XCoord)
publicKeyU2F.Write(key.YCoord)
// Step 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
// (see Section 4.3 of [FIDO-U2F-Message-Formats](https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success)).
verificationData := bytes.NewBuffer([]byte{0x00})
verificationData.Write(rpIdHash)
verificationData.Write(clientDataHash)
verificationData.Write(credentialID)
verificationData.Write(publicKeyU2F.Bytes())
// Step 6. Verify the sig using verificationData and the certificate public key per section 4.1.4 of [SEC1] with
// SHA-256 as the hash function used in step two.
if err = attCert.CheckSignature(x509.ECDSAWithSHA256, verificationData.Bytes(), sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err)).WithError(err)
}
// TODO: Step 7. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt
// conveys a Basic or AttCA attestation.
// Step 8. If successful, return implementation-specific values representing attestation type Basic, AttCA or
// uncertainty, and attestation trust path x5c.
return string(metadata.BasicFull), x5c, nil
}
func init() {
RegisterAttestationFormat(AttestationFormatFIDOUniversalSecondFactor, attestationFormatValidationHandlerFIDOU2F)
}
@@ -0,0 +1,342 @@
package protocol
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/metadata"
)
func TestVerifyU2FFormat(t *testing.T) {
successAttResponse := attestationTestUnpackResponse(t, u2fTestResponse["success"]).Response.AttestationObject
successClientDataHash := sha256.Sum256(attestationTestUnpackResponse(t, u2fTestResponse["success"]).Raw.AttestationResponse.ClientDataJSON)
testCases := []struct {
name string
att AttestationObject
clientDataHash []byte
attestationType string
err string
}{
{
name: "ShouldSuccessfullyVerifyU2FFormat",
att: successAttResponse,
clientDataHash: successClientDataHash[:],
attestationType: string(metadata.BasicFull),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
attestationType, _, err := attestationFormatValidationHandlerFIDOU2F(tc.att, tc.clientDataHash, nil)
if tc.err != "" {
require.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.attestationType, attestationType)
})
}
}
func TestVerifyU2FFormat_Errors(t *testing.T) {
zeroAAGUID := make([]byte, 16)
es256Key := []byte{
0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01,
0x21, 0x58, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x22, 0x58, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
}
testCases := []struct {
name string
att AttestationObject
err string
}{
{
name: "ShouldFailInvalidPublicKey",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: []byte("not-cbor"),
},
},
},
err: "Error parsing public key",
},
{
name: "ShouldFailNonES256Algorithm",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: []byte{
0xa3, 0x01, 0x02, 0x03, 0x39, 0x01, 0x00, 0x20, 0x01,
},
},
},
},
err: "Non-ES256 Public Key algorithm used",
},
{
name: "ShouldFailMissingX5C",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{},
},
err: "Missing properly formatted x5c data",
},
{
name: "ShouldFailMissingSig",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{
stmtX5C: []any{[]byte("cert")},
},
},
err: "Missing sig data",
},
{
name: "ShouldFailX5CNotExactlyOne",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{
stmtX5C: []any{[]byte("cert1"), []byte("cert2")},
stmtSignature: []byte("sig"),
},
},
err: "x5c must contain exactly one element",
},
{
name: "ShouldFailX5CElementNotBytes",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{
stmtX5C: []any{"not-bytes"},
stmtSignature: []byte("sig"),
},
},
err: "Error decoding ASN.1 data from x5c",
},
{
name: "ShouldFailX5CInvalidCert",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{
stmtX5C: []any{[]byte("not-a-cert")},
stmtSignature: []byte("sig"),
},
},
err: "Error parsing certificate from ASN.1 data into certificate",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := attestationFormatValidationHandlerFIDOU2F(tc.att, []byte("hash"), nil)
require.EqualError(t, err, tc.err)
})
}
}
func TestVerifyU2FFormat_CertificateErrors(t *testing.T) {
zeroAAGUID := make([]byte, 16)
es256Key := []byte{
0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01,
0x21, 0x58, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x22, 0x58, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
}
shortCoordKey := []byte{
0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01,
0x21, 0x58, 0x10,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x22, 0x58, 0x10,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
}
rsaCertDER := u2fTestGenerateRSACert(t)
p384CertDER := u2fTestGenerateP384Cert(t)
p256CertDER := u2fTestGenerateP256Cert(t)
testCases := []struct {
name string
att AttestationObject
err string
}{
{
name: "ShouldFailNonECDSACert",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{
stmtX5C: []any{rsaCertDER},
stmtSignature: []byte("sig"),
},
},
err: "Attestation certificate public key algorithm is not ECDSA",
},
{
name: "ShouldFailNonP256Curve",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: es256Key,
},
},
AttStatement: map[string]any{
stmtX5C: []any{p384CertDER},
stmtSignature: []byte("sig"),
},
},
err: "Attestation certificate does not contain a P-256 ECDSA public key",
},
{
name: "ShouldFailShortCoordinates",
att: AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: zeroAAGUID,
CredentialPublicKey: shortCoordKey,
},
},
AttStatement: map[string]any{
stmtX5C: []any{p256CertDER},
stmtSignature: []byte("sig"),
},
},
err: "X or Y Coordinate for key is invalid length",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := attestationFormatValidationHandlerFIDOU2F(tc.att, []byte("hash"), nil)
require.EqualError(t, err, tc.err)
})
}
}
// Supporting functions.
func u2fTestGenerateRSACert(t *testing.T) []byte {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test RSA"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
require.NoError(t, err)
return der
}
func u2fTestGenerateP384Cert(t *testing.T) []byte {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test P384"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
require.NoError(t, err)
return der
}
func u2fTestGenerateP256Cert(t *testing.T) []byte {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test P256"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
require.NoError(t, err)
return der
}
var u2fTestResponse = map[string]string{
`success`: `{
"rawId": "7nJsttr4dLSsmrWnaHB3espJ0ua9rsJ2ws-93BFcNOP64g_s_4wLFDvklrNYcg0BCN6ddUjJLxDfDSBreKQLAw",
"id": "7nJsttr4dLSsmrWnaHB3espJ0ua9rsJ2ws-93BFcNOP64g_s_4wLFDvklrNYcg0BCN6ddUjJLxDfDSBreKQLAw",
"response": {
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJhTDJ1d0FwZ3d1bUJ6VFlDY29MMF80RFJ2X21mWXlremdxSkJGb0pqX1dDS05aT3B2VVFueWpkd01XSVdLY1k4NDR0eUROTE81cFFQQk1KckhQel8zZyIsImNsaWVudEV4dGVuc2lvbnMiOnt9LCJoYXNoQWxnb3JpdGhtIjoiU0hBLTI1NiIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjQ0MzI5IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9",
"attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgRMxowC__Z-mgVR6netL6C7Q15weqiTCPwwq1EaeJVqMCIQCHb9cCad1VloGhQ60mw7KTJhkx61mfgKKwHUVZf1wR6mN4NWOBWQLCMIICvjCCAaagAwIBAgIEdIb9wjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTk1NTAwMzg0MjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJVd8633JH0xde_9nMTzGk6HjrrhgQlWYVD7OIsuX2Unv1dAmqWBpQ0KxS8YRFwKE1SKE1PIpOWacE5SO8BN6-2jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEPigEfOMCk0VgAYXER-e3H0wDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAMVxIgOaaUn44Zom9af0KqG9J655OhUVBVW-q0As6AIod3AH5bHb2aDYakeIyyBCnnGMHTJtuekbrHbXYXERIn4aKdkPSKlyGLsA_A-WEi-OAfXrNVfjhrh7iE6xzq0sg4_vVJoywe4eAJx0fS-Dl3axzTTpYl71Nc7p_NX6iCMmdik0pAuYJegBcTckE3AoYEg4K99AM_JaaKIblsbFh8-3LxnemeNf7UwOczaGGvjS6UzGVI0Odf9lKcPIwYhuTxM5CaNMXTZQ7xq4_yTfC3kPWtE4hFT34UJJflZBiLrxG4OsYxkHw_n5vKgmpspB3GfYuYTWhkDKiE8CYtyg87mhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQO5ybLba-HS0rJq1p2hwd3rKSdLmva7CdsLPvdwRXDTj-uIP7P-MCxQ75JazWHINAQjenXVIyS8Q3w0ga3ikCwOlAQIDJiABIVggUOAo5xqsJoPfJWsU50h7c2S7_llP0KwGI6vJkEj1N48iWCA2TMSeBfhJ84HyMQQgjJvBiA6JnHA0chxSlmuZeT9Xgg"
},
"type": "public-key"
}`,
}
+254
View File
@@ -0,0 +1,254 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"strings"
"time"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func init() {
RegisterAttestationFormat(AttestationFormatPacked, attestationFormatValidationHandlerPacked)
}
// attestationFormatValidationHandlerPacked is the handler for the Packed Attestation Statement Format.
//
// The syntax of a Packed Attestation statement is defined by the following CDDL:
//
// $$attStmtType //= (
//
// fmt: "packed",
// attStmt: packedStmtFormat
// )
//
// packedStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
// } //
// {
// alg: COSEAlgorithmIdentifier
// sig: bytes,
// }
//
// Specification: §8.2. Packed Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-packed-attestation
func attestationFormatValidationHandlerPacked(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
var (
alg int64
sig []byte
x5c []any
ok bool
)
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
if alg, ok = att.AttStatement[stmtAlgorithm].(int64); !ok {
return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
if x5c, ok = att.AttStatement[stmtX5C].([]any); ok {
// Handle Basic Attestation steps for the x509 Certificate.
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c, mds)
}
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
// Also make sure the we did not have an x509.
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement[stmtECDAAKID].([]byte)
if ecdaaKeyPresent {
// Handle ECDAA Attestation steps for the x509 Certificate.
return handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID, mds)
}
// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
return handleSelfAttestation(alg, att.AuthData.AttData.CredentialPublicKey, att.RawAuthData, clientDataHash, sig, mds)
}
// Handle the attestation steps laid out in the basic format.
//
//nolint:gocyclo
func handleBasicAttestation(sig, clientDataHash, authData, aaguid []byte, alg int64, x5c []any, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
// Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData
// and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
var attestnCert *x509.Certificate
for i, raw := range x5c {
rawByes, ok := raw.([]byte)
if !ok {
return "", x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
cert, err := x509.ParseCertificate(rawByes)
if err != nil {
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
}
if cert.NotBefore.After(time.Now()) || cert.NotAfter.Before(time.Now()) {
return "", x5c, ErrAttestationFormat.WithDetails("Cert in chain is either no longer valid or not yet valid")
}
if i == 0 {
attestnCert = cert
}
}
if attestnCert == nil {
return "", x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(authData, clientDataHash...) //nolint:gocritic // This is intentional.
if sigAlg := webauthncose.SigAlgFromCOSEAlg(webauthncose.COSEAlgorithmIdentifier(alg)); sigAlg == x509.UnknownSignatureAlgorithm {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", alg))
} else if err = attestnCert.CheckSignature(sigAlg, signatureData, sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err)).WithError(err)
}
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
// §8.2.1 can be found here https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
// Step 2.2.1 (from §8.2.1) Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
if attestnCert.Version != 3 {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate is incorrect version")
}
// Step 2.2.2 (from §8.2.1) Subject field MUST be set to:
// Subject-C
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString).
if len(attestnCert.Subject.Country) != 1 || !isISO3166Alpha2(attestnCert.Subject.Country[0]) {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid")
}
// Subject-O
// Legal name of the Authenticator vendor (UTF8String).
subjectString := strings.Join(attestnCert.Subject.Organization, "")
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid")
}
// Subject-OU
// Literal string “Authenticator Attestation” (UTF8String).
subjectString = strings.Join(attestnCert.Subject.OrganizationalUnit, " ")
if subjectString != "Authenticator Attestation" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organizational Unit is invalid")
}
// Subject-CN
// A UTF8String of the vendors choosing.
subjectString = attestnCert.Subject.CommonName
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set")
}
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
var foundAAGUID []byte
for _, extension := range attestnCert.Extensions {
if extension.Id.Equal(oidFIDOGenCeAAGUID) {
if extension.Critical {
return "", x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
}
foundAAGUID = extension.Value
}
}
// We validate the AAGUID as mentioned above
// This is not well defined in§8.2.1 but mentioned in step 2.3: we validate the AAGUID if it is present within the certificate
// and make sure it matches the auth data AAGUID
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
if len(foundAAGUID) > 0 {
var unMarshalledAAGUID []byte
if _, err = asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID); err != nil {
return "", x5c, ErrInvalidAttestation.WithDetails("Error unmarshalling AAGUID from certificate")
}
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
return "", x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
}
}
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
if attestnCert.IsCA {
return "", x5c, ErrInvalidAttestation.WithDetails("Attestation certificate's Basic Constraints marked as CA")
}
// Note for 2.2.5 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL
// Distribution Point extension [RFC5280](https://www.w3.org/TR/webauthn/#biblio-rfc5280) are
// both OPTIONAL as the status of many attestation certificates is available through authenticator
// metadata services. See, for example, the FIDO Metadata Service
// [FIDOMetadataService] (https://www.w3.org/TR/webauthn/#biblio-fidometadataservice)
// Step 2.4 If successful, return attestation type Basic and attestation trust path x5c.
// We don't handle trust paths yet but we're done.
return string(metadata.BasicFull), x5c, nil
}
func handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
}
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, sig []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
verificationData := append(authData, clientDataHash...) //nolint:gocritic // This is intentional.
var (
key any
valid bool
)
if key, err = webauthncose.ParsePublicKey(pubKey); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v", err))
}
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
switch k := key.(type) {
case webauthncose.OKPPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
case webauthncose.EC2PublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
case webauthncose.RSAPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
default:
return "", nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
}
if err != nil {
return "", nil, err
}
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
// clientDataHash using the credential public key with alg.
if valid, err = webauthncose.VerifySignature(key, verificationData, sig); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error verifying the signature: %+v", err)).WithError(err)
} else if !valid {
return "", nil, ErrInvalidAttestation.WithDetails("Unable to verify signature")
}
return string(metadata.BasicSurrogate), nil, err
}
func verifyKeyAlgorithm(keyAlgorithm, attestedAlgorithm int64) error {
if keyAlgorithm != attestedAlgorithm {
return ErrInvalidAttestation.WithDetails("Public key algorithm does not equal att statement algorithm")
}
return nil
}
@@ -0,0 +1,509 @@
package protocol
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func Test_VerifyPackedFormat(t *testing.T) {
successAttResponseES256 := attestationTestUnpackResponse(t, packedTestResponseES256["success"]).Response.AttestationObject
successClientDataHashES256 := sha256.Sum256(attestationTestUnpackResponse(t, packedTestResponseES256["success"]).Raw.AttestationResponse.ClientDataJSON)
successAttResponseES512 := attestationTestUnpackResponse(t, packedTestResponseES512["success"]).Response.AttestationObject
successClientDataHashES512 := sha256.Sum256(attestationTestUnpackResponse(t, packedTestResponseES512["success"]).Raw.AttestationResponse.ClientDataJSON)
successAttResponseSolo2 := attestationTestUnpackResponse(t, packedTestResponseSolo2["success"]).Response.AttestationObject
successClientDataHashSolo2 := sha256.Sum256(attestationTestUnpackResponse(t, packedTestResponseSolo2["success"]).Raw.AttestationResponse.ClientDataJSON)
testCases := []struct {
name string
att AttestationObject
clientDataHash []byte
attestationType string
err string
}{
{
name: "ShouldSuccessfullyVerifyES256",
att: successAttResponseES256,
clientDataHash: successClientDataHashES256[:],
attestationType: string(metadata.BasicFull),
},
{
name: "ShouldSuccessfullyVerifyES512SelfAttestation",
att: successAttResponseES512,
clientDataHash: successClientDataHashES512[:],
attestationType: string(metadata.BasicSurrogate),
},
{
name: "ShouldSuccessfullyVerifySolo2",
att: successAttResponseSolo2,
clientDataHash: successClientDataHashSolo2[:],
attestationType: string(metadata.BasicFull),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
attestationType, _, err := attestationFormatValidationHandlerPacked(tc.att, tc.clientDataHash, nil)
if tc.err != "" {
require.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.attestationType, attestationType)
})
}
}
func TestPackedFormat_HandlerErrors(t *testing.T) {
testCases := []struct {
name string
attStatement map[string]any
err string
}{
{
name: "ShouldFailMissingAlg",
attStatement: map[string]any{},
err: "Error retrieving alg value",
},
{
name: "ShouldFailAlgWrongType",
attStatement: map[string]any{stmtAlgorithm: "not-int"},
err: "Error retrieving alg value",
},
{
name: "ShouldFailMissingSig",
attStatement: map[string]any{stmtAlgorithm: int64(-7)},
err: "Error retrieving sig value",
},
{
name: "ShouldFailSigWrongType",
attStatement: map[string]any{stmtAlgorithm: int64(-7), stmtSignature: "not-bytes"},
err: "Error retrieving sig value",
},
{
name: "ShouldReturnECDAANotImplemented",
attStatement: map[string]any{stmtAlgorithm: int64(-7), stmtSignature: []byte("sig"), stmtECDAAKID: []byte("keyid")},
err: "This field is not yet supported by the WebAuthn spec",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
att := AttestationObject{
Format: "packed",
AttStatement: tc.attStatement,
}
_, _, err := attestationFormatValidationHandlerPacked(att, []byte("hash"), nil)
require.EqualError(t, err, tc.err)
})
}
}
func TestPackedFormat_BasicAttestationErrors(t *testing.T) {
testCases := []struct {
name string
x5c []any
alg int64
err string
}{
{
name: "ShouldFailX5CElementNotBytes",
x5c: []any{"not-bytes"},
alg: int64(-7),
err: "Error getting certificate from x5c cert chain",
},
{
name: "ShouldFailX5CInvalidCert",
x5c: []any{[]byte("not-a-cert")},
alg: int64(-7),
err: "Error parsing certificate from ASN.1 data: x509: malformed certificate",
},
{
name: "ShouldFailEmptyX5C",
x5c: []any{},
alg: int64(-7),
err: "Error getting certificate from x5c cert chain",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := handleBasicAttestation([]byte("sig"), []byte("hash"), []byte("auth"), []byte("aaguid"), tc.alg, tc.x5c, nil)
require.EqualError(t, err, tc.err)
})
}
}
func TestPackedFormat_BasicAttestationSignatureAndTimeErrors(t *testing.T) {
authData := []byte("fake-auth-data")
clientDataHash := []byte("fake-client-hash")
signatureData := append(authData, clientDataHash...) //nolint:gocritic
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
validTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"US"},
Organization: []string{"Test"},
OrganizationalUnit: []string{"Authenticator Attestation"},
CommonName: "Test Cert",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
validCertDER, err := x509.CreateCertificate(rand.Reader, validTemplate, validTemplate, &key.PublicKey, key)
require.NoError(t, err)
h := sha256.Sum256(signatureData)
validSig, err := key.Sign(rand.Reader, h[:], nil)
require.NoError(t, err)
expiredTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: validTemplate.Subject,
NotBefore: time.Now().Add(-48 * time.Hour),
NotAfter: time.Now().Add(-24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
expiredCertDER, err := x509.CreateCertificate(rand.Reader, expiredTemplate, expiredTemplate, &key.PublicKey, key)
require.NoError(t, err)
futureTemplate := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: validTemplate.Subject,
NotBefore: time.Now().Add(24 * time.Hour),
NotAfter: time.Now().Add(48 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
futureCertDER, err := x509.CreateCertificate(rand.Reader, futureTemplate, futureTemplate, &key.PublicKey, key)
require.NoError(t, err)
testCases := []struct {
name string
x5c []any
alg int64
sig []byte
err string
}{
{
name: "ShouldFailExpiredCert",
x5c: []any{expiredCertDER},
alg: int64(webauthncose.AlgES256),
sig: validSig,
err: "Cert in chain is either no longer valid or not yet valid",
},
{
name: "ShouldFailFutureCert",
x5c: []any{futureCertDER},
alg: int64(webauthncose.AlgES256),
sig: validSig,
err: "Cert in chain is either no longer valid or not yet valid",
},
{
name: "ShouldFailUnsupportedAlgorithm",
x5c: []any{validCertDER},
alg: int64(0),
sig: validSig,
err: "Unsupported COSE alg: 0",
},
{
name: "ShouldFailInvalidSignature",
x5c: []any{validCertDER},
alg: int64(webauthncose.AlgES256),
sig: []byte("bad-signature"),
err: "Signature validation error: x509: ECDSA verification failure",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := handleBasicAttestation(tc.sig, clientDataHash, authData, nil, tc.alg, tc.x5c, nil)
require.EqualError(t, err, tc.err)
})
}
}
func TestPackedFormat_SelfAttestationErrors(t *testing.T) {
pcc := attestationTestUnpackResponse(t, packedTestResponseES512["success"])
validPubKey := pcc.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
testCases := []struct {
name string
alg int64
pub []byte
err string
}{
{
name: "ShouldFailInvalidPublicKey",
alg: int64(-7),
pub: []byte("not-cbor"),
err: "Error parsing the public key: Unsupported Public Key Type",
},
{
name: "ShouldFailAlgorithmMismatch",
alg: int64(-7),
pub: validPubKey,
err: "Public key algorithm does not equal att statement algorithm",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := handleSelfAttestation(tc.alg, tc.pub, []byte("auth"), []byte("hash"), []byte("sig"), nil)
require.EqualError(t, err, tc.err)
})
}
}
func TestVerifyKeyAlgorithm(t *testing.T) {
testCases := []struct {
name string
keyAlg int64
attAlg int64
err string
}{
{
name: "ShouldSucceedWhenMatch",
keyAlg: int64(-7),
attAlg: int64(-7),
},
{
name: "ShouldFailWhenMismatch",
keyAlg: int64(-7),
attAlg: int64(-257),
err: "Public key algorithm does not equal att statement algorithm",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := verifyKeyAlgorithm(tc.keyAlg, tc.attAlg)
if tc.err != "" {
require.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestPackedFormat_BasicAttestationCertRequirements(t *testing.T) {
authData := []byte("fake-auth-data")
clientDataHash := []byte("fake-client-hash")
signatureData := append(authData, clientDataHash...) //nolint:gocritic
testCases := []struct {
name string
template *x509.Certificate
err string
}{
{
name: "ShouldFailMissingCountry",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Org"},
OrganizationalUnit: []string{"Authenticator Attestation"},
CommonName: "Test",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Country Code is invalid",
},
{
name: "ShouldFailUnassignedCountry",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"ZI"},
Organization: []string{"Test Org"},
OrganizationalUnit: []string{"Authenticator Attestation"},
CommonName: "Test",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Country Code is invalid",
},
{
name: "ShouldFailWrongCaseCountry",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"us"},
Organization: []string{"Test Org"},
OrganizationalUnit: []string{"Authenticator Attestation"},
CommonName: "Test",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Country Code is invalid",
},
{
name: "ShouldFailAlpha3Country",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"USA"},
Organization: []string{"Test Org"},
OrganizationalUnit: []string{"Authenticator Attestation"},
CommonName: "Test",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Country Code is invalid",
},
{
name: "ShouldFailMissingOrganization",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"US"},
OrganizationalUnit: []string{"Authenticator Attestation"},
CommonName: "Test",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Organization is invalid",
},
{
name: "ShouldFailWrongOrganizationalUnit",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"US"},
Organization: []string{"Test Org"},
OrganizationalUnit: []string{"Wrong OU"},
CommonName: "Test",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Organizational Unit is invalid",
},
{
name: "ShouldFailMissingCommonName",
template: &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"US"},
Organization: []string{"Test Org"},
OrganizationalUnit: []string{"Authenticator Attestation"},
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
},
err: "Attestation Certificate Common Name not set",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
certDER, err := x509.CreateCertificate(rand.Reader, tc.template, tc.template, &key.PublicKey, key)
require.NoError(t, err)
sigAlg := webauthncose.SigAlgFromCOSEAlg(webauthncose.AlgES256)
cert, err := x509.ParseCertificate(certDER)
require.NoError(t, err)
sig, err := key.Sign(rand.Reader, packedTestHashForSigAlg(t, sigAlg, signatureData), nil)
require.NoError(t, err)
x5c := []any{certDER}
_, _, err = handleBasicAttestation(sig, clientDataHash, authData, nil, int64(webauthncose.AlgES256), x5c, nil)
require.EqualError(t, err, tc.err)
_ = cert
})
}
}
// Supporting functions.
func packedTestHashForSigAlg(t *testing.T, alg x509.SignatureAlgorithm, data []byte) []byte {
t.Helper()
switch alg {
case x509.ECDSAWithSHA256:
h := sha256.Sum256(data)
return h[:]
default:
t.Fatalf("unsupported signature algorithm: %v", alg)
return nil
}
}
// Test data.
var packedTestResponseES256 = map[string]string{
`success`: `{
"rawId": "hUf7WI3IZmoLOzYhHFe7U-df4QD17lQBMi9iS-z3dWFlr79MXOoTR8dJzb_Y7sAstHBrcC1nv8pOr6aFz50K65juYXWt8k26bKu-Hu4CulPo53bIStJ4kpOr2Dlr6Z4D",
"id": "hUf7WI3IZmoLOzYhHFe7U-df4QD17lQBMi9iS-z3dWFlr79MXOoTR8dJzb_Y7sAstHBrcC1nv8pOr6aFz50K65juYXWt8k26bKu-Hu4CulPo53bIStJ4kpOr2Dlr6Z4D",
"response": {
"clientDataJSON": "ew0KCSJ0eXBlIiA6ICJ3ZWJhdXRobi5jcmVhdGUiLA0KCSJjaGFsbGVuZ2UiIDogIlBfSktRaWQxdHZzNEJsdGlaMUNzRWZYbDNHWjBJcG1MUFVRRmxZLW8weDlzZ3ZDS3lXNXpQUkpjTzc3M2VpOE93WEN5Rjl1Wk42X3B5elhOT0FKUjdBIiwNCgkib3JpZ2luIiA6ICJodHRwczovL2xvY2FsaG9zdDo0NDMyOSIsDQoJInRva2VuQmluZGluZyIgOiANCgl7DQoJCSJzdGF0dXMiIDogInN1cHBvcnRlZCINCgl9DQp9",
"attestationObject": "o2NmbXRmcGFja2VkaGF1dGhEYXRhWORJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAChiQjgyRUQ3M0M4RkI0RTVBMgBghUf7WI3IZmoLOzYhHFe7U-df4QD17lQBMi9iS-z3dWFlr79MXOoTR8dJzb_Y7sAstHBrcC1nv8pOr6aFz50K65juYXWt8k26bKu-Hu4CulPo53bIStJ4kpOr2Dlr6Z4DpQECAyYgASFYIA9RHvpjfWoWN_Im7eYwG1Y8kA77s7QH9uf9TePknT3mIlggJ8tNsMrPPrewstqf65ItALMxBIi4VUoTIZEyAkXN6U1nYXR0U3RtdKNjYWxnJmNzaWdYRzBFAiBsbcx3U1xgYinrnczLOUDOlYGvYENDGzv77WdM1W3FTQIhAJ16HUK8XyG83cOVQFKkijdgHyDV97XylRMU_rWHAkP_Y3g1Y4NZAkUwggJBMIIB6KADAgECAhAVn3vCzYkY8Shrk0j6nzPiMAoGCCqGSM49BAMCMEkxCzAJBgNVBAYTAkNOMR0wGwYDVQQKDBRGZWl0aWFuIFRlY2hub2xvZ2llczEbMBkGA1UEAwwSRmVpdGlhbiBGSURPMiBDQS0xMCAXDTE4MDQxMTAwMDAwMFoYDzIwMzMwNDEwMjM1OTU5WjBvMQswCQYDVQQGEwJDTjEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9sb2dpZXMxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xHTAbBgNVBAMMFEZUIEJpb1Bhc3MgRklETzIgVVNCMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgAZ1XFn7yUmwFajSCpJYl76DCrLv6Cz4j-2gkJZj5UjHHxEnBTO0JEZ4nUz-4QFDipTpgz3iACwvKh3Xb03bXaOBiTCBhjAdBgNVHQ4EFgQUelSCQoBi2Irnr4SYJcSvkak0mPIwHwYDVR0jBBgwFoAUTTvYxGcVG7sT6POE2DBPnWkVwIMwDAYDVR0TAQH_BAIwADATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBBCODJFRDczQzhGQjRFNUEyMAoGCCqGSM49BAMCA0cAMEQCICRLRaO-iNy34CWixqMSz_uG7bwnSiLBBS4xSFHw6LCHAiA0Gr9OHCTyCxpz1T2swqn5FbQbsjprAW8f7_jg5_iQwFkB_zCCAfswggGgoAMCAQICEBWfe8LNiRjxKGuTSPqfM-EwCgYIKoZIzj0EAwIwSzELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFEZlaXRpYW4gVGVjaG5vbG9naWVzMR0wGwYDVQQDDBRGZWl0aWFuIEZJRE8gUm9vdCBDQTAgFw0xODA0MTAwMDAwMDBaGA8yMDM4MDQwOTIzNTk1OVowSTELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFEZlaXRpYW4gVGVjaG5vbG9naWVzMRswGQYDVQQDDBJGZWl0aWFuIEZJRE8yIENBLTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASOfmAJ7MEWZcyg-sPpb-UIO5VtVyUR61sy9NZnOVfdZ9i2FzUd_0u5gOYLqbkzuZo0MPMX6iETB1a9agd03nWPo2YwZDAdBgNVHQ4EFgQUTTvYxGcVG7sT6POE2DBPnWkVwIMwHwYDVR0jBBgwFoAU0aGYTYF_w7lr9gdnvVAS_pBF8VQwEgYDVR0TAQH_BAgwBgEB_wIBADAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSQAwRgIhAPt_o9JAR6ERUMJ4Vm0hzJAWmOyhf087SDRTecpg5MJlAiEA6wpDwYjB172IPpEkYFbCsLlbWKJ0bwufPKkcKS0rWexZAdwwggHYMIIBfqADAgECAhAVn3vCzYkY8Shrk0j6nzPWMAoGCCqGSM49BAMCMEsxCzAJBgNVBAYTAkNOMR0wGwYDVQQKDBRGZWl0aWFuIFRlY2hub2xvZ2llczEdMBsGA1UEAwwURmVpdGlhbiBGSURPIFJvb3QgQ0EwIBcNMTgwNDAxMDAwMDAwWhgPMjA0ODAzMzEyMzU5NTlaMEsxCzAJBgNVBAYTAkNOMR0wGwYDVQQKDBRGZWl0aWFuIFRlY2hub2xvZ2llczEdMBsGA1UEAwwURmVpdGlhbiBGSURPIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASd8ApuO8xfUTLVvqT5ZBB01Uy30mAZbInc-8zgFIrlepN-j77SgCP_i2fDIgvQcUFH1K36S2OpJcN-OJcC6uzzo0IwQDAdBgNVHQ4EFgQU0aGYTYF_w7lr9gdnvVAS_pBF8VQwDwYDVR0TAQH_BAUwAwEB_zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSAAwRQIhALexPWUGMZ4X7EpOnNXUphTZyRqFN3iYsnLNg6Foe_iKAiAPYliR_IflDgGmjyuug7Qi3uhiMXaSDL95JndT0aVqrA"
},
"type": "public-key"
}`,
}
var packedTestResponseES512 = map[string]string{
`success`: `{
"rawId": "6YIJExgLDzTvfys9WgQlIGTL1L9Ys9bhaaA1Pr-OAPc",
"id": "6YIJExgLDzTvfys9WgQlIGTL1L9Ys9bhaaA1Pr-OAPc",
"response": {
"clientDataJSON": "eyJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo0NDMyOSIsImNoYWxsZW5nZSI6IlFQQS1GckNTd2ctcUhoell2UklkbkEiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZzgjY3NpZ1iKMIGHAkE9Vr0j3zGzH6_YASuNse-D4bIDPU4ralNkJqgbCyv_tPNdt27VKaPDnK3WKWgv1qna04qMA7yukZeOPods8arRVQJCAZibACvAfmwBNT4cvR32MNvgGienLXmi2q8MwytcGrtOMnyhnxgco0pOFH7eWHXzn64mVqdSD-wPRTIfJ3McBxW0aGF1dGhEYXRhWOlJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0EAAABmI4irjYkVQUaTutQ-Zx0lOAAg6YIJExgLDzTvfys9WgQlIGTL1L9Ys9bhaaA1Pr-OAPelAQIDOCMgAyFYQgGzEwyupDz8u1IHtClxewg8CYWBRqD6_SufCj6-LevV57awHyeFGbyfS78ZB4e_I7RmndDI-jO24T3WZ1JMoE1mMCJYQgCpx32yAvYCfKWILgd5aLYuE5L8lEWuN5lhzGwNXoi6pj0JcQR60yCzI8HPlESzEvpqtCNBqF99eD2JETVIqkiwvQ"
},
"type": "public-key"
}`,
}
var packedTestResponseSolo2 = map[string]string{
`success`: `{
"id":"owBY6F5857tda9Pg5iFNCg6ksHpGOYhrNqIn46pkvhEMKIgNGcKS-vDGAUEroq0-VHnl1LhzQkPRQmYBTHjGcpLKZKSLa2m2ANI-91HjXzoJd_zFOiEnu7CDwQTff9KZ6uPlx7kUK-JJOHar-IyRKcNhc_kOJ2ezglmj1JYuIJLoDEyXlKkkviFdwk1vbWLnO3p_oWROUeIgH_S4CLVLPIJXkPe0YvMgp3ESs9CsrN6kvMTysVRIt_h5KUqpZo0TKCL96zwFk1X_2PwCLKWmOxVL35lJfUKOHG9rc3bmKlqZR6aOgZjerY6BpU8BTJkAqfOvdVlqFeEcywJQgveR7FOvnVtoqzd5oaEwjA",
"rawId":"owBY6F5857tda9Pg5iFNCg6ksHpGOYhrNqIn46pkvhEMKIgNGcKS-vDGAUEroq0-VHnl1LhzQkPRQmYBTHjGcpLKZKSLa2m2ANI-91HjXzoJd_zFOiEnu7CDwQTff9KZ6uPlx7kUK-JJOHar-IyRKcNhc_kOJ2ezglmj1JYuIJLoDEyXlKkkviFdwk1vbWLnO3p_oWROUeIgH_S4CLVLPIJXkPe0YvMgp3ESs9CsrN6kvMTysVRIt_h5KUqpZo0TKCL96zwFk1X_2PwCLKWmOxVL35lJfUKOHG9rc3bmKlqZR6aOgZjerY6BpU8BTJkAqfOvdVlqFeEcywJQgveR7FOvnVtoqzd5oaEwjA",
"response":{
"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAIXRMqmC2_bHTkKUwOvLvmAikuQPCk__9clILwjhOz3VAiEApJXTrN4WMiPwFXqTIh0oI8AZBm3vs-y_UotbQFSnX99jeDVjgVkCqzCCAqcwggJMoAMCAQICFGqj6W3EVhRWQJPun0qqCMyTlnqKMAoGCCqGSM49BAMCMC0xETAPBgNVBAoMCFNvbG9LZXlzMQswCQYDVQQGEwJDSDELMAkGA1UEAwwCRjEwIBcNMjEwNTIzMDA1MjA2WhgPMjA3MTA1MTEwMDUyMDZaMIGDMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU29sb0tleXMxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xPTA7BgNVBAMMNFNvbG8gMiBORkMrVVNCLUMgMjM2OUQ0RDAxM0NFNDhDQjlGMjZGN0VEOEM5QTYwNjggQjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS6N5V2fT-agh34bRiW--Wl6CQPSsnLqqSEID0t5RRKjjl1NDI__mzuyYuOrWyb5yzGZRHgnHq65cm2ROpxo6AOo4HwMIHtMB0GA1UdDgQWBBQ6CEDC5W8_zAMOhVgV8wHJI8n3bzAfBgNVHSMEGDAWgBRBa7ZL76IZDeRiX_0pBJa5gim0-DAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9pLnMycGtpLm5ldC9mMS8wJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL2MuczJwa2kubmV0L3IxLzAhBgsrBgEEAYLlHAEBBAQSBBAjadTQE85Iy58m9-2MmmBoMBMGCysGAQQBguUcAgEBBAQDAgQwMAoGCCqGSM49BAMCA0kAMEYCIQCP82Rolr0U2FvOJq53AZYcA6xfC4-cNDczvf0FtU1SQAIhAIvb21Z3D8RCvwk2-Ryn4wpsGnn2vma6Bw3E1f48hyVwaGF1dGhEYXRhWQFtarm78N-aFvkduzO7sTL6-dF8eCxIJsbscOzuWNl-9SpBAAAAJyNp1NATzkjLnyb37YyaYGgBDKMAWOhefOe7XWvT4OYhTQoOpLB6RjmIazaiJ-OqZL4RDCiIDRnCkvrwxgFBK6KtPlR55dS4c0JD0UJmAUx4xnKSymSki2tptgDSPvdR4186CXf8xTohJ7uwg8EE33_Smerj5ce5FCviSTh2q_iMkSnDYXP5Didns4JZo9SWLiCS6AxMl5SpJL4hXcJNb21i5zt6f6FkTlHiIB_0uAi1SzyCV5D3tGLzIKdxErPQrKzepLzE8rFUSLf4eSlKqWaNEygi_es8BZNV_9j8AiylpjsVS9-ZSX1Cjhxva3N25ipamUemjoGY3q2OgaVPAUyZAKnzr3VZahXhHMsCUIL3kexTr51baKs3eaGhMIykAQEDJyAGIVggjz9UkJ7cKooE3blSuzlqxkdLppMuFl3CIiST8odWS6k",
"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ1dieENUMEc0TDJ5T1JwQkw2U1dWaWd3ZTJrUUVYQmhvNUw2d0U0Ny1FcyIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4uZmlyc3R5ZWFyLmlkLmF1IiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
},
"type":"public-key"
}`,
}
@@ -0,0 +1,196 @@
package protocol
import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
"github.com/go-viper/mapstructure/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/go-webauthn/webauthn/metadata"
)
// attestationFormatValidationHandlerAndroidSafetyNet is the handler for the Android SafetyNet Attestation Statement
// Format.
//
// When the authenticator is a platform authenticator on certain Android platforms, the attestation statement may be
// based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of the
// SafetyNet API (typically an application running on the Android platform) and the attestation statement provides some
// statements about the health of the platform and the identity of the calling application (see SafetyNet Documentation
// for more details).
//
// The syntax of an Android Attestation statement is defined as follows:
//
// $$attStmtType //= (
// fmt: "android-safetynet",
// attStmt: safetynetStmtFormat
// )
//
// safetynetStmtFormat = {
// ver: text,
// response: bytes
// }
//
// Specification: §8.5. Android SafetyNet Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation
//
//nolint:gocyclo
func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
// The syntax of an Android Attestation statement is defined as follows:
// $$attStmtType //= (
// fmt: "android-safetynet",
// attStmt: safetynetStmtFormat
// )
// safetynetStmtFormat = {
// ver: text,
// response: bytes
// }
// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// We have done this
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
version, present := att.AttStatement[stmtVersion].(string)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
}
if version == "" {
return "", nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
}
// TODO: provide user the ability to designate their supported versions.
response, present := att.AttStatement["response"].([]byte)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
}
var token *jwt.Token
if token, err = jwt.Parse(string(response), keyFuncSafetyNetJWT, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// marshall the JWT payload into the safetynet response json.
var safetyNetResponse SafetyNetResponse
if err = mapstructure.Decode(token.Claims, &safetyNetResponse); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err)).WithError(err)
}
// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
// of authenticatorData and clientDataHash.
nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response").WithError(err)
}
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
certChain, ok := token.Header[stmtX5C].([]any)
if !ok || len(certChain) == 0 {
return "", nil, ErrInvalidAttestation.WithDetails("Error getting certificate from JWT header x5c")
}
first, ok := certChain[0].(string)
if !ok || first == "" {
return "", nil, ErrInvalidAttestation.WithDetails("Error getting first certificate from JWT header x5c")
}
l := make([]byte, base64.StdEncoding.DecodedLen(len(first)))
n, err := base64.StdEncoding.Decode(l, []byte(first))
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
attestationCert, err := x509.ParseCertificate(l[:n])
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com".
if err = attestationCert.VerifyHostname(attStatementAndroidSafetyNetHostname); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
if !safetyNetResponse.CtsProfileMatch {
return "", nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
}
if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(time.Now()) {
// Zero tolerance for post-dated timestamps.
return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
} else if t.Before(time.Now().Add(-time.Minute)) {
// Small tolerance for pre-dated timestamps.
if mds != nil && mds.GetValidateEntry(context.Background()) {
return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago")
}
}
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
// trust path attestationCert.
return string(metadata.BasicFull), nil, nil
}
func keyFuncSafetyNetJWT(token *jwt.Token) (key any, err error) {
var (
ok bool
raw any
chain []any
first string
der []byte
cert *x509.Certificate
)
if raw, ok = token.Header[stmtX5C]; !ok {
return nil, fmt.Errorf("jwt header missing x5c")
}
if chain, ok = raw.([]any); !ok || len(chain) == 0 {
return nil, fmt.Errorf("jwt header x5c is not a non-empty array")
}
if first, ok = chain[0].(string); !ok || first == "" {
return nil, fmt.Errorf("jwt header x5c[0] not a base64 string")
}
if der, err = base64.StdEncoding.DecodeString(first); err != nil {
return nil, fmt.Errorf("decode x5c leaf: %w", err)
}
if cert, err = x509.ParseCertificate(der); err != nil {
if cert != nil {
return cert.PublicKey, fmt.Errorf("parse x5c leaf: %w", err)
}
return nil, fmt.Errorf("parse x5c leaf: %w", err)
}
return cert.PublicKey, nil
}
type SafetyNetResponse struct {
Nonce string `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []any `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
func init() {
RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, attestationFormatValidationHandlerAndroidSafetyNet)
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,236 @@
package protocol
import (
"crypto/sha256"
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// WebAuthn Level 3 Specification Test Vectors
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors
//
// All test vectors use:
// - RP ID: example.org
// - Origin: https://example.org
// - Certificate validity: 2024-01-01 to 3024-01-01
// - Deterministic random via HKDF-SHA-256 from IKM "WebAuthn test vectors"
// - ECDSA signatures use deterministic nonces per RFC 6979
// §16.2 None Attestation - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256
func TestSpecVectors_NoneES256(t *testing.T) {
attObjHex := "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"
clientDataJSONHex := "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c453551227d"
att := specTestParseAndVerify(t, attObjHex, clientDataJSONHex, specCredParamsES256)
assert.Equal(t, stmtFmtNone, att.Format)
assert.True(t, att.AuthData.Flags.HasUserPresent())
assert.True(t, att.AuthData.Flags.HasAttestedCredentialData())
assert.Empty(t, att.AttStatement)
credID := specTestDecodeHex(t, "f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
}
// §16.3 Self Attestation (Packed) - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-self-es256
func TestSpecVectors_PackedSelfES256(t *testing.T) {
attObjHex := "a363666d74667061636b65646761747453746d74a263616c672663736967584630440220067a20754ab925005dbf378097c92120031581c73228d1fb4f5b881bcd7da98302207fc7b147558c7c0eba3af18bd9d121fa3d3a26d17fe3f220272178f473b6006d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2"
clientDataJSONHex := "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f307859485667227d"
att := specTestParseAndVerify(t, attObjHex, clientDataJSONHex, specCredParamsES256)
assert.Equal(t, "packed", att.Format)
assert.True(t, att.AuthData.Flags.HasUserPresent())
assert.True(t, att.AuthData.Flags.HasUserVerified())
assert.True(t, att.AuthData.Flags.HasAttestedCredentialData())
credID := specTestDecodeHex(t, "455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
rawClientDataJSON := specTestDecodeHex(t, clientDataJSONHex)
clientDataHash := sha256.Sum256(rawClientDataJSON)
attestationType, _, err := attestationFormatValidationHandlerPacked(att, clientDataHash[:], nil)
require.NoError(t, err)
assert.Equal(t, string(metadata.BasicSurrogate), attestationType)
}
// §16.7 Packed Attestation - ES256 (Full Attestation with x5c)
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-es256
func TestSpecVectors_PackedES256(t *testing.T) {
attObjHex := "a363666d74667061636b65646761747453746d74a363616c6726637369675847304502203f19ec4b229f46ab8c45eff29b904ff10c0390dc40bf1216f04a78f4ceba3425022100fe7041a32759aff05a0f9f26c70a999c7a284451ba89234a1d3483c25e21925b637835638159022530820221308201c8a00302010202110088c220f83c8ef1feafe94deae45faad0300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004a91ba4389409dd38a428141940ca8feb1ac0d7b4350558104a3777a49322f3798440f378b3398ab2d3bb7bf91322c92eb23556f59ad0a836fec4c7663b0e4dc3a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414a589ba72d060842ab11f74fb246bdedab16f9b9b301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d040302034700304402201726b9d85ecd8a5ed51163722ca3a20886fd9b242a0aa0453d442116075defd502207ef471e530ac87961a88a7f0d0c17b091ffc6b9238d30f79f635b417be5910e768617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d00000000876ca4f52071c3e9b25509ef2cdf7ed60020c9a6f5b3462d02873fea0c56862234f99f081728084e511bb7760201a89054a5a50102032620012158201cf27f25da591208a4239c2e324f104f585525479a29edeedd830f48e77aeae522582059e4b7da6c0106e206ce390c93ab98a15a5ec3887e57f0cc2bece803b920c423"
clientDataJSONHex := "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a227752684b58393334424634543345663153324831706c61325a725751475046746877365356756d56494249222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20396138624e596a4b436757724258552d66436c316167227d"
att := specTestParseAndVerify(t, attObjHex, clientDataJSONHex, specCredParamsES256)
assert.Equal(t, "packed", att.Format)
credID := specTestDecodeHex(t, "c9a6f5b3462d02873fea0c56862234f99f081728084e511bb7760201a89054a5")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
rawClientDataJSON := specTestDecodeHex(t, clientDataJSONHex)
clientDataHash := sha256.Sum256(rawClientDataJSON)
attestationType, x5cs, err := attestationFormatValidationHandlerPacked(att, clientDataHash[:], nil)
require.NoError(t, err)
assert.Equal(t, string(metadata.BasicFull), attestationType)
assert.NotEmpty(t, x5cs)
}
// §16.13 TPM Attestation - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-tpm-es256
//
// The spec test vectors use synthetic manufacturer ID "00000000" which is not in the real TPM
// manufacturer registry. This test validates CBOR parsing, authData, and rpIdHash but the
// format-specific handler rejects the synthetic manufacturer.
func TestSpecVectors_TPMES256(t *testing.T) {
attObjHex := "a363666d746374706d6761747453746d74a663616c67266373696758463044022066e5826a652091030fd444e33c3eca2bc6dc548cf3045013addb38aa6457a21002203f3a5c95c9e707d0e555041bcc8698ee4ebc04e26cc8bae459705471789851766376657263322e30637835638159023a30820236308201dca0030201020210311fc42da0ab10c43a9b1bf3a75e34e2300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004c54e3f109094f60d7699b7db5d838569ffd1f3e1c9e897cd9eb40063f9402e3e9937e936cf1fcd5eb743ff443c97ab2edcd7c8e0e6cf6cfd413b8ab19fffa769a381d33081d0300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604145f546cb6973d4981e80fcdc7463859f5879680e4301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e30100603551d250409300706056781050803305e0603551d110101ff04543052a450304e314c3014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a3030303030303030301e060567810502020c15576562417574686e207465737420766563746f7273300a06082a8648ce3d0403020348003045022063c9a2797b8066f1db34dd609f1ab6695607e7a98e9ff8090a68853c9a9fc949022100a55831a39f5b8a2aa9a68837829cabf43fea2a5cea4859ae851cac78e6ac3e97677075624172656158560023000b0004000000000010001000030010002041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b0020d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d076863657274496e666f5869ff544347801700000020277d0e05579dd013215a62273f7f3a3e7e191ead2654a3036d75a5a3ee37a6b0000000000000000011111111222222223300000000000000000022000b9c42d8aad5939331b9af3711af179f17123178098c9a7d0ca89fcd1fc800f3c7000068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d000000004b92a377fc5f6107c4c85c190adbfd990020ec27bec7521c894bbb821105ea3724c90e770cf1fa354157ef18d0f18f78bea9a501020326200121582041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b225820d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d07"
clientDataJSONHex := "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a227a38677333787a753648595343716950413254776b51475452677a376c364d587376344a427054356f706b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"
att := specTestParseAttestationObject(t, attObjHex)
assert.Equal(t, "tpm", att.Format)
credID := specTestDecodeHex(t, "ec27bec7521c894bbb821105ea3724c90e770cf1fa354157ef18d0f18f78bea9")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
clientDataHash := sha256.Sum256(specTestDecodeHex(t, clientDataJSONHex))
_, _, err := attestationFormatValidationHandlerTPM(att, clientDataHash[:], nil)
assert.NoError(t, err)
}
// §16.14 Android Key Attestation - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-android-key-es256
//
// The spec test vectors use a synthetic certificate whose Android keystore extension encodes the
// AttestationSecurityLevel and KeymasterSecurityLevel fields as ASN.1 INTEGER instead of ENUMERATED.
// Real Android keystore certificates use ENUMERATED, so the full format handler cannot parse the
// synthetic extension. This test validates CBOR parsing, authData, and rpIdHash only.
func TestSpecVectors_AndroidKeyES256(t *testing.T) {
attObjHex := "a363666d746b616e64726f69642d6b65796761747453746d74a363616c67266373696758483046022100e95512982aa3f216cff2e87c8ec57057b8529f674eaabeccaa27fd03d8779f19022100afb6bf459da4a826f00d01fc6b60712ff31dc4eb331619c8f874bb17e4314e94637835638159026f3082026b30820210a00302010202101ff91f76b63f44812f998b250b0286bf300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000499169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accfdd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6ba381a83081a5300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604141ac81e50641e8d1339ab9f7eb25f0cd5aac054b0301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e3045060a2b06010401d679020111043730350202012c0201000201000201000420b435028d7b6a8f83bb461d41c19b053a9d3cdb30351a4f374cd4cde8dbefb606040030003000300a06082a8648ce3d040302034900304602210081671f2474f336e6b5a868d28b47cd054c0ed4261f531fcdf1a1ceed19f600ad022100e7ac683848c34842a432ff4a26e9dbc537b88e83fc4cb59138de3ca3a3e1081468617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000ade9705e1ce7085b899a540d02199bf800200a4729519788b6ed8a2d772b494e186244d8c798c052960dbc8c10c915176795a501020326200121582099169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accf225820dd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6b"
att := specTestParseAttestationObject(t, attObjHex)
assert.Equal(t, "android-key", att.Format)
credID := specTestDecodeHex(t, "0a4729519788b6ed8a2d772b494e186244d8c798c052960dbc8c10c915176795")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
rpIDHash := sha256.Sum256([]byte(specTestRPID))
assert.Equal(t, rpIDHash[:], att.AuthData.RPIDHash)
}
// §16.15 Apple Anonymous Attestation - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-apple-es256
//
// The spec test vectors use a synthetic CA not in the hardcoded Apple hardware root pool.
// This test validates CBOR parsing, authData, and rpIdHash.
func TestSpecVectors_AppleES256(t *testing.T) {
attObjHex := "a363666d74656170706c656761747453746d74a1637835638159025c30820258308201fea0030201020210394275613d5310b81a29ce90f48b61c1300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d030107034200048a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761af728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136ca38196308193300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e0416041412f1ce6c0ae39b403bfc9200317bc183a4e4d766301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e303306092a864886f76364080204263024a1220420d7a86e7233fb843eb0eeb407d8b76ff7e4f82d218cf5dbb461d752073f5cb29a300a06082a8648ce3d0403020348003045022070f5c2ede3000e9dae358d412b26a4acbf18f4cdeb80f5b13fcd564d090c39ec022100f672e2c3dbe117c9b1490b3c660abf5dcd74398187082dacb58b6744de4aca6068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54900000000748210a20076616a733b2114336fc38400209c4a5886af9283d9be3e9ec55978dedfdce2e3b365cab193ae850c16238fafb8a50102032620012158208a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761a225820f728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136c"
clientDataJSONHex := "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22395f61494954685341486431414a7a34774a6239714a316775616e37576c4464676432596d4b396142676b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20546a4c506e704f6158515572464e6362483274545a41227d"
att := specTestParseAttestationObject(t, attObjHex)
assert.Equal(t, "apple", att.Format)
credID := specTestDecodeHex(t, "9c4a5886af9283d9be3e9ec55978dedfdce2e3b365cab193ae850c16238fafb8")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
clientDataHash := sha256.Sum256(specTestDecodeHex(t, clientDataJSONHex))
_, _, err := attestationFormatValidationHandlerAppleAnonymous(att, clientDataHash[:], nil)
assert.NoError(t, err)
}
// §16.16 FIDO U2F Attestation - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-fido-u2f-es256
//
// The spec test vector uses a non-zero AAGUID (afb3c2ef...) which is correctly rejected by
// the FIDO U2F handler per §8.6 which requires AAGUID to be all zeros. This test validates
// CBOR parsing, authData, rpIdHash, and credential algorithm matching.
func TestSpecVectors_FIDOU2FES256(t *testing.T) {
attObjHex := "a363666d74686669646f2d7532666761747453746d74a26373696758473045022100f41887a20063bb26867cb9751978accea5b81791a68f4f4dd6ea1fb6a5c086c302204e5e00aa3895777e6608f1f375f95450045da3da57a0e4fd451df35a31d2d98a637835638159022530820221308201c7a003020102021004f66dc6542ea7719dea416d325a2401300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000456fffa7093dede46aefeefb6e520c7ccc78967636e2f92582ba71455f64e93932dff3be4e0d4ef68e3e3b73aa087e26a0a0a30b02dc2aa2309db4c3a2fc936dea360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414420822eb1908b5cd3911017fbcad4641c05e05a3301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d040302034800304502200d0b777f0a0b181ad2830275acc3150fd6092430bcd034fd77beb7bdf8c2d546022100d4864edd95daa3927080855df199f1717299b24a5eecefbd017455a9b934d8f668617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54100000000afb3c2efc054df425013d5c88e79c3c10020a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4a5010203262001215820b0d62de6b30f86f0bac7a9016951391c2e31849e2e64661cbd2b13cd7d5508ad225820503b0bda2a357a9a4b34475a28e65b660b4898a9e3e9bbf0820d43494297edd0"
clientDataJSONHex := "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22344851334b5a4335797155486f696666786e73414e344445557955344452715177672d4237583049444159222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"
att := specTestParseAttestationObject(t, attObjHex)
assert.Equal(t, "fido-u2f", att.Format)
credID := specTestDecodeHex(t, "a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4")
assert.Equal(t, credID, att.AuthData.AttData.CredentialID)
rpIDHash := sha256.Sum256([]byte(specTestRPID))
require.NoError(t, att.AuthData.Verify(rpIDHash[:], nil, false, true))
var pk webauthncose.PublicKeyData
require.NoError(t, webauthncbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &pk))
assert.Equal(t, int64(webauthncose.AlgES256), pk.Algorithm)
clientDataHash := sha256.Sum256(specTestDecodeHex(t, clientDataJSONHex))
_, _, err := attestationFormatValidationHandlerFIDOU2F(att, clientDataHash[:], nil)
assert.NoError(t, err)
}
// Supporting constants, variables, and functions.
const specTestRPID = "example.org"
var specCredParamsES256 = []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}}
func specTestDecodeHex(t *testing.T, s string) []byte {
t.Helper()
data, err := hex.DecodeString(s)
require.NoError(t, err)
return data
}
func specTestParseAndVerify(t *testing.T, attObjHex, clientDataJSONHex string, credParams []CredentialParameter) AttestationObject {
t.Helper()
rawAttObj := specTestDecodeHex(t, attObjHex)
rawClientDataJSON := specTestDecodeHex(t, clientDataJSONHex)
var att AttestationObject
require.NoError(t, webauthncbor.Unmarshal(rawAttObj, &att))
require.NoError(t, att.AuthData.Unmarshal(att.RawAuthData))
rpIDHash := sha256.Sum256([]byte(specTestRPID))
assert.Equal(t, rpIDHash[:], att.AuthData.RPIDHash)
clientDataHash := sha256.Sum256(rawClientDataJSON)
require.NoError(t, att.Verify(specTestRPID, clientDataHash[:], false, true, nil, credParams))
return att
}
func specTestParseAttestationObject(t *testing.T, attObjHex string) AttestationObject {
t.Helper()
rawAttObj := specTestDecodeHex(t, attObjHex)
var att AttestationObject
require.NoError(t, webauthncbor.Unmarshal(rawAttObj, &att))
require.NoError(t, att.AuthData.Unmarshal(att.RawAuthData))
rpIDHash := sha256.Sum256([]byte(specTestRPID))
assert.Equal(t, rpIDHash[:], att.AuthData.RPIDHash)
return att
}
+403
View File
@@ -0,0 +1,403 @@
package protocol
import (
"crypto/sha256"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/metadata"
)
func TestAttestationVerify(t *testing.T) {
testCases := []struct {
name string
options string
response string
}{
{
name: "ShouldVerifySelfAttestationEC256MacOS",
options: testAttestationOptions[0],
response: testAttestationResponses[0],
},
{
name: "ShouldVerifyDirectAttestationEC256Titan",
options: testAttestationOptions[1],
response: testAttestationResponses[1],
},
{
name: "ShouldVerifyNoneAttestationEC256Titan",
options: testAttestationOptions[2],
response: testAttestationResponses[2],
},
{
name: "ShouldVerifyPackedAttestationGramThanos",
options: testAttestationOptions[3],
response: testAttestationResponses[3],
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
options := CredentialCreation{}
require.NoError(t, json.Unmarshal([]byte(tc.options), &options))
ccr := CredentialCreationResponse{}
require.NoError(t, json.Unmarshal([]byte(tc.response), &ccr))
var pcc ParsedCredentialCreationData
pcc.ID, pcc.RawID, pcc.Type, pcc.ClientExtensionResults = ccr.ID, ccr.RawID, ccr.Type, ccr.ClientExtensionResults
pcc.Raw = ccr
parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
require.NoError(t, err)
pcc.Response = *parsedAttestationResponse
_, err = pcc.Verify(options.Response.Challenge.String(), options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginExplicitVerificationMode, false, false, false, nil, options.Response.Parameters)
require.NoError(t, err)
})
}
}
func TestPackedAttestationVerification(t *testing.T) {
pcc := attestationTestUnpackResponse(t, testAttestationResponses[0])
clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
_, _, err := attestationFormatValidationHandlerPacked(pcc.Response.AttestationObject, clientDataHash[:], nil)
require.NoError(t, err)
}
func TestAttestationResponseParse_Errors(t *testing.T) {
testCases := []struct {
name string
response AuthenticatorAttestationResponse
err string
}{
{
name: "ShouldFailInvalidClientDataJSON",
response: AuthenticatorAttestationResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: []byte("not-json"),
},
AttestationObject: []byte{0xa0},
},
err: "Error parsing the authenticator response",
},
{
name: "ShouldFailInvalidAttestationObjectCBOR",
response: AuthenticatorAttestationResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: []byte(`{"type":"webauthn.create","challenge":"dGVzdA","origin":"https://example.com"}`),
},
AttestationObject: []byte("not-cbor"),
},
err: "Error parsing the authenticator response",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.response.Parse()
require.EqualError(t, err, tc.err)
})
}
}
func TestAttestationObject_VerifyAttestation_Errors(t *testing.T) {
testCases := []struct {
name string
att AttestationObject
err string
}{
{
name: "ShouldFailNoneFormatWithStatement",
att: AttestationObject{
Format: "none",
AttStatement: map[string]any{"key": "value"},
},
err: "Invalid attestation format",
},
{
name: "ShouldFailUnsupportedFormat",
att: AttestationObject{
Format: "unsupported-format",
AttStatement: map[string]any{},
},
err: "Invalid attestation format",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.att.VerifyAttestation([]byte("hash"), nil)
require.EqualError(t, err, tc.err)
})
}
}
func TestAttestationObject_Verify_AlgorithmMismatch(t *testing.T) {
pcc := attestationTestUnpackResponse(t, testAttestationResponses[0])
att := pcc.Response.AttestationObject
clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
wrongParams := []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: -257}}
err := att.Verify("localhost", clientDataHash[:], false, false, nil, wrongParams)
require.EqualError(t, err, "Invalid attestation format")
}
func TestAttestationObject_VerifyAttestation_HandlerErrors(t *testing.T) {
withFreshAttestationRegistry(t)
testCases := []struct {
name string
format string
handler attestationFormatValidationHandler
authData AuthenticatorData
err string
errType string
errDetails string
}{
{
name: "ShouldWrapProtocolError",
format: "test-format",
handler: func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return string(metadata.BasicFull), nil, ErrInvalidAttestation.WithDetails("handler failed")
},
err: "handler failed",
errType: ErrInvalidAttestation.Type,
},
{
name: "ShouldWrapNonProtocolError",
format: "test-format",
handler: func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return string(metadata.BasicFull), nil, fmt.Errorf("stdlib error")
},
err: "stdlib error",
errType: ErrInvalidAttestation.Type,
},
{
name: "ShouldReturnNilForCompoundAttestationType",
format: "test-format",
handler: func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return string(AttestationFormatCompound), nil, nil
},
},
{
name: "ShouldFailWithInvalidAAGUIDLength",
format: "test-format",
handler: func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return string(metadata.BasicFull), nil, nil
},
authData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: []byte{0x01, 0x02, 0x03},
},
},
err: "invalid UUID (got 3 bytes)",
errType: ErrInvalidAttestation.Type,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
attestationRegistry[AttestationFormat(tc.format)] = tc.handler
att := AttestationObject{
Format: tc.format,
AttStatement: map[string]any{},
AuthData: tc.authData,
}
err := att.VerifyAttestation([]byte("hash"), nil)
if tc.err != "" {
require.Error(t, err)
assert.EqualError(t, err, tc.err)
if tc.errType != "" {
var protoErr *Error
require.ErrorAs(t, err, &protoErr)
assert.Equal(t, tc.errType, protoErr.Type)
}
} else {
assert.NoError(t, err)
}
})
}
}
// Supporting functions.
func attestationTestUnpackResponse(t *testing.T, response string) (pcc ParsedCredentialCreationData) {
t.Helper()
ccr := CredentialCreationResponse{}
require.NoError(t, json.Unmarshal([]byte(response), &ccr))
pcc.ID, pcc.RawID, pcc.Type, pcc.ClientExtensionResults = ccr.ID, ccr.RawID, ccr.Type, ccr.ClientExtensionResults
pcc.Raw = ccr
parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
require.NoError(t, err)
pcc.Response = *parsedAttestationResponse
return pcc
}
// Test data.
var testAttestationOptions = []string{
// Direct Self Attestation with EC256 - MacOS.
`{"publicKey": {
"challenge": "rWiex8xDOPfiCgyFu4BLW6vVOmXKgPwHrlMCgEs9SBA",
"rp": {
"name": "http://localhost:9005",
"id": "localhost"
},
"user": {
"name": "self",
"displayName": "self",
"id": "2iEAAAAAAAAAAA=="
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"authenticatorSelection": {
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
},
"timeout": 60000,
"attestation": "direct"
}}`,
// Direct Attestation with EC256.
`{"publicKey": {
"challenge": "-Ri5NZTzJ8b6mvW3TVScLotEoALfgBa2Bn4YSaIObHc",
"rp": {
"name": "https://webauthn.io",
"id": "webauthn.io"
},
"user": {
"name": "flort",
"displayName": "flort",
"id": "1DMAAAAAAAAAAA=="
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"authenticatorSelection": {
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
},
"timeout": 60000,
"attestation": "direct"
}}`,
// None Attestation with EC256.
`{
"publicKey": {
"challenge": "sVt4ScceMzqFSnfAq8hgLzblvo3fa4_aFVEcIESHIJ0",
"rp": {
"name": "https://webauthn.io",
"id": "webauthn.io"
},
"user": {
"name": "testuser1",
"displayName": "testuser1",
"id": "1zMAAAAAAAAAAA=="
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"authenticatorSelection": {
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
},
"timeout": 60000,
"attestation": "none"
}
}`,
`{
"publicKey": {
"rp": {
"name": "https://gramthanos.github.io",
"id": "gramthanos.github.io"
},
"user": {
"name": "john.smith@email.com",
"displayName": "J. Smith",
"id": "am9obi5zbWl0aEBlbWFpbC5jb20="
},
"challenge": "Dw4NDAsKCQgHBgUEAwIBAA==",
"pubKeyCredParams": [
{"type": "public-key", "alg": -7},
{"type": "public-key", "alg": -37},
{"type": "public-key", "alg": -257},
{"type": "public-key", "alg": -8}
],
"timeout": 120000,
"attestation": "direct"
}
}`,
}
var testAttestationResponses = []string{
// Self Attestation with EC256 - MacOS.
`{
"id": "AOx6vFGGITtlwjhqFFvAkJmBzSzfwE1dBa1fVR_Ltq5L35FJRNdgkXe84v3-0TEVNCSp",
"rawId": "AOx6vFGGITtlwjhqFFvAkJmBzSzfwE1dBa1fVR_Ltq5L35FJRNdgkXe84v3-0TEVNCSp",
"response": {
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIhAJgdgw5x8JzE4JfR6x1RBO8eCHNE8eW_L1VTV03zpyL5AiBv8eUzua3XSS3bPYC7m8eXzJhcaRyeGe7UcuqIrDSvC2hhdXRoRGF0YVi3SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFXJE5zK3OAAI1vMYKZIsLJfHwVQMAMwDserxRhiE7ZcI4ahRbwJCZgc0s38BNXQWtX1Ufy7auS9-RSUTXYJF3vOL9_tExFTQkqaUBAgMmIAEhWCCm9OYidwiIoH9SwVQqUAnH8Gj5ZJ2_qr8gjbg41q4M1SJYIA07XKpHSgS1mE7R1MjotVIQqyHi9WAxGwHQsCteVK2V",
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJyV2lleDh4RE9QZmlDZ3lGdTRCTFc2dlZPbVhLZ1B3SHJsTUNnRXM5U0JBIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo5MDA1IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9"
},
"type": "public-key"
}`,
// Direct Attestation with EC256 - Titan.
`{
"id": "FOxcmsqPLNCHtyILvbNkrtHMdKAeqSJXYZDbeFd0kc5Enm8Kl6a0Jp0szgLilDw1S4CjZhe9Z2611EUGbjyEmg",
"rawId": "FOxcmsqPLNCHtyILvbNkrtHMdKAeqSJXYZDbeFd0kc5Enm8Kl6a0Jp0szgLilDw1S4CjZhe9Z2611EUGbjyEmg",
"response": {
"attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEYwRAIgfyIhwZj-fkEVyT1GOK8chDHJR2chXBLSRg6bTCjODmwCIHH6GXI_BQrcR-GHg5JfazKVQdezp6_QWIFfT4ltTCO2Y3g1Y4FZAlMwggJPMIIBN6ADAgECAgQSNtF_MA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjAxMS8wLQYDVQQDDCZZdWJpY28gVTJGIEVFIFNlcmlhbCAyMzkyNTczNDEwMzI0MTA4NzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNNlqR5emeDVtDnA2a-7h_QFjkfdErFE7bFNKzP401wVE-QNefD5maviNnGVk4HJ3CsHhYuCrGNHYgTM9zTWriGjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMA0GCSqGSIb3DQEBCwUAA4IBAQAiG5uzsnIk8T6-oyLwNR6vRklmo29yaYV8jiP55QW1UnXdTkEiPn8mEQkUac-Sn6UmPmzHdoGySG2q9B-xz6voVQjxP2dQ9sgbKd5gG15yCLv6ZHblZKkdfWSrUkrQTrtaziGLFSbxcfh83vUjmOhDLFC5vxV4GXq2674yq9F2kzg4nCS4yXrO4_G8YWR2yvQvE2ffKSjQJlXGO5080Ktptplv5XN4i5lS-AKrT5QRVbEJ3B4g7G0lQhdYV-6r4ZtHil8mF4YNMZ0-RaYPxAaYNWkFYdzOZCaIdQbXRZefgGfbMUiAC2gwWN7fiPHV9eu82NYypGU32OijG9BjhGt_aGF1dGhEYXRhWMR0puqSE8mcL3SyJJKzIM9AJiqUwalQoDl_KSULYIQe8EEAAAAAAAAAAAAAAAAAAAAAAAAAAABAFOxcmsqPLNCHtyILvbNkrtHMdKAeqSJXYZDbeFd0kc5Enm8Kl6a0Jp0szgLilDw1S4CjZhe9Z2611EUGbjyEmqUBAgMmIAEhWCD_ap3Q9zU8OsGe967t48vyRxqn8NfFTk307mC1WsH2ISJYIIcqAuW3MxhU0uDtaSX8-Ftf_zeNJLdCOEjZJGHsrLxH",
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiItUmk1TlpUeko4YjZtdlczVFZTY0xvdEVvQUxmZ0JhMkJuNFlTYUlPYkhjIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
},
"type": "public-key"
}`,
// None Attestation with EC256 - Titan.
`{
"id": "6Jry73M_WVWDoXLsGxRsBVVHpPWDpNy1ETGXUEvJLdTAn5Ew6nDGU6W8iO3ZkcLEqr-CBwvx0p2WAxzt8RiwQQ",
"rawId": "6Jry73M_WVWDoXLsGxRsBVVHpPWDpNy1ETGXUEvJLdTAn5Ew6nDGU6W8iO3ZkcLEqr-CBwvx0p2WAxzt8RiwQQ",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOia8u9zP1lVg6Fy7BsUbAVVR6T1g6TctRExl1BLyS3UwJ-RMOpwxlOlvIjt2ZHCxKq_ggcL8dKdlgMc7fEYsEGlAQIDJiABIVgg--n_QvZithDycYmnifk6vMHiwBP6kugn2PlsnvkrcSgiWCBAlBYm2B-rMtQlp5MxGTLoGDHoktxb0p364Hy2BH9U2Q",
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJzVnQ0U2NjZU16cUZTbmZBcThoZ0x6Ymx2bzNmYTRfYUZWRWNJRVNISUowIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
},
"type": "public-key"
}`, `{
"type": "public-key",
"id": "GramThanos8pyTMpdk0qJLv3eLhUP3EXIXjD-uyqD0gab1pdvGy1ig77ZLl_ZU_vnd2296FoIZ67pZqTChpSJPq_oqUhjmr5Osv_LLiY7YGsAafMUdIb_LKOdwc6sfXyy_Ygl3_w-vl3tU9EPGyzgtI7hTBeMXnSIaOV6CUUf6d9op4JyxEDJr-roWxRMJPfnVAMLvv4lF_Cpd6Of0o75nDcCtEsTiynINihIwee1gmg0BAVKh3seWoNqXMpiXgPWc9Jt8ibjN9O-bsag3tELVs9uOoe-NZEmwbph0jJh_Y6e2H5Nwkp7WghST0P6krTL_sUlbpmDolhfFut0YljLrOrz_llW-WHySwvaAG2vzgvxA",
"rawId": "GramThanos8pyTMpdk0qJLv3eLhUP3EXIXjD-uyqD0gab1pdvGy1ig77ZLl_ZU_vnd2296FoIZ67pZqTChpSJPq_oqUhjmr5Osv_LLiY7YGsAafMUdIb_LKOdwc6sfXyy_Ygl3_w-vl3tU9EPGyzgtI7hTBeMXnSIaOV6CUUf6d9op4JyxEDJr-roWxRMJPfnVAMLvv4lF_Cpd6Of0o75nDcCtEsTiynINihIwee1gmg0BAVKh3seWoNqXMpiXgPWc9Jt8ibjN9O-bsag3tELVs9uOoe-NZEmwbph0jJh_Y6e2H5Nwkp7WghST0P6krTL_sUlbpmDolhfFut0YljLrOrz_llW-WHySwvaAG2vzgvxA",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRHc0TkRBc0tDUWdIQmdVRUF3SUJBQSIsIm9yaWdpbiI6Imh0dHBzOi8vZ3JhbXRoYW5vcy5naXRodWIuaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2UsInZpcnR1YWxfYXV0aGVudGljYXRvciI6IkdyYW1UaGFub3MgJiBVbml2ZXJzaXR5IG9mIFBpcmFldXMifQ",
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgaTjQj-hC9GH1fCbOT_8m4wdVJBZMG0252iBEwIGKWkUCIApZyPGh_ihn57GRKN-qTVCwgBqe4V40LL-r9_Y2pRXiY3g1Y4FZAgUwggIBMIIBpqADAgECAgVixtGpsjAKBggqhkjOPQQDAjBQMQswCQYDVQQGEwJHUjESMBAGA1UECgwJVU5JUEkgU1NMMS0wKwYDVQQDEyRVTklQSSBGSURPMiBWaXJ0dWFsIEF1dGhlbnRpY2F0b3IgQ0EwIhgPMjAyMDEyMzEyMjAwMDBaGA8yMTIwMTIzMTIyMDAwMFowcTELMAkGA1UEBhMCR1IxEjAQBgNVBAoMCVVOSVBJIFNTTDEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEqMCgGA1UEAwwhVU5JUEkgRklETzIgVmlydHVhbCBBdXRoZW50aWNhdG9yMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE_l8G-E0tTiXogmgXZZ0nRUMc7NO5-sowWP0lZhX8GZbU_n2TPO1J-39UbRABHUK_2J-ZbzcDAu2oy_nazsz4CqNIMEYwIQYLKwYBBAGC5RwBAQQEEgQQCJhwWMrcS4G24TDeUNy-ljATBgsrBgEEAYLlHAIBAQQEAwIFIDAMBgNVHRMBAf8EAjAAMAoGCCqGSM49BAMCA0kAMEYCIQDsyXh97GlMAcRq8khd4U-26d1E92a0lupZUGNBlki_MQIhAJFqO_qmBakyeD1esP4v3gIWsYKmHpiwJ64UKlid5NobaGF1dGhEYXRhWQGWou-FTChrR7AO-C0KXtsaxN1QIX4DOq_aCmYeKeUXnlZFAAAAAQiYcFjK3EuBtuEw3lDcvpYBEhq2pk4Wp6LPKckzKXZNKiS793i4VD9xFyF4w_rsqg9IGm9aXbxstYoO-2S5f2VP753dtvehaCGeu6WakwoaUiT6v6KlIY5q-TrL_yy4mO2BrAGnzFHSG_yyjncHOrH18sv2IJd_8Pr5d7VPRDxss4LSO4UwXjF50iGjleglFH-nfaKeCcsRAya_q6FsUTCT351QDC77-JRfwqXejn9KO-Zw3ArRLE4spyDYoSMHntYJoNAQFSod7HlqDalzKYl4D1nPSbfIm4zfTvm7GoN7RC1bPbjqHvjWRJsG6YdIyYf2Onth-TcJKe1oIUk9D-pK0y_7FJW6Zg6JYXxbrdGJYy6zq8_5ZVvlh8ksL2gBtr84L8SlAQIDJiABIVgg_l8G-E0tTiXogmgXZZ0nRUMc7NO5-sowWP0lZhX8GZYiWCDU_n2TPO1J-39UbRABHUK_2J-ZbzcDAu2oy_nazsz4Cg"
}
}`,
}
+635
View File
@@ -0,0 +1,635 @@
package protocol
import (
"bytes"
"crypto"
"crypto/subtle"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"errors"
"fmt"
"strings"
"github.com/google/go-tpm/tpm2"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// attestationFormatValidationHandlerTPM is the handler for the TPM Attestation Statement Format.
//
// The syntax of a TPM Attestation statement is as follows:
//
// $$attStmtType // = (
//
// fmt: "tpm",
// attStmt: tpmStmtFormat
// )
//
// tpmStmtFormat = {
// ver: "2.0",
// (
// alg: COSEAlgorithmIdentifier,
// x5c: [ aikCert: bytes, * (caCert: bytes) ]
// )
// sig: bytes,
// certInfo: bytes,
// pubArea: bytes
// }
//
// Specification: §8.3. TPM Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-tpm-attestation
//
//nolint:gocyclo
func attestationFormatValidationHandlerTPM(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
var statement *tpm2AttStatement
if statement, err = newTPM2AttStatement(att.AttStatement); err != nil {
return "", nil, err
}
if statement.HasECDAAKeyID || statement.HasValidECDAAKeyID {
return "", nil, ErrNotImplemented
}
if !statement.HasX5C || !statement.HasValidX5C {
return "", nil, ErrNotImplemented
}
if statement.Version != versionTPM20 {
return "", nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
}
var (
pubArea *tpm2.TPMTPublic
key any
)
if pubArea, err = tpm2.Unmarshal[tpm2.TPMTPublic](statement.PubArea); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement").WithError(err)
}
if key, err = webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey); err != nil {
return "", nil, err
}
switch k := key.(type) {
case webauthncose.EC2PublicKeyData:
var (
params *tpm2.TPMSECCParms
point *tpm2.TPMSECCPoint
)
if params, err = pubArea.Parameters.ECCDetail(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
if point, err = pubArea.Unique.ECC(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
if params.CurveID != k.TPMCurveID() {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
if !bytes.Equal(point.X.Buffer, k.XCoord) || !bytes.Equal(point.Y.Buffer, k.YCoord) {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
case webauthncose.RSAPublicKeyData:
var (
params *tpm2.TPMSRSAParms
modulus *tpm2.TPM2BPublicKeyRSA
)
if params, err = pubArea.Parameters.RSADetail(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
if modulus, err = pubArea.Unique.RSA(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
if !bytes.Equal(modulus.Buffer, k.Modulus) {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
exp := uint32(k.Exponent[0]) + uint32(k.Exponent[1])<<8 + uint32(k.Exponent[2])<<16
if tpm2Exponent(params) != exp {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
default:
return "", nil, ErrUnsupportedKey
}
// Concatenate authenticatorData and clientDataHash to form attToBeSigned.
attToBeSigned := append(att.RawAuthData, clientDataHash...) //nolint:gocritic // This is intentional.
var certInfo *tpm2.TPMSAttest
// Validate that certInfo is valid:
// 1/4 Verify that magic is set to TPM_GENERATED_VALUE, handled here.
if certInfo, err = tpm2.Unmarshal[tpm2.TPMSAttest](statement.CertInfo); err != nil {
return "", nil, err
}
if err = certInfo.Magic.Check(); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Magic is not set to TPM_GENERATED_VALUE")
}
// 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
if certInfo.Type != tpm2.TPMSTAttestCertify {
return "", nil, ErrAttestationFormat.WithDetails("Type is not set to TPM_ST_ATTEST_CERTIFY")
}
// 3/4 Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
coseAlg := webauthncose.COSEAlgorithmIdentifier(statement.Algorithm)
h := webauthncose.HasherFromCOSEAlg(coseAlg)
h.Write(attToBeSigned)
if !bytes.Equal(certInfo.ExtraData.Buffer, h.Sum(nil)) {
return "", nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
}
// Note that the remaining fields in the "Standard Attestation Structure"
// [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion
// are ignored. These fields MAY be used as an input to risk engines.
var (
aikCert *x509.Certificate
raw []byte
ok bool
)
if len(statement.X5C) == 0 {
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
// In this case:
// Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
if raw, ok = statement.X5C[0].([]byte); !ok {
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
if aikCert, err = x509.ParseCertificate(raw); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
}
if sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg); sigAlg == x509.UnknownSignatureAlgorithm {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", statement.Algorithm))
} else if err = aikCert.CheckSignature(sigAlg, statement.CertInfo, statement.Signature); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v", err))
}
// Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements.
// 1/6 Version MUST be set to 3.
if aikCert.Version != 3 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
}
// 2/6 Subject field MUST be set to empty.
if aikCert.Subject.String() != "" {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty")
}
var (
manufacturer, model, version string
ekuValid = false
eku []asn1.ObjectIdentifier
constraints tpmBasicConstraints
rest []byte
)
for _, ext := range aikCert.Extensions {
switch {
case ext.Id.Equal(oidExtensionSubjectAltName):
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
return "", nil, err
}
case ext.Id.Equal(oidExtensionExtendedKeyUsage):
if rest, err = asn1.Unmarshal(ext.Value, &eku); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate extended key usage malformed")
} else if len(rest) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate extended key usage contains extra data")
}
found := false
for _, oid := range eku {
if oid.Equal(oidTCGKpAIKCertificate) {
found = true
break
}
}
if !found {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate extended key usage missing 2.23.133.8.3")
}
ekuValid = true
case ext.Id.Equal(oidExtensionBasicConstraints):
if rest, err = asn1.Unmarshal(ext.Value, &constraints); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
} else if len(rest) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
}
}
}
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
if manufacturer == "" || model == "" || version == "" {
return "", nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
}
if !isValidTPMManufacturer(manufacturer) {
return "", nil, ErrAttestationFormat.WithDetails("Invalid TPM manufacturer")
}
// 4/6 The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
if !ekuValid {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
}
// 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
// extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
// through metadata services. See, for example, the FIDO Metadata Service.
if constraints.IsCA {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
}
// 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
// [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
// as computed using the algorithm in the nameAlg field of pubArea
// using the procedure specified in [TPMv2-Part1] section 16.
//
// This needs to move after the x5c check as the QualifiedSigner only gets populated when it can be verified.
if ok, err = tpm2NameMatch(certInfo, pubArea); err != nil {
return "", nil, err
} else if !ok {
return "", nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea")
}
return string(metadata.AttCA), statement.X5C, err
}
func tpm2Exponent(params *tpm2.TPMSRSAParms) (exp uint32) {
if params.Exponent != 0 {
return params.Exponent
}
return 65537
}
func tpm2NameMatch(certInfo *tpm2.TPMSAttest, pubArea *tpm2.TPMTPublic) (match bool, err error) {
if certInfo == nil || pubArea == nil {
return false, nil
}
var (
certifyInfo *tpm2.TPMSCertifyInfo
name *tpm2.TPM2BName
)
if certifyInfo, err = certInfo.Attested.Certify(); err != nil {
return false, err
}
if name, err = tpm2.ObjectName(pubArea); err != nil {
return false, err
}
// Per the WebAuthn Specification §8.3 step 5:
//
// Note: The remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e.,
// qualifiedSigner, clockInfo and firmwareVersion are ignored. Depending on the properties of the aikCert key used,
// these fields may be obfuscated. If valid, these MAY be used as an input to risk engines.
//
// See: https://w3c.github.io/webauthn/#sctn-tpm-attestation
return subtle.ConstantTimeCompare(certifyInfo.Name.Buffer, name.Buffer) == 1, nil
}
func tpm2NameDigest(name tpm2.TPM2BName) (alg tpm2.TPMIAlgHash, digest []byte, err error) {
buf := name.Buffer
if len(buf) < 3 {
return 0, nil, fmt.Errorf("name too short")
}
alg = tpm2.TPMIAlgHash(binary.BigEndian.Uint16(buf[:2]))
var hash crypto.Hash
if hash, err = alg.Hash(); err != nil {
return 0, nil, fmt.Errorf("invalid hash algorithm: %w", err)
}
digest = buf[2:]
if len(digest) == 0 {
return 0, nil, fmt.Errorf("name digest is empty")
}
if len(digest) != hash.Size() {
return 0, nil, fmt.Errorf("invalid name digest length: %d", len(digest))
}
return alg, digest, nil
}
type tpm2AttStatement struct {
Version string
Algorithm int64
Signature []byte
CertInfo []byte
PubArea []byte
X5C []any
HasX5C bool
HasValidX5C bool
HasECDAAKeyID bool
HasValidECDAAKeyID bool
ECDAAKeyID []byte
}
func newTPM2AttStatement(raw map[string]any) (statement *tpm2AttStatement, err error) {
var ok bool
statement = &tpm2AttStatement{}
// Given the verification procedure inputs attStmt, authenticatorData
// and clientDataHash, the verification procedure is as follows.
// Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
if statement.Version, ok = raw[stmtVersion].(string); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving ver value")
}
if statement.Algorithm, ok = raw[stmtAlgorithm].(int64); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
if statement.Signature, ok = raw[stmtSignature].([]byte); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
if statement.CertInfo, ok = raw[stmtCertInfo].([]byte); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving certInfo value")
}
if statement.PubArea, ok = raw[stmtPubArea].([]byte); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving pubArea value")
}
var rawX5C, rawECDAAKeyID any
rawX5C, statement.HasX5C = raw[stmtX5C]
statement.X5C, statement.HasValidX5C = rawX5C.([]any)
rawECDAAKeyID, statement.HasECDAAKeyID = raw[stmtECDAAKID]
statement.ECDAAKeyID, statement.HasValidECDAAKeyID = rawECDAAKeyID.([]byte)
return statement, nil
}
// forEachSAN loops through the TPM SAN extension.
//
// RFC 5280, 4.2.1.6
// SubjectAltName ::= GeneralNames
//
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
//
// GeneralName ::= CHOICE {
// otherName [0] OtherName,
// rfc822Name [1] IA5String,
// dNSName [2] IA5String,
// x400Address [3] ORAddress,
// directoryName [4] Name,
// ediPartyName [5] EDIPartyName,
// uniformResourceIdentifier [6] IA5String,
// iPAddress [7] OCTET STRING,
// registeredID [8] OBJECT IDENTIFIER }
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
var seq asn1.RawValue
rest, err := asn1.Unmarshal(extension, &seq)
if err != nil {
return err
} else if len(rest) != 0 {
return errors.New("x509: trailing data after X.509 extension")
}
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
return asn1.StructuralError{Msg: "bad SAN sequence"}
}
rest = seq.Bytes
for len(rest) > 0 {
var v asn1.RawValue
rest, err = asn1.Unmarshal(rest, &v)
if err != nil {
return err
}
if err = callback(v.Tag, v.Bytes); err != nil {
return err
}
}
return nil
}
const (
nameTypeDN = 4
)
func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) {
err = forEachSAN(value, func(tag int, data []byte) error {
if tag == nameTypeDN {
tpmDeviceAttributes := pkix.RDNSequence{}
if _, err = asn1.Unmarshal(data, &tpmDeviceAttributes); err != nil {
return err
}
for _, rdn := range tpmDeviceAttributes {
if len(rdn) == 0 {
continue
}
for _, atv := range rdn {
value, ok := atv.Value.(string)
if !ok {
continue
}
if atv.Type.Equal(oidTCGAtTpmManufacturer) {
manufacturer = strings.TrimPrefix(value, "id:")
}
if atv.Type.Equal(oidTCGAtTpmModel) {
model = value
}
if atv.Type.Equal(oidTCGAtTPMVersion) {
version = strings.TrimPrefix(value, "id:")
}
}
}
}
return nil
})
return
}
type tpmManufacturer struct {
id string
name string
code string
}
// See https://trustedcomputinggroup.org/resource/vendor-id-registry/ for registry contents.
var (
tpmManufacturers = []tpmManufacturer{
{"414D4400", "AMD", "AMD"},
{"414E5400", "Ant Group", "ANT"},
{"41544D4C", "Atmel", "ATML"},
{"4252434D", "Broadcom", "BRCM"},
{"4353434F", "Cisco", "CSCO"},
{"464C5953", "Flyslice Technologies", "FLYS"},
{"524F4343", "Fuzhou Rockchip", "ROCC"},
{"474F4F47", "Google", "GOOG"},
{"48504900", "HPI", "HPI"},
{"48504500", "HPE", "HPE"},
{"48495349", "Huawei", "HISI"},
{"49424d00", "IBM", "IBM"},
{"49424D00", "IBM", "IBM"},
{"49465800", "Infineon", "IFX"},
{"494E5443", "Intel", "INTC"},
{"4C454E00", "Lenovo", "LEN"},
{"4D534654", "Microsoft", "MSFT"},
{"4E534D20", "National Semiconductor", "NSM"},
{"4E545A00", "Nationz", "NTZ"},
{"4E534700", "NSING", "NSG"},
{"4E544300", "Nuvoton Technology", "NTC"},
{"51434F4D", "Qualcomm", "QCOM"},
{"534D534E", "Samsung", "SECE"},
{"53454345", "SecEdge", "SecEdge"},
{"534E5300", "Sinosun", "SNS"},
{"534D5343", "SMSC", "SMSC"},
{"53544D20", "ST Microelectronics", "STM"},
{"54584E00", "Texas Instruments", "TXN"},
{"57454300", "Winbond", "WEC"},
{"5345414C", "Wisekey", "SEAL"},
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
}
)
func isValidTPMManufacturer(id string) bool {
for _, m := range tpmManufacturers {
if m.id == id {
return true
}
}
return false
}
func tpmParseAIKAttCA(x5c *x509.Certificate, x5cis []*x509.Certificate) (err *Error) {
if err = tpmParseSANExtension(x5c); err != nil {
return err
}
if err = tpmRemoveEKU(x5c); err != nil {
return err
}
for _, parent := range x5cis {
if err = tpmRemoveEKU(parent); err != nil {
return err
}
}
return nil
}
func tpmParseSANExtension(attestation *x509.Certificate) (protoErr *Error) {
var (
manufacturer, model, version string
err error
)
for _, ext := range attestation.Extensions {
if ext.Id.Equal(oidExtensionSubjectAltName) {
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
return ErrInvalidAttestation.WithDetails("Authenticator with invalid Authenticator Identity Key SAN data encountered during attestation validation.").WithInfo(fmt.Sprintf("Error occurred parsing SAN extension: %s", err.Error())).WithError(err)
}
}
}
if manufacturer == "" || model == "" || version == "" {
return ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate.")
}
var unhandled []asn1.ObjectIdentifier
for _, uce := range attestation.UnhandledCriticalExtensions {
if uce.Equal(oidExtensionSubjectAltName) {
continue
}
unhandled = append(unhandled, uce)
}
attestation.UnhandledCriticalExtensions = unhandled
return nil
}
type tpmBasicConstraints struct {
IsCA bool `asn1:"optional"`
MaxPathLen int `asn1:"optional,default:-1"`
}
// Remove extension key usage to avoid ExtKeyUsage check failure.
func tpmRemoveEKU(x5c *x509.Certificate) *Error {
var (
unknown []asn1.ObjectIdentifier
hasAiK bool
)
for _, eku := range x5c.UnknownExtKeyUsage {
if eku.Equal(oidTCGKpAIKCertificate) {
hasAiK = true
continue
}
if eku.Equal(oidMicrosoftKpPrivacyCA) {
continue
}
unknown = append(unknown, eku)
}
if !hasAiK {
return ErrAttestationFormat.WithDetails("Attestation Identity Key certificate missing required Extended Key Usage.")
}
x5c.UnknownExtKeyUsage = unknown
return nil
}
func init() {
RegisterAttestationFormat(AttestationFormatTPM, attestationFormatValidationHandlerTPM)
}
File diff suppressed because one or more lines are too long
+434
View File
@@ -0,0 +1,434 @@
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
)
const (
minAuthDataLength = 37
minAttestedAuthLength = 55
maxCredentialIDLength = 1023
)
// AuthenticatorResponse represents the IDL with the same name.
//
// Authenticators respond to Relying Party requests by returning an object derived from the AuthenticatorResponse
// interface
//
// Specification: §5.2. Authenticator Responses (https://www.w3.org/TR/webauthn/#iface-authenticatorresponse)
type AuthenticatorResponse struct {
// From the spec https://www.w3.org/TR/webauthn/#dom-authenticatorresponse-clientdatajson
// This attribute contains a JSON serialization of the client data passed to the authenticator
// by the client in its call to either create() or get().
ClientDataJSON URLEncodedBase64 `json:"clientDataJSON"`
}
// AuthenticatorData represents the IDL with the same name.
//
// The authenticator data structure encodes contextual bindings made by the authenticator. These bindings are controlled
// by the authenticator itself, and derive their trust from the WebAuthn Relying Party's assessment of the security
// properties of the authenticator. In one extreme case, the authenticator may be embedded in the client, and its
// bindings may be no more trustworthy than the client data. At the other extreme, the authenticator may be a discrete
// entity with high-security hardware and software, connected to the client over a secure channel. In both cases, the
// Relying Party receives the authenticator data in the same format, and uses its knowledge of the authenticator to make
// trust decisions.
//
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be devices
// with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
//
// Specification: §6.1. Authenticator Data (https://www.w3.org/TR/webauthn/#sctn-authenticator-data)
type AuthenticatorData struct {
RPIDHash []byte `json:"rpid"`
Flags AuthenticatorFlags `json:"flags"`
Counter uint32 `json:"sign_count"`
AttData AttestedCredentialData `json:"att_data"`
ExtData []byte `json:"ext_data"`
}
// AttestedCredentialData is a variable-length byte array added to the authenticator data when generating an attestation
// object for a credential.
//
// Specification: §6.5.2. Attested Credential Data (https://www.w3.org/TR/webauthn/#sctn-attested-credential-data)
type AttestedCredentialData struct {
// AAGUID is the 16-byte Authenticator Attestation GUID, a unique identifier indicating the type of the
// authenticator (i.e. make and model).
AAGUID []byte `json:"aaguid"`
// CredentialID is the credential identifier whose length is prepended as a 16-bit unsigned big-endian integer.
CredentialID []byte `json:"credential_id"`
// CredentialPublicKey is the CBOR-encoded credential public key using the COSE_Key format defined in
// Section 7 of [RFC9052].
CredentialPublicKey []byte `json:"public_key"`
}
// CredentialMediationRequirement represents mediation requirements for clients. When making a request via get(options)
// or create(options), developers can set a case-by-case requirement for user mediation by choosing the appropriate
// CredentialMediationRequirement enum value.
//
// See https://www.w3.org/TR/credential-management-1/#mediation-requirements
type CredentialMediationRequirement string
const (
// MediationDefault lets the browser choose the mediation flow completely as if it wasn't specified at all.
MediationDefault CredentialMediationRequirement = ""
// MediationSilent indicates user mediation is suppressed for the given operation. If the operation can be performed
// without user involvement, wonderful. If user involvement is necessary, then the operation will return null rather
// than involving the user.
MediationSilent CredentialMediationRequirement = "silent"
// MediationOptional indicates if credentials can be handed over for a given operation without user mediation, they
// will be. If user mediation is required, then the user agent will involve the user in the decision.
MediationOptional CredentialMediationRequirement = "optional"
// MediationConditional indicates for get(), discovered credentials are presented to the user in a non-modal dialog
// along with an indication of the origin which is requesting credentials. If the user makes a gesture outside of
// the dialog, the dialog closes without resolving or rejecting the Promise returned by the get() method and without
// causing a user-visible error condition. If the user makes a gesture that selects a credential, that credential is
// returned to the caller. The prevent silent access flag is treated as being true regardless of its actual value:
// the conditional behavior always involves user mediation of some sort if applicable credentials are discovered.
MediationConditional CredentialMediationRequirement = "conditional"
// MediationRequired indicates the user agent will not hand over credentials without user mediation, even if the
// prevent silent access flag is unset for an origin.
MediationRequired CredentialMediationRequirement = "required"
)
// AuthenticatorAttachment represents the IDL enum of the same name, and is used as part of the Authenticator Selection
// Criteria.
//
// This enumerations values describe authenticators' attachment modalities. Relying Parties use this to express a
// preferred authenticator attachment modality when calling navigator.credentials.create() to create a credential.
//
// If this member is present, eligible authenticators are filtered to only authenticators attached with the specified
// §5.4.5 Authenticator Attachment Enumeration (enum AuthenticatorAttachment). The value SHOULD be a member of
// AuthenticatorAttachment but client platforms MUST ignore unknown values, treating an unknown value as if the member
// does not exist.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-authenticatorattachment)
//
// Specification: §5.4.5. Authenticator Attachment Enumeration (https://www.w3.org/TR/webauthn/#enum-attachment)
type AuthenticatorAttachment string
const (
// Platform represents a platform authenticator is attached using a client device-specific transport, called
// platform attachment, and is usually not removable from the client device. A public key credential bound to a
// platform authenticator is called a platform credential.
Platform AuthenticatorAttachment = "platform"
// CrossPlatform represents a roaming authenticator is attached using cross-platform transports, called
// cross-platform attachment. Authenticators of this class are removable from, and can "roam" among, client devices.
// A public key credential bound to a roaming authenticator is called a roaming credential.
CrossPlatform AuthenticatorAttachment = "cross-platform"
)
// ResidentKeyRequirement represents the IDL of the same name.
//
// This enumerations values describe the Relying Party's requirements for client-side discoverable credentials
// (formerly known as resident credentials or resident keys).
//
// Specifies the extent to which the Relying Party desires to create a client-side discoverable credential. For
// historical reasons the naming retains the deprecated “resident” terminology. The value SHOULD be a member of
// ResidentKeyRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member
// does not exist. If no value is given then the effective value is required if requireResidentKey is true or
// discouraged if it is false or absent.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-residentkey)
//
// Specification: §5.4.6. Resident Key Requirement Enumeration (https://www.w3.org/TR/webauthn/#enumdef-residentkeyrequirement)
type ResidentKeyRequirement string
const (
// ResidentKeyRequirementDiscouraged indicates the Relying Party prefers creating a server-side credential, but will
// accept a client-side discoverable credential. This is the default.
ResidentKeyRequirementDiscouraged ResidentKeyRequirement = "discouraged"
// ResidentKeyRequirementPreferred indicates to the client we would prefer a discoverable credential.
ResidentKeyRequirementPreferred ResidentKeyRequirement = "preferred"
// ResidentKeyRequirementRequired indicates the Relying Party requires a client-side discoverable credential, and is
// prepared to receive an error if a client-side discoverable credential cannot be created.
ResidentKeyRequirementRequired ResidentKeyRequirement = "required"
)
// AuthenticatorTransport represents the IDL enum with the same name.
//
// Authenticators may implement various transports for communicating with clients. This enumeration defines hints as to
// how clients might communicate with a particular authenticator in order to obtain an assertion for a specific
// credential. Note that these hints represent the WebAuthn Relying Party's best belief as to how an authenticator may
// be reached. A Relying Party will typically learn of the supported transports for a public key credential via
// getTransports().
//
// Specification: §5.8.4. Authenticator Transport Enumeration (https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport)
type AuthenticatorTransport string
const (
// USB indicates the respective authenticator can be contacted over removable USB.
USB AuthenticatorTransport = "usb"
// NFC indicates the respective authenticator can be contacted over Near Field Communication (NFC).
NFC AuthenticatorTransport = "nfc"
// BLE indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE).
BLE AuthenticatorTransport = "ble"
// SmartCard indicates the respective authenticator can be contacted over ISO/IEC 7816 smart card with contacts.
//
// WebAuthn Level 3.
SmartCard AuthenticatorTransport = "smart-card"
// Hybrid indicates the respective authenticator can be contacted using a combination of (often separate)
// data-transport and proximity mechanisms. This supports, for example, authentication on a desktop computer using
// a smartphone.
//
// WebAuthn Level 3.
Hybrid AuthenticatorTransport = "hybrid"
// Internal indicates the respective authenticator is contacted using a client device-specific transport, i.e., it
// is a platform authenticator. These authenticators are not removable from the client device.
Internal AuthenticatorTransport = "internal"
)
// UserVerificationRequirement is a representation of the UserVerificationRequirement IDL enum.
//
// A WebAuthn Relying Party may require user verification for some of its operations but not for others,
// and may use this type to express its needs.
//
// Specification: §5.8.6. User Verification Requirement Enumeration (https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement)
type UserVerificationRequirement string
const (
// VerificationRequired User verification is required to create/release a credential.
VerificationRequired UserVerificationRequirement = "required"
// VerificationPreferred User verification is preferred to create/release a credential.
VerificationPreferred UserVerificationRequirement = "preferred" // This is the default.
// VerificationDiscouraged The authenticator should not verify the user for the credential.
VerificationDiscouraged UserVerificationRequirement = "discouraged"
)
// AuthenticatorFlags A byte of information returned during during ceremonies in the
// authenticatorData that contains bits that give us information about the
// whether the user was present and/or verified during authentication, and whether
// there is attestation or extension data present. Bit 0 is the least significant bit.
//
// Specification: §6.1. Authenticator Data - Flags (https://www.w3.org/TR/webauthn/#flags)
type AuthenticatorFlags byte
// The bits that do not have flags are reserved for future use.
const (
// FlagUserPresent Bit 00000001 in the byte sequence. Tells us if user is present. Also referred to as the UP flag.
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP.
// FlagRFU1 is a reserved for future use flag.
FlagRFU1
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
// by the authenticator using a biometric or PIN. Also referred to as the UV flag.
FlagUserVerified
// FlagBackupEligible Bit 00001000 in the byte sequence. Tells us if a backup is eligible for device. Also referred
// to as the BE flag.
FlagBackupEligible // Referred to as BE.
// FlagBackupState Bit 00010000 in the byte sequence. Tells us if a backup state for device. Also referred to as the
// BS flag.
FlagBackupState
// FlagRFU2 is a reserved for future use flag.
FlagRFU2
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
// the authenticator added attested credential data. Also referred to as the AT flag.
FlagAttestedCredentialData
// FlagHasExtensions Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions. Also
// referred to as the ED flag.
FlagHasExtensions
)
// UserPresent returns if the UP flag was set.
func (flag AuthenticatorFlags) UserPresent() bool {
return flag.HasUserPresent()
}
// UserVerified returns if the UV flag was set.
func (flag AuthenticatorFlags) UserVerified() bool {
return flag.HasUserVerified()
}
// HasUserPresent returns if the UP flag was set.
func (flag AuthenticatorFlags) HasUserPresent() bool {
return (flag & FlagUserPresent) == FlagUserPresent
}
// HasUserVerified returns if the UV flag was set.
func (flag AuthenticatorFlags) HasUserVerified() bool {
return (flag & FlagUserVerified) == FlagUserVerified
}
// HasAttestedCredentialData returns if the AT flag was set.
func (flag AuthenticatorFlags) HasAttestedCredentialData() bool {
return (flag & FlagAttestedCredentialData) == FlagAttestedCredentialData
}
// HasExtensions returns if the ED flag was set.
func (flag AuthenticatorFlags) HasExtensions() bool {
return (flag & FlagHasExtensions) == FlagHasExtensions
}
// HasBackupEligible returns if the BE flag was set.
func (flag AuthenticatorFlags) HasBackupEligible() bool {
return (flag & FlagBackupEligible) == FlagBackupEligible
}
// HasBackupState returns if the BS flag was set.
func (flag AuthenticatorFlags) HasBackupState() bool {
return (flag & FlagBackupState) == FlagBackupState
}
// Unmarshal will take the raw Authenticator Data and marshals it into AuthenticatorData for further validation.
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be
// devices with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
// The authenticator data structure is a byte array of 37 bytes or more, and is laid out in this table:
// https://www.w3.org/TR/webauthn/#table-authData
func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) (err error) {
if minAuthDataLength > len(rawAuthData) {
return ErrBadRequest.
WithDetails("Authenticator data length too short").
WithInfo(fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes", minAuthDataLength, len(rawAuthData)))
}
a.RPIDHash = rawAuthData[:32]
a.Flags = AuthenticatorFlags(rawAuthData[32])
a.Counter = binary.BigEndian.Uint32(rawAuthData[33:37])
remaining := len(rawAuthData) - minAuthDataLength
if a.Flags.HasAttestedCredentialData() {
if len(rawAuthData) > minAttestedAuthLength {
if err = a.unmarshalAttestedData(rawAuthData); err != nil {
return err
}
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey)
remaining -= attDataLen
} else {
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing")
}
} else {
if !a.Flags.HasExtensions() && len(rawAuthData) != 37 {
return ErrBadRequest.WithDetails("Attested credential flag not set")
}
}
if a.Flags.HasExtensions() {
if remaining != 0 {
a.ExtData = rawAuthData[len(rawAuthData)-remaining:]
remaining -= len(a.ExtData)
} else {
return ErrBadRequest.WithDetails("Extensions flag set but extensions data is missing")
}
}
if remaining != 0 {
return ErrBadRequest.WithDetails("Leftover bytes decoding AuthenticatorData")
}
return nil
}
// If Attestation Data is present, unmarshall that into the appropriate public key structure.
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) (err error) {
a.AttData.AAGUID = rawAuthData[37:53]
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
if len(rawAuthData) < int(55+idLength) {
return ErrBadRequest.WithDetails("Authenticator attestation data length too short")
}
if idLength > maxCredentialIDLength {
return ErrBadRequest.WithDetails("Authenticator attestation data credential id length too long")
}
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
a.AttData.CredentialPublicKey, err = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
if err != nil {
return ErrBadRequest.WithDetails(fmt.Sprintf("Could not unmarshal Credential Public Key: %v", err)).WithError(err)
}
return nil
}
// Unmarshall the credential's Public Key into CBOR encoding.
func unmarshalCredentialPublicKey(keyBytes []byte) (rawBytes []byte, err error) {
var m any
if err = webauthncbor.Unmarshal(keyBytes, &m); err != nil {
return nil, err
}
if rawBytes, err = webauthncbor.Marshal(m); err != nil {
return nil, err
}
return rawBytes, nil
}
// ResidentKeyRequired - Require that the key be private key resident to the client device.
func ResidentKeyRequired() *bool {
required := true
return &required
}
// ResidentKeyNotRequired - Do not require that the private key be resident to the client device.
func ResidentKeyNotRequired() *bool {
required := false
return &required
}
// Verify on AuthenticatorData handles Steps 13 through 15 & 17 for Registration
// and Steps 15 through 18 for Assertion.
func (a *AuthenticatorData) Verify(rpIdHash []byte, appIDHash []byte, userVerificationRequired bool, userPresenceRequired bool) (err error) {
// Registration Step 13 & Assertion Step 15
// Verify that the RP ID hash in authData is indeed the SHA-256
// hash of the RP ID expected by the RP.
if !bytes.Equal(a.RPIDHash, rpIdHash) && !bytes.Equal(a.RPIDHash, appIDHash) {
return ErrVerification.WithInfo(fmt.Sprintf("RP Hash mismatch. Expected %x and Received %x", a.RPIDHash, rpIdHash))
}
// Registration Step 15 & Assertion Step 16
// Verify that the User Present bit of the flags in authData is set.
if userPresenceRequired && !a.Flags.UserPresent() {
return ErrVerification.WithInfo("User presence required but flag not set by authenticator")
}
// Registration Step 15 & Assertion Step 17
// If user verification is required for this assertion, verify that
// the User Verified bit of the flags in authData is set.
if userVerificationRequired && !a.Flags.UserVerified() {
return ErrVerification.WithInfo("User verification required but flag not set by authenticator")
}
// Registration Step 17 & Assertion Step 18
// Verify that the values of the client extension outputs in clientExtensionResults
// and the authenticator extension outputs in the extensions in authData are as
// expected, considering the client extension input values that were given as the
// extensions option in the create() call. In particular, any extension identifier
// values in the clientExtensionResults and the extensions in authData MUST be also be
// present as extension identifier values in the extensions member of options, i.e., no
// extensions are present that were not requested. In the general case, the meaning
// of "are as expected" is specific to the Relying Party and which extensions are in use.
// This is not yet fully implemented by the spec or by browsers.
return nil
}
+560
View File
@@ -0,0 +1,560 @@
package protocol
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
noneAuthDataBase64 = "pkLSG3xtVeHOI8U5mCjSx0m/am7y/gPMnhDN9O1ttItBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQMAxl6G32ykWaLrv/ouCs5HoGsvONqBtOb7ZmyMs8K8PccnwyyqPzWn/yZuyQmQBguvjYSvH6gDBlFG65quUDCSlAQIDJiABIVggyJGP+ra/u/eVjqN4OeYXUShRWxrEeC6Sb5/bZmJ9q8MiWCCHIkRdg5oRb1RHoFVYUpogcjlObCKFsV1ls1T+uUc6rA=="
attAuthDataBase64 = "lWkIjx7O4yMpVANdvRDXyuORMFonUbVZu4/Xy7IpvdRBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQIniszxcGnhupdPFOHJIm6dscrWCC2h8xHicBMu91THD0kdOdB0QQtkaEn+6KfsfT1o3NmmFT8YfXrG734WfVSmlAQIDJiABIVggyoHHeiUw5aSbt8/GsL9zaqZGRzV26A4y3CnCGUhVXu4iWCBMnc8za5xgPzIygngAv9W+vZTMGJwwZcM4sjiqkcb/1g=="
)
func TestAuthenticatorFlags_UserPresent(t *testing.T) {
testCases := []struct {
name string
flag AuthenticatorFlags
expected bool
}{
{
"Present",
AuthenticatorFlags(0x01),
true,
},
{
"Missing",
AuthenticatorFlags(0x10),
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.flag.UserPresent())
})
}
}
func TestAuthenticatorFlags_UserVerified(t *testing.T) {
testCases := []struct {
name string
flag AuthenticatorFlags
expected bool
}{
{
"Present",
AuthenticatorFlags(0x04),
true,
},
{
"Missing",
AuthenticatorFlags(0x02),
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.flag.UserVerified())
})
}
}
func TestAuthenticatorFlags_HasAttestedCredentialData(t *testing.T) {
testCases := []struct {
name string
flag AuthenticatorFlags
expected bool
}{
{
"Present",
AuthenticatorFlags(0x40),
true,
},
{
"Missing",
AuthenticatorFlags(0x01),
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.flag.HasAttestedCredentialData())
})
}
}
func TestAuthenticatorFlags_HasExtensions(t *testing.T) {
testCases := []struct {
name string
flag AuthenticatorFlags
expected bool
}{
{
"Present",
AuthenticatorFlags(0x80),
true,
},
{
"Missing",
AuthenticatorFlags(0x01),
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.flag.HasExtensions())
})
}
}
func TestAuthenticatorData_Unmarshal(t *testing.T) {
type fields struct {
RPIDHash []byte
Flags AuthenticatorFlags
Counter uint32
AttData AttestedCredentialData
ExtData []byte
}
type args struct {
rawAuthData []byte
}
noneAuthData, _ := base64.StdEncoding.DecodeString(noneAuthDataBase64)
attAuthData, _ := base64.StdEncoding.DecodeString(attAuthDataBase64)
// Empty data.
badAuthData1 := []byte{}
// Attested credential data missing.
badAuthData2 := make([]byte, minAttestedAuthLength-1)
copy(badAuthData2, attAuthData)
// Flags not set but data exists.
badAuthData3 := make([]byte, len(attAuthData))
copy(badAuthData3, attAuthData)
badAuthData3[32] &= 0b0011_1111
// Extensions data missing.
badAuthData4 := make([]byte, len(attAuthData))
copy(badAuthData4, attAuthData)
badAuthData4[32] |= 0b1000_0000
// Leftover bytes.
badAuthData5 := make([]byte, len(attAuthData)) //nolint:prealloc
copy(badAuthData5, attAuthData)
badAuthData5 = append(badAuthData5, []byte("Hello World")...)
testCases := []struct {
name string
fields fields
args args
err string
errType string
errDetails string
errInfo string
}{
{
name: "NoneMarshallSuccessfully",
fields: fields{},
args: args{
noneAuthData,
},
},
{
name: "AttDataMarshallSuccessfully",
fields: fields{},
args: args{
attAuthData,
},
},
{
name: "AuthenticatorDataTooShort",
fields: fields{},
args: args{
badAuthData1,
},
err: "Authenticator data length too short",
errType: "invalid_request",
errDetails: "Authenticator data length too short",
errInfo: fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes", minAuthDataLength, len(badAuthData1)),
},
{
name: "AttestedCredentialMissing",
fields: fields{},
args: args{
badAuthData2,
},
err: "Attested credential flag set but data is missing",
errType: "invalid_request",
errDetails: "Attested credential flag set but data is missing",
errInfo: "",
},
{
name: "AttestedCredentialMissing",
fields: fields{},
args: args{
badAuthData3,
},
err: "Attested credential flag not set",
errType: "invalid_request",
errDetails: "Attested credential flag not set",
errInfo: "",
},
{
name: "ExtensionsDataMissing",
fields: fields{},
args: args{
badAuthData4,
},
err: "Extensions flag set but extensions data is missing",
errType: "invalid_request",
errDetails: "Extensions flag set but extensions data is missing",
errInfo: "",
},
{
name: "LeftoverBytes",
fields: fields{},
args: args{
badAuthData5,
},
err: "Leftover bytes decoding AuthenticatorData",
errType: "invalid_request",
errDetails: "Leftover bytes decoding AuthenticatorData",
errInfo: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &AuthenticatorData{
RPIDHash: tc.fields.RPIDHash,
Flags: tc.fields.Flags,
Counter: tc.fields.Counter,
AttData: tc.fields.AttData,
ExtData: tc.fields.ExtData,
}
err := a.Unmarshal(tc.args.rawAuthData)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
} else {
assert.NoError(t, err)
}
})
}
}
func TestAuthenticatorData_unmarshalAttestedData(t *testing.T) {
type fields struct {
RPIDHash []byte
Flags AuthenticatorFlags
Counter uint32
AttData AttestedCredentialData
ExtData []byte
}
type args struct {
rawAuthData []byte
}
noneAuthData, _ := base64.StdEncoding.DecodeString(noneAuthDataBase64)
attAuthData, _ := base64.StdEncoding.DecodeString(attAuthDataBase64)
// Data length too short.
badAuthData1 := make([]byte, len(attAuthData))
copy(badAuthData1, attAuthData)
binary.BigEndian.PutUint16(badAuthData1[53:], 256)
// ID length too long.
badAuthData2 := make([]byte, len(attAuthData)+maxCredentialIDLength+1)
copy(badAuthData2, attAuthData)
binary.BigEndian.PutUint16(badAuthData2[53:], maxCredentialIDLength+1)
// Malformed public key.
badAuthData3 := make([]byte, 119) //nolint:prealloc
copy(badAuthData3, attAuthData[:119])
badData, _ := hex.DecodeString("83FF20030102")
badAuthData3 = append(badAuthData3, badData...)
testCases := []struct {
name string
fields fields
args args
err string
errType string
errDetails string
errInfo string
}{
{
name: "None Marshall Successfully",
fields: fields{},
args: args{
noneAuthData,
},
},
{
name: "Att Data Marshall Successfully",
fields: fields{},
args: args{
attAuthData,
},
},
{
name: "Data length too short",
fields: fields{},
args: args{
badAuthData1,
},
err: "Authenticator attestation data length too short",
errType: "invalid_request",
errDetails: "Authenticator attestation data length too short",
errInfo: "",
},
{
name: "ID length too long",
fields: fields{},
args: args{
badAuthData2,
},
err: "Authenticator attestation data credential id length too long",
errType: "invalid_request",
errDetails: "Authenticator attestation data credential id length too long",
errInfo: "",
},
{
name: "Could not unmarshal Credential Public Key",
fields: fields{},
args: args{
badAuthData3,
},
err: "Could not unmarshal Credential Public Key: cbor: unexpected \"break\" code",
errType: "invalid_request",
errDetails: "Could not unmarshal Credential Public Key: cbor: unexpected \"break\" code",
errInfo: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := &AuthenticatorData{
RPIDHash: tc.fields.RPIDHash,
Flags: tc.fields.Flags,
Counter: tc.fields.Counter,
AttData: tc.fields.AttData,
ExtData: tc.fields.ExtData,
}
err := actual.unmarshalAttestedData(tc.args.rawAuthData)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
} else {
assert.NoError(t, err)
}
})
}
}
func TestAuthenticatorFlags_HasBackupEligible(t *testing.T) {
testCases := []struct {
name string
flag AuthenticatorFlags
expected bool
}{
{
name: "Present",
flag: FlagBackupEligible,
expected: true,
},
{
name: "PresentWithOtherFlags",
flag: FlagBackupEligible | FlagUserPresent,
expected: true,
},
{
name: "Missing",
flag: FlagUserPresent,
expected: false,
},
{
name: "Zero",
flag: 0,
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.flag.HasBackupEligible())
})
}
}
func TestAuthenticatorFlags_HasBackupState(t *testing.T) {
testCases := []struct {
name string
flag AuthenticatorFlags
expected bool
}{
{
name: "Present",
flag: FlagBackupState,
expected: true,
},
{
name: "PresentWithOtherFlags",
flag: FlagBackupState | FlagBackupEligible,
expected: true,
},
{
name: "Missing",
flag: FlagUserPresent,
expected: false,
},
{
name: "Zero",
flag: 0,
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.flag.HasBackupState())
})
}
}
func TestResidentKeyRequired(t *testing.T) {
result := ResidentKeyRequired()
require.NotNil(t, result)
assert.True(t, *result)
}
func TestResidentKeyNotRequired(t *testing.T) {
result := ResidentKeyNotRequired()
require.NotNil(t, result)
assert.False(t, *result)
}
func TestAuthenticatorData_Verify(t *testing.T) {
type fields struct {
RPIDHash []byte
Flags AuthenticatorFlags
Counter uint32
AttData AttestedCredentialData
ExtData []byte
}
type args struct {
rpIdHash []byte
userVerificationRequired bool
userPresenceRequired bool
}
testCases := []struct {
name string
fields fields
args args
err string
errType string
errDetails string
errInfo string
}{
{
name: "Success",
fields: fields{
RPIDHash: []byte{1, 2, 3},
Flags: AuthenticatorFlags(0x05),
},
args: args{
rpIdHash: []byte{1, 2, 3},
},
err: "",
},
{
name: "RP hash mismatch",
fields: fields{
RPIDHash: []byte{0xff},
},
args: args{
rpIdHash: []byte{0xaa},
},
err: "Error validating the authenticator response",
errType: "verification_error",
errDetails: "Error validating the authenticator response",
errInfo: "RP Hash mismatch. Expected ff and Received aa",
},
{
name: "UP flag not set",
fields: fields{
RPIDHash: []byte{1, 2, 3},
Flags: AuthenticatorFlags(0x04),
},
args: args{
rpIdHash: []byte{1, 2, 3},
userPresenceRequired: true,
},
err: "Error validating the authenticator response",
errType: "verification_error",
errDetails: "Error validating the authenticator response",
errInfo: "User presence required but flag not set by authenticator",
},
{
name: "User verification required",
fields: fields{
RPIDHash: []byte{1, 2, 3},
Flags: AuthenticatorFlags(0x01),
},
args: args{
rpIdHash: []byte{1, 2, 3},
userVerificationRequired: true,
userPresenceRequired: true,
},
err: "Error validating the authenticator response",
errType: "verification_error",
errDetails: "Error validating the authenticator response",
errInfo: "User verification required but flag not set by authenticator",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &AuthenticatorData{
RPIDHash: tc.fields.RPIDHash,
Flags: tc.fields.Flags,
Counter: tc.fields.Counter,
AttData: tc.fields.AttData,
ExtData: tc.fields.ExtData,
}
err := a.Verify(tc.args.rpIdHash, nil, tc.args.userVerificationRequired, tc.args.userPresenceRequired)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
} else {
assert.NoError(t, err)
}
})
}
}
+52
View File
@@ -0,0 +1,52 @@
package protocol
import (
"bytes"
"encoding/base64"
"reflect"
)
// URLEncodedBase64 represents a byte slice holding URL-encoded base64 data.
// When fields of this type are unmarshalled from JSON, the data is base64
// decoded into a byte slice.
type URLEncodedBase64 []byte
func (e URLEncodedBase64) String() string {
return base64.RawURLEncoding.EncodeToString(e)
}
// UnmarshalJSON base64 decodes a URL-encoded value, storing the result in the
// provided byte slice.
func (e *URLEncodedBase64) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
return nil
}
// Trim the leading and trailing quotes from raw JSON data (the whole value part).
data = bytes.Trim(data, `"`)
// Trim the trailing equal characters.
data = bytes.TrimRight(data, "=")
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
n, err := base64.RawURLEncoding.Decode(out, data)
if err != nil {
return err
}
v := reflect.ValueOf(e).Elem()
v.SetBytes(out[:n])
return nil
}
// MarshalJSON base64 encodes a non URL-encoded value, storing the result in the
// provided byte slice.
func (e URLEncodedBase64) MarshalJSON() ([]byte, error) {
if e == nil {
return []byte("null"), nil
}
return []byte(`"` + base64.RawURLEncoding.EncodeToString(e) + `"`), nil
}
+113
View File
@@ -0,0 +1,113 @@
package protocol
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestURLEncodedBase64_MarshalJSON(t *testing.T) {
testCases := []struct {
name string
have URLEncodedBase64
expected string
}{
{
name: "ShouldMarshalData",
have: URLEncodedBase64("test data"),
expected: `"dGVzdCBkYXRh"`,
},
{
name: "ShouldMarshalNil",
have: nil,
expected: `null`,
},
{
name: "ShouldMarshalEmpty",
have: URLEncodedBase64{},
expected: `""`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := tc.have.MarshalJSON()
require.NoError(t, err)
assert.Equal(t, tc.expected, string(data))
})
}
}
func TestURLEncodedBase64_UnmarshalJSON_Error(t *testing.T) {
testCases := []struct {
name string
data string
err string
}{
{
name: "ShouldFailInvalidBase64",
data: `"not valid base64!!!"`,
err: "illegal base64 data at input byte 3",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var e URLEncodedBase64
assert.EqualError(t, e.UnmarshalJSON([]byte(tc.data)), tc.err)
})
}
}
func TestBase64UnmarshalJSON(t *testing.T) {
type testData struct {
StringData string `json:"string_data"`
EncodedData URLEncodedBase64 `json:"encoded_data"`
}
testCases := []struct {
name string
message string
expected testData
err string
}{
{
name: "ShouldHandleBase64Data",
message: "\"" + base64.RawURLEncoding.EncodeToString([]byte("test base64 data")) + "\"",
expected: testData{
StringData: "test string",
EncodedData: URLEncodedBase64("test base64 data"),
},
err: "",
},
{
name: "ShouldHandleNull",
message: "null",
expected: testData{
StringData: "test string",
EncodedData: nil,
},
err: "",
},
}
for _, tc := range testCases {
raw := fmt.Sprintf(`{"string_data": "test string", "encoded_data": %s}`, tc.message)
actual := &testData{}
if tc.err != "" {
assert.EqualError(t, json.NewDecoder(strings.NewReader(raw)).Decode(actual), tc.err)
} else {
assert.NoError(t, json.NewDecoder(strings.NewReader(raw)).Decode(actual))
}
assert.Equal(t, tc.expected.EncodedData, actual.EncodedData)
assert.Equal(t, tc.expected.StringData, actual.StringData)
}
}
+20
View File
@@ -0,0 +1,20 @@
package protocol
import (
"crypto/rand"
)
// ChallengeLength - Length of bytes to generate for a challenge.
const ChallengeLength = DefaultChallengeLength
// CreateChallenge creates a new challenge that should be signed and returned by the authenticator. The spec recommends
// using at least 16 bytes with 100 bits of entropy. We use 32 bytes.
func CreateChallenge() (challenge URLEncodedBase64, err error) {
challenge = make([]byte, ChallengeLength)
if _, err = rand.Read(challenge); err != nil {
return nil, err
}
return challenge, nil
}
+44
View File
@@ -0,0 +1,44 @@
package protocol
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateChallenge(t *testing.T) {
challenge, err := CreateChallenge()
assert.NoError(t, err)
require.NotNil(t, challenge)
assert.Len(t, challenge, 32)
}
func TestChallenge_String(t *testing.T) {
newChallenge, err := CreateChallenge()
require.NoError(t, err)
assert.NotNil(t, newChallenge)
expectedChallenge := base64.RawURLEncoding.EncodeToString(newChallenge)
testCases := []struct {
name string
have URLEncodedBase64
expected string
}{
{
"Successful",
newChallenge,
expectedChallenge,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.have.String())
})
}
}
+321
View File
@@ -0,0 +1,321 @@
package protocol
import (
"crypto/subtle"
"fmt"
"net/url"
"strings"
)
// CollectedClientData represents the contextual bindings of both the WebAuthn Relying Party
// and the client. It is a key-value mapping whose keys are strings. Values can be any type
// that has a valid encoding in JSON. Its structure is defined by the following Web IDL.
//
// Specification: §5.8.1. Client Data Used in WebAuthn Signatures (https://www.w3.org/TR/webauthn/#dictdef-collectedclientdata)
type CollectedClientData struct {
// Type contains the string "webauthn.create" when creating new credentials, and "webauthn.get" when getting an
// assertion from an existing credential. The purpose of this member is to prevent certain types of signature
// confusion attacks (where an attacker substitutes one legitimate signature for another).
Type CeremonyType `json:"type"`
// Challenge contains the base64url encoding of the challenge provided by the Relying Party.
Challenge string `json:"challenge"`
// Origin contains the fully qualified origin of the requester, as provided to the authenticator by the client.
Origin string `json:"origin"`
// TopOrigin contains the fully qualified top-level origin of the requester when the client is cross-origin.
// This is only present when CrossOrigin is true.
//
// WebAuthn Level 3.
TopOrigin string `json:"topOrigin,omitempty"`
// CrossOrigin indicates whether the calling context is an iframe that is not same-origin with its ancestor.
//
// WebAuthn Level 3.
CrossOrigin bool `json:"crossOrigin,omitempty"`
// TokenBinding contains information about the state of the Token Binding protocol.
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"`
// Hint is an opaque field that may be added by the client. Chromium-based browsers include this field to remind
// implementers not to perform string comparison on the clientDataJSON.
Hint string `json:"new_keys_may_be_added_here,omitempty"`
}
// CeremonyType represents the type of WebAuthn ceremony being performed.
//
// Specification: §5.8.1. Client Data Used in WebAuthn Signatures (https://www.w3.org/TR/webauthn/#dom-collectedclientdata-type)
type CeremonyType string
const (
// CreateCeremony is the ceremony type for credential registration ("webauthn.create").
CreateCeremony CeremonyType = "webauthn.create"
// AssertCeremony is the ceremony type for authentication assertion ("webauthn.get").
AssertCeremony CeremonyType = "webauthn.get"
)
// TokenBinding contains information about the state of the Token Binding protocol used when communicating with the
// Relying Party. Its absence indicates that the client doesn't support token binding.
//
// Specification: §5.8.1. Client Data Used in WebAuthn Signatures (https://www.w3.org/TR/webauthn/#dom-collectedclientdata-tokenbinding)
type TokenBinding struct {
Status TokenBindingStatus `json:"status"`
ID string `json:"id,omitempty"`
}
// TokenBindingStatus represents the state of Token Binding between the client and the Relying Party.
type TokenBindingStatus string
const (
// Present indicates token binding was used when communicating with the
// Relying Party. In this case, the id member MUST be present.
Present TokenBindingStatus = "present"
// Supported indicates the client supports token binding, but it was not
// negotiated when communicating with the Relying Party.
Supported TokenBindingStatus = "supported"
// NotSupported indicates token binding not supported
// when communicating with the Relying Party.
NotSupported TokenBindingStatus = "not-supported"
)
// FullyQualifiedOrigin returns the origin per the HTML spec: (scheme)://(host)[:(port)].
func FullyQualifiedOrigin(rawOrigin string) (fqOrigin string, err error) {
if strings.HasPrefix(rawOrigin, "android:apk-key-hash:") {
return rawOrigin, nil
}
var origin *url.URL
if origin, err = url.ParseRequestURI(rawOrigin); err != nil {
return "", err
}
if origin.Host == "" {
return "", fmt.Errorf("url '%s' does not have a host", rawOrigin)
}
origin.Path, origin.RawPath, origin.RawQuery, origin.User = "", "", "", nil
return origin.String(), nil
}
// Verify handles steps 3 through 6 of verifying the registering client data of a
// new credential and steps 7 through 10 of verifying an authentication assertion
// See https://www.w3.org/TR/webauthn/#registering-a-new-credential
// and https://www.w3.org/TR/webauthn/#verifying-assertion
//
// Note: the rpTopOriginsVerify parameter does not accept the TopOriginVerificationMode value of
// TopOriginDefaultVerificationMode as it's expected this value is updated by the config validation process.
//
//nolint:gocyclo
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, allowCrossOrigin bool) (err error) {
// Registration Step 3. Verify that the value of C.type is webauthn.create.
// Assertion Step 7. Verify that the value of C.type is the string webauthn.get.
if c.Type != ceremony {
return ErrVerification.WithDetails("Error validating ceremony type").WithInfo(fmt.Sprintf("Expected Value: %s, Received: %s", ceremony, c.Type))
}
// Registration Step 4. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the create() call.
// Assertion Step 8. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the PublicKeyCredentialRequestOptions
// passed to the get() call.
challenge := c.Challenge
if subtle.ConstantTimeCompare([]byte(storedChallenge), []byte(challenge)) != 1 {
return ErrVerification.
WithDetails("Error validating challenge").
WithInfo(fmt.Sprintf("Expected b Value: %#v\nReceived b: %#v\n", storedChallenge, challenge))
}
// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
// the Relying Party's origin.
if !IsOriginInHaystack(c.Origin, rpOrigins) {
return ErrVerification.
WithDetails("Error validating origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, c.Origin))
}
if !allowCrossOrigin && c.CrossOrigin {
return ErrVerification.
WithDetails("Error validating cross origin flag").
WithInfo("The cross origin flag is invalid due to the configuration.")
}
switch len(c.TopOrigin) {
case 0:
break
default:
if !c.CrossOrigin {
return ErrVerification.
WithDetails("Error validating topOrigin").
WithInfo("The topOrigin can't have values unless crossOrigin is true.")
}
var possibleTopOrigins []string
switch rpTopOriginsVerify {
case TopOriginExplicitVerificationMode:
possibleTopOrigins = rpTopOrigins
case TopOriginAutoVerificationMode:
possibleTopOrigins = make([]string, 0, len(rpTopOrigins)+len(rpOrigins))
possibleTopOrigins = append(possibleTopOrigins, rpTopOrigins...)
possibleTopOrigins = append(possibleTopOrigins, rpOrigins...)
case TopOriginImplicitVerificationMode:
possibleTopOrigins = rpOrigins
default:
return ErrNotImplemented.WithDetails("Error handling unknown Top Origin verification mode")
}
if !IsOriginInHaystack(c.TopOrigin, possibleTopOrigins) {
return ErrVerification.
WithDetails("Error validating top origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", possibleTopOrigins, c.TopOrigin))
}
}
// Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status
// matches the state of Token Binding for the TLS connection over which the assertion was
// obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id
// matches the base64url encoding of the Token Binding ID for the connection.
if c.TokenBinding != nil {
if c.TokenBinding.Status == "" {
return ErrParsingData.WithDetails("Error decoding clientData, token binding present without status")
}
if c.TokenBinding.Status != Present && c.TokenBinding.Status != Supported && c.TokenBinding.Status != NotSupported {
return ErrParsingData.
WithDetails("Error decoding clientData, token binding present with invalid status").
WithInfo(fmt.Sprintf("Got: %s", c.TokenBinding.Status))
}
}
// Not yet fully implemented by the spec, browsers, and me.
return nil
}
// TopOriginVerificationMode determines how the Relying Party validates the topOrigin field in
// [CollectedClientData]. This is relevant for cross-origin iframe scenarios where the top-level browsing context's
// origin differs from the embedded origin making the WebAuthn API call.
//
// WebAuthn Level 3.
type TopOriginVerificationMode int
const (
// TopOriginDefaultVerificationMode is the zero value of [TopOriginVerificationMode] and has no matching rule in
// the verifier; passing it directly to [CollectedClientData.Verify] returns an "unknown Top Origin verification
// mode" error. High-level callers using [webauthn.Config] have this value coerced to
// [TopOriginExplicitVerificationMode] by config validation, which is the recommended default.
TopOriginDefaultVerificationMode TopOriginVerificationMode = iota
// TopOriginAutoVerificationMode accepts the Top Origin if it matches any entry in either the allowed Top Origins
// list or the allowed Origins list. The two lists are unioned (RPTopOrigins RPOrigins). This is the most
// permissive of the three active modes and should only be used when an RP deliberately wants cross-origin and
// same-origin embeddings to share an allow-list.
TopOriginAutoVerificationMode
// TopOriginImplicitVerificationMode accepts the Top Origin only if it matches an entry in the allowed Origins
// list (RPOrigins). The RPTopOrigins list is ignored in this mode.
TopOriginImplicitVerificationMode
// TopOriginExplicitVerificationMode accepts the Top Origin only if it matches an entry in the allowed Top Origins
// list (RPTopOrigins). The RPOrigins list is ignored in this mode. This is the strictest mode and the one
// [webauthn.Config] coerces the zero value to.
TopOriginExplicitVerificationMode
)
// IsOriginInHaystack checks if the needle is in the haystack using the mechanism to determine origin equality defined
// in HTML5 Section 5.3 and RFC3986 Section 6.2.1.
//
// Specifically if the needle value has the 'http://' or 'https://' prefix (case-insensitive) and can be parsed as a
// URL; we check each item in the haystack to see if it matches the same rules, and then if the scheme and host (with
// a normalized port) components match case-insensitively then they're considered a match.
//
// If the needle value does not have the 'http://' or 'https://' prefix (case-insensitive) or can't be parsed as a URL
// equality is determined using simple string comparison.
//
// It is important to note that this function completely ignores Apple Associated Domains entirely as Apple is using
// an unassigned Well-Known URI in breech of Well-Known Uniform Resource Identifiers (RFC8615).
//
// See (Origin Definition): https://www.w3.org/TR/2011/WD-html5-20110525/origin-0.html
//
// See (Simple String Comparison Definition): https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.1
//
// See (Apple Associated Domains): https://developer.apple.com/documentation/xcode/supporting-associated-domains
//
// See (IANA Well Known URI Assignments): https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
//
// See (Well-Known Uniform Resource Identifiers): https://datatracker.ietf.org/doc/html/rfc8615
func IsOriginInHaystack(needle string, haystack []string) bool {
needleURI := parseOriginURI(needle)
if needleURI != nil {
for _, hay := range haystack {
if hayURI := parseOriginURI(hay); hayURI != nil {
if isOriginEqual(needleURI, hayURI) {
return true
}
}
}
} else {
for _, hay := range haystack {
if needle == hay {
return true
}
}
}
return false
}
func isOriginEqual(a *url.URL, b *url.URL) bool {
if !strings.EqualFold(a.Scheme, b.Scheme) {
return false
}
if !strings.EqualFold(a.Host, b.Host) {
return false
}
return true
}
func parseOriginURI(raw string) *url.URL {
if !isPossibleFQDN(raw) {
return nil
}
// We can ignore the error here because it's effectively not a FQDN if this fails.
uri, _ := url.Parse(raw)
if uri == nil {
return nil
}
// Normalize the port if necessary.
switch uri.Scheme {
case "http":
if uri.Port() == "80" {
uri.Host = uri.Hostname()
}
case "https":
if uri.Port() == "443" {
uri.Host = uri.Hostname()
}
}
return uri
}
func isPossibleFQDN(raw string) bool {
normalized := strings.ToLower(raw)
return strings.HasPrefix(normalized, "http://") || strings.HasPrefix(normalized, "https://")
}
+450
View File
@@ -0,0 +1,450 @@
package protocol
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVerifyCollectedClientData(t *testing.T) {
testCases := []struct {
name string
origin string
topOrigin string
crossOrigin bool
rpOrigins []string
rpTopOrigins []string
topOriginMode TopOriginVerificationMode
allowCrossOrign bool
ceremony CeremonyType
err string
errType string
errDetails string
errInfo string
}{
{
name: "ShouldSucceed",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
topOriginMode: TopOriginExplicitVerificationMode,
allowCrossOrign: true,
},
{
name: "ShouldSucceedNoTopOrigin",
origin: "http://example.com",
crossOrigin: true,
topOriginMode: TopOriginExplicitVerificationMode,
allowCrossOrign: true,
},
{
name: "ShouldSucceedTopOriginDifferentFromOrigin",
origin: "http://example.com",
topOrigin: "http://example2.com",
crossOrigin: true,
allowCrossOrign: true,
topOriginMode: TopOriginExplicitVerificationMode,
},
{
name: "ShouldFailTopOriginMismatch",
origin: "http://example.com",
topOrigin: "http://example2.com",
crossOrigin: true,
allowCrossOrign: true,
rpTopOrigins: []string{"https://example3.com"},
topOriginMode: TopOriginExplicitVerificationMode,
err: "Error validating top origin",
},
{
name: "ShouldSucceedTopOriginImplicit",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: true,
topOriginMode: TopOriginImplicitVerificationMode,
},
{
name: "ShouldSucceedTopOriginAuto",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: true,
rpTopOrigins: []string{"https://example.com"},
topOriginMode: TopOriginAutoVerificationMode,
},
{
name: "ShouldSucceedMultipleExpectedOrigins",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: true,
rpOrigins: []string{"https://exmaple.com", "9C:B4:AE:EF:05:53:6E:73:0E:C4:B8:02:E7:67:F6:7D:A4:E7:BC:26:D7:42:B5:27:FF:01:7D:68:2A:EB:FA:1D", "http://example.com"},
topOriginMode: TopOriginExplicitVerificationMode,
},
{
name: "ShouldFailTopOriginInvalidMode",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: true,
rpTopOrigins: []string{"https://example.com"},
topOriginMode: -1,
errType: "not_implemented",
errDetails: "Error handling unknown Top Origin verification mode",
},
{
name: "ShouldFailCrossOriginNotAllowed",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: false,
topOriginMode: TopOriginExplicitVerificationMode,
errType: "verification_error",
errDetails: "Error validating cross origin flag",
errInfo: "The cross origin flag is invalid due to the configuration.",
},
{
name: "ShouldFailUnexpectedOrigin",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: true,
rpOrigins: []string{"http://different.com"},
topOriginMode: TopOriginExplicitVerificationMode,
errType: "verification_error",
errDetails: "Error validating origin",
errInfo: "Expected Values: [http://different.com], Received: http://example.com",
},
{
name: "ShouldFailTopOriginWithoutCrossOrigin",
origin: "http://example.com",
topOrigin: "http://example2.com",
crossOrigin: false,
topOriginMode: TopOriginExplicitVerificationMode,
errType: "verification_error",
errDetails: "Error validating topOrigin",
errInfo: "The topOrigin can't have values unless crossOrigin is true.",
},
{
name: "ShouldFailUnexpectedTopOrigin",
origin: "http://example.com",
topOrigin: "http://example.com",
crossOrigin: true,
allowCrossOrign: true,
rpOrigins: []string{"http://example.com"},
rpTopOrigins: []string{"http://different.com"},
topOriginMode: TopOriginExplicitVerificationMode,
err: "Error validating top origin",
},
{
name: "ShouldFailCeremonyMismatch",
origin: "http://example.com",
crossOrigin: false,
topOriginMode: TopOriginExplicitVerificationMode,
ceremony: AssertCeremony,
errType: "verification_error",
errDetails: "Error validating ceremony type",
errInfo: fmt.Sprintf("Expected Value: %s, Received: %s", AssertCeremony, CreateCeremony),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
challenge, err := CreateChallenge()
require.NoError(t, err)
ccd := setupCollectedClientData(challenge, tc.origin, tc.topOrigin, tc.crossOrigin)
rpOrigins := tc.rpOrigins
if rpOrigins == nil {
rpOrigins = []string{ccd.Origin}
}
rpTopOrigins := tc.rpTopOrigins
if rpTopOrigins == nil {
rpTopOrigins = []string{ccd.TopOrigin}
}
ceremony := tc.ceremony
if ceremony == "" {
ceremony = ccd.Type
}
err = ccd.Verify(challenge.String(), ceremony, rpOrigins, rpTopOrigins, tc.topOriginMode, tc.allowCrossOrign)
switch {
case tc.err != "":
assert.EqualError(t, err, tc.err)
case tc.errType != "":
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
default:
assert.NoError(t, err)
}
})
}
}
func TestVerifyCollectedClientData_IncorrectChallenge(t *testing.T) {
challenge, err := CreateChallenge()
require.NoError(t, err)
ccd := setupCollectedClientData(challenge, "http://example.com", "http://example.com", true)
bogusChallenge, err := CreateChallenge()
require.NoError(t, err)
AssertIsProtocolError(t, ccd.Verify(bogusChallenge.String(), ccd.Type, []string{ccd.Origin}, []string{ccd.TopOrigin}, TopOriginExplicitVerificationMode, true), "verification_error", "Error validating challenge", fmt.Sprintf("Expected b Value: \"%s\"\nReceived b: \"%s\"\n", bogusChallenge.String(), challenge.String()))
}
func TestVerifyCollectedClientData_TokenBinding(t *testing.T) {
testCases := []struct {
name string
tokenBinding *TokenBinding
err string
}{
{
name: "ShouldSucceedWithNilTokenBinding",
tokenBinding: nil,
},
{
name: "ShouldSucceedWithPresentStatus",
tokenBinding: &TokenBinding{Status: Present, ID: "abc"},
},
{
name: "ShouldSucceedWithSupportedStatus",
tokenBinding: &TokenBinding{Status: Supported},
},
{
name: "ShouldSucceedWithNotSupportedStatus",
tokenBinding: &TokenBinding{Status: NotSupported},
},
{
name: "ShouldFailWithEmptyStatus",
tokenBinding: &TokenBinding{},
err: "Error decoding clientData, token binding present without status",
},
{
name: "ShouldFailWithInvalidStatus",
tokenBinding: &TokenBinding{Status: "invalid-status"},
err: "Error decoding clientData, token binding present with invalid status",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
newChallenge, err := CreateChallenge()
require.NoError(t, err)
ccd := setupCollectedClientData(newChallenge, "http://example.com", "", false)
ccd.TokenBinding = tc.tokenBinding
err = ccd.Verify(newChallenge.String(), CreateCeremony, []string{ccd.Origin}, nil, TopOriginExplicitVerificationMode, false)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestFullyQualifiedOrigin(t *testing.T) {
testCases := []struct {
name string
have string
expected, expectedErr string
}{
{"ShouldParse", "https://app.example.com", "https://app.example.com", ``},
{"ShouldParseWithPath", "https://app.example.com/apath", "https://app.example.com", ``},
{"ShouldParseWithPort", "https://app.example.com:8443/apath", "https://app.example.com:8443", ``},
{"ShouldParseWithCredentials", "https://user:password@app.example.com/", "https://app.example.com", ``},
{"ShouldParseWithQuery", "https://app.example.com/?abc=123", "https://app.example.com", ``},
{"ShouldParseWithFragment", "https://app.example.com/#abc", "https://app.example.com", ``},
{"ShouldSkipParsingAndroidNative", "android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0", "android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0", ""},
{"ShouldFailToParseMissingScheme", "app.example.com/apath", "", `parse "app.example.com/apath": invalid URI for request`},
{"ShouldFailToParseBlankScheme", "://app.example.com/apath", "", `parse "://app.example.com/apath": missing protocol scheme`},
{"ShouldFailToParseMissingHost", "https:///apath", "", `url 'https:///apath' does not have a host`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, actualErr := FullyQualifiedOrigin(tc.have)
assert.Equal(t, tc.expected, actual)
if tc.expectedErr == "" {
assert.NoError(t, actualErr)
} else {
assert.EqualError(t, actualErr, tc.expectedErr)
}
})
}
}
func TestIsOriginInHaystack(t *testing.T) {
testCases := []struct {
name string
origin string
haystack []string
expected bool
}{
{
"ShouldHandleFullyQualifiedOrigin",
"https://app.example.com",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginCaseInsensitiveScheme",
"https://app.example.com",
[]string{"HTTPS://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginCaseInsensitiveHost",
"https://app.EXAMPLE.com",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginWithPort",
"https://app.example.com:443",
[]string{"https://app.example.com:443"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDifferentScheme",
"http://app.example.com",
[]string{"https://app.example.com"},
false,
},
{
"ShouldHandleFullyQualifiedOriginDifferentPort",
"https://app.example.com:443",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDifferentPortNotMatchingScheme",
"https://app.example.com:80",
[]string{"https://app.example.com"},
false,
},
{
"ShouldHandleFullyQualifiedOriginDifferentPath",
"https://app.example.com/abc",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDifferentQuery",
"https://app.example.com/?abc=123",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDifferentQueryCount",
"https://app.example.com/?abc=123",
[]string{"https://app.example.com/?zyz=123&abc=123"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDifferentQueryOrder",
"https://app.example.com/?abc=123&xyz=123",
[]string{"https://app.example.com/?xyz=123&abc=123"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDifferentQueryValue",
"https://app.example.com/?abc=123&xyz=123",
[]string{"https://app.example.com/?xyz=1234&abc=123"},
true,
},
{
"ShouldHandleFullyQualifiedOriginFragment",
"https://app.example.com/#abc",
[]string{"https://app.example.com/#abc"},
true,
},
{
"ShouldHandleFullyQualifiedOriginFragmentDifferent",
"https://app.example.com/#abc",
[]string{"https://app.example.com/#abc2"},
true,
},
{
"ShouldHandleFullyQualifiedOriginWithoutAllowed",
"https://app.example.com",
nil,
false,
},
{
"ShouldHandleFullyQualifiedOriginWithTrailingSlashes",
"https://app.example.com/",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleNativeAppAndroid",
"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0",
[]string{"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0"},
true,
},
{
"ShouldHandleNativeAppAndroidCaseSensitive",
"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69F0",
[]string{"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0"},
false,
},
{
"ShouldHandleNonFQDNOrigin",
"https://user:password@app.example.com/",
[]string{"https://app.example.com/"},
true,
},
{
"ShouldHandleNonFQDNOriginExactStringMatch",
"https://user:password@app.example.com/",
[]string{"https://user:password@app.example.com/"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDefaultPortEquivalentHTTPS",
"https://app.example.com:443",
[]string{"https://app.example.com"},
true,
},
{
"ShouldHandleFullyQualifiedOriginDefaultPortEquivalentHTTP",
"http://app.example.com:80",
[]string{"http://app.example.com"},
true,
},
{
"ShouldHandleInvalidURLAsSimpleStringMatch",
"http://app.example.%%%&123?1",
[]string{"http://app.example.%%%&123?1"},
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, IsOriginInHaystack(tc.origin, tc.haystack))
})
}
}
func setupCollectedClientData(challenge URLEncodedBase64, origin, topOrigin string, crossOrigin bool) *CollectedClientData {
ccd := &CollectedClientData{
Type: CreateCeremony,
Origin: origin,
TopOrigin: topOrigin,
CrossOrigin: crossOrigin,
Challenge: challenge.String(),
}
return ccd
}
+239
View File
@@ -0,0 +1,239 @@
package protocol
import (
"encoding/asn1"
)
const (
none = "none"
stmtFmtNone = none
stmtTypNone = none
stmtAttStmt = "attStmt"
stmtFmt = "fmt"
stmtX5C = "x5c"
stmtSignature = "sig"
stmtAlgorithm = "alg"
stmtVersion = "ver"
stmtECDAAKID = "ecdaaKeyId"
stmtCertInfo = "certInfo"
stmtPubArea = "pubArea"
)
const (
versionTPM20 = "2.0"
)
const (
attStatementAndroidSafetyNetHostname = "attest.android.com"
)
const (
// MinimumChallengeLength defines the minimum length of the challenge.
MinimumChallengeLength = 16
// DefaultChallengeLength defines the default length of the challenge.
DefaultChallengeLength = 32
)
var (
// internalRemappedAuthenticatorTransport handles remapping of AuthenticatorTransport values. Specifically it is
// intentional on remapping only transports that never made recommendation but are being used in the wild. It
// should not be used to handle transports that were ratified.
internalRemappedAuthenticatorTransport = map[string]AuthenticatorTransport{
// The Authenticator Transport 'hybrid' was previously named 'cable'; even if it was for a short period.
"cable": Hybrid,
}
)
const (
/*
Apple Anonymous Attestation Root 1 in PEM form.
Source: https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
SHA256 Fingerprints:
Root 1: 09:15:DD:5C:07:A2:8D:B5:49:D1:F6:77:BB:5A:75:D4:BF:BE:95:61:A7:73:42:43:27:76:2E:9E:02:F9:BB:29
*/
certificateAppleRoot1 = `-----BEGIN CERTIFICATE-----
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
1bWeT0vT
-----END CERTIFICATE-----`
)
const (
/*
Google Hardware Attestation Root 1 through Root 5 in PEM form.
Source: https://developer.android.com/training/articles/security-key-attestation#root_certificate
SHA256 Fingerprints:
Root 1: CE:DB:1C:B6:DC:89:6A:E5:EC:79:73:48:BC:E9:28:67:53:C2:B3:8E:E7:1C:E0:FB:E3:4A:9A:12:48:80:0D:FC
Root 2: 6D:9D:B4:CE:6C:5C:0B:29:31:66:D0:89:86:E0:57:74:A8:77:6C:EB:52:5D:9E:43:29:52:0D:E1:2B:A4:BC:C0
Root 3: C1:98:4A:3E:F4:5C:1E:2A:91:85:51:DE:10:60:3C:86:F7:05:1B:22:49:C4:89:1C:AE:32:30:EA:BD:0C:97:D5
Root 4: 1E:F1:A0:4B:8B:A5:8A:B9:45:89:AC:49:8C:89:82:A7:83:F2:4E:A7:30:7E:01:59:A0:C3:A7:3B:37:7D:87:CC
Root 5: AB:66:41:17:8A:36:E1:79:AA:0C:1C:DD:DF:9A:16:EB:45:FA:20:94:3E:2B:8C:D7:C7:C0:5C:26:CF:8B:48:7A
*/
certificateAndroidKeyRoot1 = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgw
NzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7
174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGIC
W/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2G
tkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkx
oSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG
1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mF
mr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPz
lHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVw
n6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1Eu
zbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHo
vaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHn
w1IdYIg2Wxg7yHcQZemFQg==
-----END CERTIFICATE-----`
certificateAndroidKeyRoot2 = `-----BEGIN CERTIFICATE-----
MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc
MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET
MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4
WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex
MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG
EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt
G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5
Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA
MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH
EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY
uR2zh/80lQyu9vAFCj6E4AXc+osmRg==
-----END CERTIFICATE-----`
certificateAndroidKeyRoot3 = `-----BEGIN CERTIFICATE-----
MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
-----END CERTIFICATE-----`
certificateAndroidKeyRoot4 = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
ex0SdDrx+tWUDqG8At2JHA==
-----END CERTIFICATE-----`
certificateAndroidKeyRoot5 = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJAMNrfES5rhgxMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjExMTE3MjMxMDQyWhcNMzYxMTEzMjMx
MDQyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBTNNZe5cuf8oiq+jV0itTG
zWVhSTjOBEk2FQvh11J3o3lna0o7rd8RFHnN00q4hi6TapFhh4qaw/iG6Xg+xOan
63niLWIC5GOPFgPeYXM9+nBb3zZzC8ABypYuCusWCmt6Tn3+Pjbz3MTVhRGXuT/T
QH4KGFY4PhvzAyXwdjTOCXID+aHud4RLcSySr0Fq/L+R8TWalvM1wJJPhyRjqRCJ
erGtfBagiALzvhnmY7U1qFcS0NCnKjoO7oFedKdWlZz0YAfu3aGCJd4KHT0MsGiL
Zez9WP81xYSrKMNEsDK+zK5fVzw6jA7cxmpXcARTnmAuGUeI7VVDhDzKeVOctf3a
0qQLwC+d0+xrETZ4r2fRGNw2YEs2W8Qj6oDcfPvq9JySe7pJ6wcHnl5EZ0lwc4xH
7Y4Dx9RA1JlfooLMw3tOdJZH0enxPXaydfAD3YifeZpFaUzicHeLzVJLt9dvGB0b
HQLE4+EqKFgOZv2EoP686DQqbVS1u+9k0p2xbMA105TBIk7npraa8VM0fnrRKi7w
lZKwdH+aNAyhbXRW9xsnODJ+g8eF452zvbiKKngEKirK5LGieoXBX7tZ9D1GNBH2
Ob3bKOwwIWdEFle/YF/h6zWgdeoaNGDqVBrLr2+0DtWoiB1aDEjLWl9FmyIUyUm7
mD/vFDkzF+wm7cyWpQpCVQ==
-----END CERTIFICATE-----`
)
var (
oidExtensionAppleAnonymousAttestation = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
oidExtensionAndroidKeystore = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}
oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
oidExtensionBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19}
oidFIDOGenCeAAGUID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
oidMicrosoftKpPrivacyCA = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 21, 36}
oidTCGKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
oidTCGAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
oidTCGAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
oidTCGAtTPMVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
)
+46
View File
@@ -0,0 +1,46 @@
package protocol
const (
testAttTypeSome = "some-att-type"
)
const (
certificateAndroidKeyIntermediateFAKE1 = `-----BEGIN CERTIFICATE-----
MIIC6jCCApGgAwIBAgIBAjAKBggqhkjOPQQDAjCBxjE9MDsGA1UEAww0RkFLRSBB
bmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3QgRkFLRTEx
MC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9y
ZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQG
EwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0xODA1MDkx
MjMxNDRaFw00NTA5MjQxMjMxNDRaMIHOMUUwQwYDVQQDDDxGQUtFIEFuZHJvaWQg
S2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gSW50ZXJtZWRpYXRlIEZBS0Ux
MTAvBgkqhkiG9w0BCQEWImNvbmZvcm1hbmNlLXRvb2xzQGZpZG9hbGxpYW5jZS5v
cmcxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxDDAKBgNVBAsMA0NXRzELMAkGA1UE
BhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAASrUGErYk0Xu8O1GwRJOwVJC4wfi52883my3tygfFKh
17YN0yF13Ct+3bwm2wjVX4b2cbaU3DBNpKKKjE4DpvXHo2YwZDASBgNVHRMBAf8E
CDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIChDAdBgNVHQ4EFgQUo9KqLO8NjPIkAtUc
tGC8v2pbJBQwHwYDVR0jBBgwFoAUUpobMuBWqs1RD+9fgDcGi/KRIx0wCgYIKoZI
zj0EAwIDRwAwRAIgad2eo/GB+0JKOa0aCt+50uMz14b+uUVgVfo9zJl4udICID7T
D6b3BrVW6RQkKrBm8ocT3ZI4vJbXGF0FXzIXVKUW
-----END CERTIFICATE-----`
certificateAndroidKeyIntermediateFAKE2 = `-----BEGIN CERTIFICATE-----
MIIDFDCCArqgAwIBAgIBAjAKBggqhkjOPQQDAjCB3DE9MDsGA1UEAww0RkFLRSBB
bmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3QgRkFLRTEx
MC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9y
ZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEiMCAGA1UECwwZQXV0aGVudGljYXRv
ciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQH
DAlXYWtlZmllbGQwHhcNMTkwNDI1MDU0OTMyWhcNNDYwOTEwMDU0OTMyWjCB5DFF
MEMGA1UEAww8RkFLRSBBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0
aW9uIEludGVybWVkaWF0ZSBGQUtFMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5j
ZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNl
MSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJV
UzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABKtQYStiTRe7w7UbBEk7BUkLjB+LnbzzebLe3KB8UqHXtg3T
IXXcK37dvCbbCNVfhvZxtpTcME2kooqMTgOm9cejYzBhMA8GA1UdEwEB/wQFMAMB
Af8wDgYDVR0PAQH/BAQDAgKEMB0GA1UdDgQWBBSj0qos7w2M8iQC1Ry0YLy/alsk
FDAfBgNVHSMEGDAWgBRSmhsy4FaqzVEP71+ANwaL8pEjHTAKBggqhkjOPQQDAgNI
ADBFAiEAsW8uQC+0es5tOY3w/T7IshPj3o//B5IQRsHq8IlZKH0CIG75Q6isJ4tw
XhaLE4b0TkuLadd7i4zarqZsoaSWXy75
-----END CERTIFICATE-----`
)
+283
View File
@@ -0,0 +1,283 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"io"
"net/http"
"github.com/go-webauthn/webauthn/metadata"
)
// Credential is the basic credential type from the Credential Management specification that is inherited by WebAuthn's
// PublicKeyCredential type.
//
// Specification: Credential Management §2.2. The Credential Interface (https://www.w3.org/TR/credential-management/#credential)
type Credential struct {
// ID is The credentials identifier. The requirements for the
// identifier are distinct for each type of credential. It might
// represent a username for username/password tuples, for example.
ID string `json:"id"`
// Type is the value of the objects interface object's [[type]] slot,
// which specifies the credential type represented by this object.
// This should be type "public-key" for Webauthn credentials.
Type string `json:"type"`
}
// ParsedCredential is the parsed PublicKeyCredential interface, inherits from Credential, and contains
// the attributes that are returned to the caller when a new credential is created, or a new assertion is requested.
type ParsedCredential struct {
ID string `cbor:"id"`
Type string `cbor:"type"`
}
// PublicKeyCredential represents the IDL of the same name and contains the raw response returned to the Relying Party
// from the client's call to navigator.credentials.create() or navigator.credentials.get().
//
// Specification: §5.1. PublicKeyCredential Interface (https://www.w3.org/TR/webauthn/#iface-pkcredential)
type PublicKeyCredential struct {
Credential
RawID URLEncodedBase64 `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
AuthenticatorAttachment string `json:"authenticatorAttachment,omitempty"`
}
// ParsedPublicKeyCredential is the parsed form of [PublicKeyCredential] with typed fields.
type ParsedPublicKeyCredential struct {
ParsedCredential
RawID []byte `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
}
// CredentialCreationResponse is the raw response returned to the Relying Party from the client for a credential
// registration ceremony. It contains the [AuthenticatorAttestationResponse] which holds the attestation object
// and client data.
//
// Specification: §5.4. Options for Credential Creation (https://www.w3.org/TR/webauthn/#sctn-credentialcreationoptions-extension)
type CredentialCreationResponse struct {
PublicKeyCredential
AttestationResponse AuthenticatorAttestationResponse `json:"response"`
}
// ParsedCredentialCreationData is the parsed form of [CredentialCreationResponse]. It is the result of parsing the
// raw response from the authenticator and can be used with [ParsedCredentialCreationData.Verify] to complete the
// registration ceremony verification.
type ParsedCredentialCreationData struct {
ParsedPublicKeyCredential
Response ParsedAttestationResponse
Raw CredentialCreationResponse
}
// ParseCredentialCreationResponse parses a registration/attestation response from a [*http.Request]. The request body
// is automatically drained and closed after parsing.
//
// This is the standard entry point when using [net/http]. For implementations that don't use [net/http], see
// [ParseCredentialCreationResponseBody] (accepts an [io.Reader]) or [ParseCredentialCreationResponseBytes] (accepts a
// []byte).
func ParseCredentialCreationResponse(request *http.Request) (*ParsedCredentialCreationData, error) {
if request == nil || request.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
defer func() {
_, _ = io.Copy(io.Discard, request.Body)
_ = request.Body.Close()
}()
return ParseCredentialCreationResponseBody(request.Body)
}
// ParseCredentialCreationResponseBody parses a registration/attestation response from an [io.Reader]. The caller is
// responsible for closing the reader if applicable.
//
// This is the framework-agnostic variant of [ParseCredentialCreationResponse]. For a [*http.Request] use
// [ParseCredentialCreationResponse] instead. For raw bytes use [ParseCredentialCreationResponseBytes].
func ParseCredentialCreationResponseBody(body io.Reader) (pcc *ParsedCredentialCreationData, err error) {
var ccr CredentialCreationResponse
if err = decodeBody(body, &ccr); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error()).WithError(err)
}
return ccr.Parse()
}
// ParseCredentialCreationResponseBytes parses a registration/attestation response from raw bytes.
//
// See also [ParseCredentialCreationResponse] (for [*http.Request]) and [ParseCredentialCreationResponseBody] (for
// [io.Reader]).
func ParseCredentialCreationResponseBytes(data []byte) (pcc *ParsedCredentialCreationData, err error) {
var ccr CredentialCreationResponse
if err = decodeBytes(data, &ccr); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error()).WithError(err)
}
return ccr.Parse()
}
// Parse validates and parses the CredentialCreationResponse into a ParsedCredentialCreationData. This receiver
// is unlikely to be expressly guaranteed under the versioning policy. Users looking for this guarantee should see
// ParseCredentialCreationResponseBody instead, and this receiver should only be used if that function is inadequate
// for their use case.
func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData, err error) {
if ccr.ID == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID")
}
testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID)
if err != nil || len(testB64) == 0 {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("ID not base64.RawURLEncoded")
}
if ccr.Type == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type")
}
if ccr.Type != string(PublicKeyCredentialType) {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
}
response, err := ccr.AttestationResponse.Parse()
if err != nil {
return nil, ErrParsingData.WithDetails("Error parsing attestation response")
}
var attachment AuthenticatorAttachment
switch ccr.AuthenticatorAttachment {
case "platform":
attachment = Platform
case "cross-platform":
attachment = CrossPlatform
}
return &ParsedCredentialCreationData{
ParsedPublicKeyCredential{
ParsedCredential{ccr.ID, ccr.Type}, ccr.RawID, ccr.ClientExtensionResults, attachment,
},
*response,
ccr,
}, nil
}
// Verify the Client and Attestation data.
//
// Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential)
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, allowCrossOrigin, verifyUser, verifyUserPresence bool, mds metadata.Provider, credParams []CredentialParameter) (clientDataHash []byte, err error) {
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data.
if err = pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify, allowCrossOrigin); err != nil {
return nil, err
}
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
sum := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
clientDataHash = sum[:]
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
// structure to obtain the attestation statement format fmt, the authenticator data authData, and the
// attestation statement attStmt.
// We do the above step while parsing and decoding the CredentialCreationResponse
// Handle steps 9 through 14 - This verifies the attestation object.
if err = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash, verifyUser, verifyUserPresence, mds, credParams); err != nil {
return clientDataHash, err
}
// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
// one way to obtain such information, using the AAGUID in the attestedCredentialData in authData.
// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
// the future, however.
// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
// the set of acceptable trust anchors obtained in step 15.
// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
// attestation public key correctly chains up to an acceptable root certificate.
// TODO: We're not supporting trust anchors, self-attestation policy, or acceptable root certs yet.
// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
// requested for a credential that is already registered to a different user, the Relying Party SHOULD
// fail this registration ceremony, or it MAY decide to accept the registration, i.e. while deleting
// the older registration.
// TODO: We can't support this in the code's current form, the Relying Party would need to check for this
// against their database.
// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
// register the new credential with the account that was denoted in the options.user passed to create(), by
// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
// appropriate for the Relying Party's system.
// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
// the Relying Party SHOULD fail the registration ceremony.
// TODO: Not implemented for the reasons mentioned under Step 16.
return clientDataHash, nil
}
// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
//
// 1. Check that the Session Data's AuthenticationExtensions has been provided and if it hasn't return an error.
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return an empty string if it doesn't.
// 3. Check that the Credential AttestationFormat is `fido-u2f` and return an empty string if it isn't.
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and if it doesn't return an empty string.
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and if it isn't return an error.
// 6. Check that the appid output is true and if it isn't return an empty string.
// 7. Check that the Session Data has an appid extension defined and if it doesn't return an error.
// 8. Check that the appid extension in Session Data is a string and if it isn't return an error.
// 9. Return the appid extension value from the Session data.
func (ppkc ParsedPublicKeyCredential) GetAppID(authExt AuthenticationExtensions, credentialAttestationFormat string) (appID string, err error) {
var (
value, clientValue interface{}
enableAppID, ok bool
)
if authExt == nil {
return "", nil
}
if ppkc.ClientExtensionResults == nil {
return "", nil
}
// If the credential is not in the fido-u2f attestation FORMAT it is assumed to NOT be a fido-u2f credential.
// https://www.w3.org/TR/webauthn/#sctn-fido-u2f-attestation
if credentialAttestationFormat != string(AttestationFormatFIDOUniversalSecondFactor) {
return "", nil
}
if clientValue, ok = ppkc.ClientExtensionResults[ExtensionAppID]; !ok {
return "", nil
}
if enableAppID, ok = clientValue.(bool); !ok {
return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type")
}
if !enableAppID {
return "", nil
}
if value, ok = authExt[ExtensionAppID]; !ok {
return "", ErrBadRequest.WithDetails("Session Data does not have an appid but Client Output indicates it should be set")
}
if appID, ok = value.(string); !ok {
return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type")
}
return appID, nil
}
+552
View File
@@ -0,0 +1,552 @@
package protocol
import (
"bytes"
"encoding/base64"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func TestParseCredentialCreationResponse(t *testing.T) {
type args struct {
responseName string
}
byteID, _ := base64.RawURLEncoding.DecodeString("6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g")
byteAuthData, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw")
byteRPIDHash, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")
byteCredentialPubKey, _ := base64.RawURLEncoding.DecodeString("pSJYIMfCKfxl2SvnqJIiHQysHmpmITNgtCkQ5ESExSRjqrhXAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNc")
byteAttObject, _ := base64.RawURLEncoding.DecodeString("o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw")
byteClientDataJSON, _ := base64.RawURLEncoding.DecodeString("eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ")
testCases := []struct {
name string
args args
expected *ParsedCredentialCreationData
err string
errType string
errDetails string
errInfo string
}{
{
name: "ShouldParseCredentialRequest",
args: args{
responseName: "success",
},
expected: &ParsedCredentialCreationData{
ParsedPublicKeyCredential: ParsedPublicKeyCredential{
ParsedCredential: ParsedCredential{
ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
Type: string(PublicKeyCredentialType),
},
RawID: byteID,
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
"appid": true,
},
AuthenticatorAttachment: Platform,
},
Response: ParsedAttestationResponse{
CollectedClientData: CollectedClientData{
Type: CeremonyType("webauthn.create"),
Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE",
Origin: "https://webauthn.io",
},
AttestationObject: AttestationObject{
Format: "none",
RawAuthData: byteAuthData,
AuthData: AuthenticatorData{
RPIDHash: byteRPIDHash,
Counter: 0,
Flags: 0x041,
AttData: AttestedCredentialData{
AAGUID: make([]byte, 16),
CredentialID: byteID,
CredentialPublicKey: byteCredentialPubKey,
},
},
},
Transports: []AuthenticatorTransport{USB, NFC, "fake"},
},
Raw: CredentialCreationResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
Type: string(PublicKeyCredentialType),
ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
},
RawID: byteID,
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
"appid": true,
},
AuthenticatorAttachment: "platform",
},
AttestationResponse: AuthenticatorAttestationResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: byteClientDataJSON,
},
AttestationObject: byteAttObject,
Transports: []string{"usb", "nfc", "fake"},
},
},
},
err: "",
},
{
name: "ShouldHandleTrailingData",
args: args{
responseName: "trailingData",
},
expected: nil,
err: "Parse error for Registration",
errType: "invalid_request",
errDetails: "Parse error for Registration",
errInfo: "body contains trailing data",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, subtest := range []string{"Response", "ResponseBody", "Bytes"} {
t.Run(subtest, func(t *testing.T) {
var (
actual *ParsedCredentialCreationData
err error
)
switch subtest {
case "Response":
body := io.NopCloser(bytes.NewReader([]byte(testCredentialRequestResponses[tc.args.responseName])))
request := &http.Request{
Body: body,
}
actual, err = ParseCredentialCreationResponse(request)
case "ResponseBody":
body := io.NopCloser(bytes.NewReader([]byte(testCredentialRequestResponses[tc.args.responseName])))
actual, err = ParseCredentialCreationResponseBody(body)
case "Bytes":
body := []byte(testCredentialRequestResponses[tc.args.responseName])
actual, err = ParseCredentialCreationResponseBytes(body)
}
if tc.err != "" {
assert.EqualError(t, err, tc.err)
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
return
}
assert.Equal(t, tc.expected.ClientExtensionResults, actual.ClientExtensionResults)
assert.Equal(t, tc.expected.ID, actual.ID)
assert.Equal(t, tc.expected.Type, actual.Type)
assert.Equal(t, tc.expected.ParsedCredential, actual.ParsedCredential)
assert.Equal(t, tc.expected.ParsedPublicKeyCredential, actual.ParsedPublicKeyCredential)
assert.Equal(t, tc.expected.Raw, actual.Raw)
assert.Equal(t, tc.expected.RawID, actual.RawID)
assert.Equal(t, tc.expected.Response.Transports, actual.Response.Transports)
assert.Equal(t, tc.expected.Response.CollectedClientData, actual.Response.CollectedClientData)
assert.Equal(t, tc.expected.Response.AttestationObject.AuthData.AttData.CredentialID, actual.Response.AttestationObject.AuthData.AttData.CredentialID)
assert.Equal(t, tc.expected.Response.AttestationObject.Format, actual.Response.AttestationObject.Format)
var pkExpected, pkActual any
pkBytesExpected := tc.expected.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
assert.NoError(t, webauthncbor.Unmarshal(pkBytesExpected, &pkExpected))
pkBytesActual := actual.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
assert.NoError(t, webauthncbor.Unmarshal(pkBytesActual, &pkActual))
assert.Equal(t, pkExpected, pkActual)
})
}
})
}
}
func TestParsedCredentialCreationData_Verify(t *testing.T) {
byteID, _ := base64.RawURLEncoding.DecodeString("6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g")
byteChallenge, _ := base64.RawURLEncoding.DecodeString("W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE")
byteAuthData, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw")
byteRPIDHash, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")
byteCredentialPubKey, _ := base64.RawURLEncoding.DecodeString("pSJYIMfCKfxl2SvnqJIiHQysHmpmITNgtCkQ5ESExSRjqrhXAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNc")
byteAttObject, _ := base64.RawURLEncoding.DecodeString("o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw")
byteClientDataJSON, _ := base64.RawURLEncoding.DecodeString("eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ")
type fields struct {
ParsedPublicKeyCredential ParsedPublicKeyCredential
Response ParsedAttestationResponse
Raw CredentialCreationResponse
}
type args struct {
storedChallenge URLEncodedBase64
verifyUser bool
relyingPartyID string
relyingPartyOrigin []string
credParams []CredentialParameter
}
testCases := []struct {
name string
fields fields
args args
expected []byte
err string
}{
{
name: "SuccessfulVerificationTest",
fields: fields{
ParsedPublicKeyCredential: ParsedPublicKeyCredential{
ParsedCredential: ParsedCredential{
ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
Type: string(PublicKeyCredentialType),
},
RawID: byteID,
},
Response: ParsedAttestationResponse{
CollectedClientData: CollectedClientData{
Type: CeremonyType("webauthn.create"),
Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE",
Origin: "https://webauthn.io",
},
AttestationObject: AttestationObject{
Format: "none",
RawAuthData: byteAuthData,
AuthData: AuthenticatorData{
RPIDHash: byteRPIDHash,
Counter: 0,
Flags: 0x041,
AttData: AttestedCredentialData{
AAGUID: make([]byte, 16),
CredentialID: byteID,
CredentialPublicKey: byteCredentialPubKey,
},
},
},
},
Raw: CredentialCreationResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
Type: string(PublicKeyCredentialType),
ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
},
RawID: byteID,
},
AttestationResponse: AuthenticatorAttestationResponse{
AuthenticatorResponse: AuthenticatorResponse{
ClientDataJSON: byteClientDataJSON,
},
AttestationObject: byteAttObject,
},
},
},
args: args{
storedChallenge: URLEncodedBase64(byteChallenge),
verifyUser: false,
relyingPartyID: `webauthn.io`,
relyingPartyOrigin: []string{`https://webauthn.io`},
credParams: []CredentialParameter{{Type: "public-key", Algorithm: webauthncose.AlgES256}},
},
expected: []byte{0xa, 0xaf, 0x43, 0xda, 0x7e, 0xd3, 0x94, 0x98, 0x9b, 0xbc, 0x47, 0xcb, 0x0, 0x72, 0x6b, 0xbc, 0xf3, 0xa2, 0x4a, 0x49, 0x5f, 0x84, 0x4f, 0x45, 0x97, 0x91, 0x6a, 0x2d, 0xff, 0x47, 0xbc, 0xad},
err: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pcc := &ParsedCredentialCreationData{
ParsedPublicKeyCredential: tc.fields.ParsedPublicKeyCredential,
Response: tc.fields.Response,
Raw: tc.fields.Raw,
}
actual, err := pcc.Verify(tc.args.storedChallenge.String(), tc.args.relyingPartyID, tc.args.relyingPartyOrigin, nil, TopOriginExplicitVerificationMode, false, tc.args.verifyUser, false, nil, tc.args.credParams)
assert.Equal(t, tc.expected, actual)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestParseCredentialCreationResponse_NilRequest(t *testing.T) {
testCases := []struct {
name string
request *http.Request
err string
}{
{
name: "ShouldFailNilRequest",
request: nil,
err: "No response given",
},
{
name: "ShouldFailNilBody",
request: &http.Request{},
err: "No response given",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParseCredentialCreationResponse(tc.request)
assert.Nil(t, result)
assert.EqualError(t, err, tc.err)
})
}
}
func TestCredentialCreationResponse_Parse_Errors(t *testing.T) {
testCases := []struct {
name string
ccr CredentialCreationResponse
err string
}{
{
name: "ShouldFailMissingID",
ccr: CredentialCreationResponse{},
err: "Parse error for Registration",
},
{
name: "ShouldFailIDNotBase64",
ccr: CredentialCreationResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "not valid base64 %%%",
},
},
},
err: "Parse error for Registration",
},
{
name: "ShouldFailMissingType",
ccr: CredentialCreationResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "dGVzdA",
},
},
},
err: "Parse error for Registration",
},
{
name: "ShouldFailBadType",
ccr: CredentialCreationResponse{
PublicKeyCredential: PublicKeyCredential{
Credential: Credential{
ID: "dGVzdA",
Type: "bad-type",
},
},
},
err: "Parse error for Registration",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := tc.ccr.Parse()
assert.Nil(t, result)
assert.EqualError(t, err, tc.err)
})
}
}
func TestGetAppID(t *testing.T) {
testCases := []struct {
name string
ppkc ParsedPublicKeyCredential
authExt AuthenticationExtensions
credentialAttestationFormat string
expectedAppID string
err string
}{
{
name: "ShouldReturnEmptyWhenAuthExtNil",
ppkc: ParsedPublicKeyCredential{},
authExt: nil,
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
expectedAppID: "",
},
{
name: "ShouldReturnEmptyWhenClientExtNil",
ppkc: ParsedPublicKeyCredential{},
authExt: AuthenticationExtensions{ExtensionAppID: "https://example.com"},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
expectedAppID: "",
},
{
name: "ShouldReturnEmptyWhenNotFIDOU2F",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
ExtensionAppID: true,
},
},
authExt: AuthenticationExtensions{ExtensionAppID: "https://example.com"},
credentialAttestationFormat: "packed",
expectedAppID: "",
},
{
name: "ShouldReturnEmptyWhenAppIDNotInClientExt",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
"other": "value",
},
},
authExt: AuthenticationExtensions{ExtensionAppID: "https://example.com"},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
expectedAppID: "",
},
{
name: "ShouldFailWhenClientAppIDNotBool",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
ExtensionAppID: "not-a-bool",
},
},
authExt: AuthenticationExtensions{ExtensionAppID: "https://example.com"},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
err: "Client Output appid did not have the expected type",
},
{
name: "ShouldReturnEmptyWhenAppIDFalse",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
ExtensionAppID: false,
},
},
authExt: AuthenticationExtensions{ExtensionAppID: "https://example.com"},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
expectedAppID: "",
},
{
name: "ShouldFailWhenSessionAppIDMissing",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
ExtensionAppID: true,
},
},
authExt: AuthenticationExtensions{},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
err: "Session Data does not have an appid but Client Output indicates it should be set",
},
{
name: "ShouldFailWhenSessionAppIDNotString",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
ExtensionAppID: true,
},
},
authExt: AuthenticationExtensions{ExtensionAppID: 123},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
err: "Session Data appid did not have the expected type",
},
{
name: "ShouldReturnAppID",
ppkc: ParsedPublicKeyCredential{
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
ExtensionAppID: true,
},
},
authExt: AuthenticationExtensions{ExtensionAppID: "https://example.com"},
credentialAttestationFormat: string(AttestationFormatFIDOUniversalSecondFactor),
expectedAppID: "https://example.com",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
appID, err := tc.ppkc.GetAppID(tc.authExt, tc.credentialAttestationFormat)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedAppID, appID)
}
})
}
}
var testCredentialRequestResponses = map[string]string{
`success`: `
{
"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"type":"public-key",
"authenticatorAttachment":"platform",
"clientExtensionResults":{
"appid":true
},
"response":{
"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
"transports":["usb","nfc","fake"]
}
}
`,
`trailingData`: `
{
"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"type":"public-key",
"authenticatorAttachment":"platform",
"clientExtensionResults":{
"appid":true
},
"response":{
"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
"transports":["usb","nfc","fake"]
}
}
trailing
`,
`successDeprecatedTransports`: `
{
"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"type":"public-key",
"authenticatorAttachment":"not-valid",
"transports":["usb","nfc","fake"],
"clientExtensionResults":{
"appid":true
},
"response":{
"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
}
}
`,
`successDeprecatedTransportsAndNew`: `
{
"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
"type":"public-key",
"authenticatorAttachment":"cross-platform",
"transports":["usb","nfc","fake"],
"clientExtensionResults":{
"appid":true
},
"response":{
"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
"transports":["usb","nfc"]
}
}
`,
}
+40
View File
@@ -0,0 +1,40 @@
package protocol
import (
"bytes"
"encoding/json"
"errors"
"io"
)
func decodeBody(body io.Reader, v any) (err error) {
decoder := json.NewDecoder(body)
if err = decoder.Decode(v); err != nil {
return err
}
_, err = decoder.Token()
if !errors.Is(err, io.EOF) {
return errors.New("body contains trailing data")
}
return nil
}
func decodeBytes(data []byte, v any) (err error) {
decoder := json.NewDecoder(bytes.NewReader(data))
if err = decoder.Decode(v); err != nil {
return err
}
_, err = decoder.Token()
if !errors.Is(err, io.EOF) {
return errors.New("body contains trailing data")
}
return nil
}
+8
View File
@@ -0,0 +1,8 @@
// Package protocol contains data structures and validation functionality
// outlined in the Web Authentication specification (https://www.w3.org/TR/webauthn).
// The data structures here attempt to conform as much as possible to their definitions,
// but some structs (like those that are used as part of validation steps) contain
// additional fields that help us unpack and validate the data we unmarshall.
// When implementing this library, most developers will primarily be using the API
// outlined in the webauthn package.
package protocol
+46
View File
@@ -0,0 +1,46 @@
package protocol
// CredentialEntity represents the PublicKeyCredentialEntity IDL and it describes a user account, or a WebAuthn Relying
// Party with which a public key credential is associated.
//
// Specification: §5.4.1. Public Key Entity Description (https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity)
type CredentialEntity struct {
// A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents:
//
// When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the Relying Party,
// intended only for display. For example, "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех".
//
// When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a user account. It is
// intended only for display, i.e., aiding the user in determining the difference between user accounts with similar
// displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234".
Name string `json:"name"`
}
// The RelyingPartyEntity represents the PublicKeyCredentialRpEntity IDL and is used to supply additional Relying Party
// attributes when creating a new credential.
//
// Specification: §5.4.2. Relying Party Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dictionary-rp-credential-params)
type RelyingPartyEntity struct {
CredentialEntity
// A unique identifier for the Relying Party entity, which sets the RP ID.
ID string `json:"id"`
}
// The UserEntity represents the PublicKeyCredentialUserEntity IDL and is used to supply additional user account
// attributes when creating a new credential.
//
// Specification: §5.4.3 User Account Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity)
type UserEntity struct {
CredentialEntity
// A human-palatable name for the user account, intended only for display.
// For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let
// the user choose this, and SHOULD NOT restrict the choice more than necessary.
DisplayName string `json:"displayName"`
// ID is the user handle of the user account entity. To ensure secure operation,
// authentication and authorization decisions MUST be made on the basis of this id
// member, not the displayName nor name members. See Section 6.1 of
// [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266).
ID any `json:"id"`
}
+154
View File
@@ -0,0 +1,154 @@
package protocol
// Error is a struct that describes specific error conditions in a structured format.
type Error struct {
// Short name for the type of error that has occurred.
Type string `json:"type"`
// Additional details about the error.
Details string `json:"error"`
// Information to help debug the error.
DevInfo string `json:"debug"`
// Inner error.
Err error `json:"-"`
}
func (e *Error) Error() string {
return e.Details
}
func (e *Error) Unwrap() error {
return e.Err
}
func (e *Error) WithDetails(details string) *Error {
err := *e
err.Details = details
return &err
}
func (e *Error) WithInfo(info string) *Error {
err := *e
err.DevInfo = info
return &err
}
func (e *Error) WithError(err error) *Error {
errCopy := *e
errCopy.Err = err
return &errCopy
}
// ErrorUnknownCredential is a special Error which signals the fact the provided credential is unknown. The reason this
// specific error type is useful is so that the relying-party can send a signal to the Authenticator that the
// credential has been removed.
type ErrorUnknownCredential struct {
Err *Error
}
func (e *ErrorUnknownCredential) Error() string {
return e.Err.Error()
}
func (e *ErrorUnknownCredential) Unwrap() error {
return e.Err
}
func (e *ErrorUnknownCredential) copy() ErrorUnknownCredential {
err := *e.Err
return ErrorUnknownCredential{Err: &err}
}
func (e *ErrorUnknownCredential) WithDetails(details string) *ErrorUnknownCredential {
err := e.copy()
err.Err.Details = details
return &err
}
func (e *ErrorUnknownCredential) WithInfo(info string) *ErrorUnknownCredential {
err := e.copy()
err.Err.DevInfo = info
return &err
}
func (e *ErrorUnknownCredential) WithError(err error) *ErrorUnknownCredential {
errCopy := e.copy()
errCopy.Err.Err = err
return &errCopy
}
var (
ErrBadRequest = &Error{
Type: "invalid_request",
Details: "Error reading the request data",
}
ErrPolicyRestriction = &Error{
Type: "policy_restriction",
Details: "Policy restriction prevented the operation from completing",
}
ErrChallengeMismatch = &Error{
Type: "challenge_mismatch",
Details: "Stored challenge and received challenge do not match",
}
ErrParsingData = &Error{
Type: "parse_error",
Details: "Error parsing the authenticator response",
}
ErrAuthData = &Error{
Type: "auth_data",
Details: "Error verifying the authenticator data",
}
ErrVerification = &Error{
Type: "verification_error",
Details: "Error validating the authenticator response",
}
ErrAttestation = &Error{
Type: "attestation_error",
Details: "Error validating the attestation data provided",
}
ErrInvalidAttestation = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation data",
}
ErrMetadata = &Error{
Type: "invalid_metadata",
Details: "",
}
ErrAttestationFormat = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation format",
}
ErrAttestationCertificate = &Error{
Type: "invalid_certificate",
Details: "Invalid attestation certificate",
}
ErrAssertionSignature = &Error{
Type: "invalid_signature",
Details: "Assertion Signature against auth data and client hash is not valid",
}
ErrUnsupportedKey = &Error{
Type: "invalid_key_type",
Details: "Unsupported Public Key Type",
}
ErrUnsupportedAlgorithm = &Error{
Type: "unsupported_key_algorithm",
Details: "Unsupported public key algorithm",
}
ErrNotSpecImplemented = &Error{
Type: "spec_unimplemented",
Details: "This field is not yet supported by the WebAuthn spec",
}
ErrNotImplemented = &Error{
Type: "not_implemented",
Details: "This field is not yet supported by this library",
}
)
+99
View File
@@ -0,0 +1,99 @@
package protocol
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestError_Copy(t *testing.T) {
e1 := &Error{
Type: "test",
Details: "This is a test",
DevInfo: "Really, it's a test",
Err: errors.New("some error"),
}
e2 := e1.WithInfo("Diff Info")
e3 := e1.WithDetails("Diff Details")
e4 := e1.WithError(errors.New("some other error"))
assert.Equal(t, "Really, it's a test", e1.DevInfo)
assert.Equal(t, "This is a test", e1.Details)
assert.EqualError(t, e1.Err, "some error")
assert.Equal(t, "Diff Info", e2.DevInfo)
assert.Equal(t, e1.Details, e2.Details)
assert.Equal(t, e1.Err, e2.Err)
assert.Equal(t, "Really, it's a test", e3.DevInfo)
assert.Equal(t, "Diff Details", e3.Details)
assert.EqualError(t, e3.Err, "some error")
assert.Equal(t, e1.DevInfo, e3.DevInfo)
assert.Equal(t, "Diff Details", e3.Details)
assert.Equal(t, e1.Err, e3.Err)
assert.Equal(t, e1.DevInfo, e4.DevInfo)
assert.Equal(t, e1.Details, e4.Details)
assert.EqualError(t, e4.Err, "some other error")
assert.NotEqual(t, e1, e2)
assert.NotEqual(t, e1, e3)
assert.NotEqual(t, e1, e4)
assert.NotEqual(t, e2, e3)
assert.NotEqual(t, e2, e4)
assert.NotEqual(t, e3, e4)
e := e1.Unwrap()
assert.EqualError(t, e, "some error")
assert.EqualError(t, e1, "This is a test")
}
func TestErrorUnknownCredential_Copy(t *testing.T) {
e1 := &ErrorUnknownCredential{
Err: &Error{
Type: "test",
Details: "This is a test",
DevInfo: "Really, it's a test",
Err: errors.New("some error"),
},
}
e2 := e1.WithInfo("Diff Info")
e3 := e1.WithDetails("Diff Details")
e4 := e1.WithError(errors.New("some other error"))
assert.Equal(t, "Really, it's a test", e1.Err.DevInfo)
assert.Equal(t, "This is a test", e1.Err.Details)
assert.EqualError(t, e1.Err.Err, "some error")
assert.Equal(t, "Diff Info", e2.Err.DevInfo)
assert.Equal(t, e1.Err.Details, e2.Err.Details)
assert.Equal(t, e1.Err.Err, e2.Err.Err)
assert.Equal(t, "Really, it's a test", e3.Err.DevInfo)
assert.Equal(t, "Diff Details", e3.Err.Details)
assert.EqualError(t, e3.Err.Err, "some error")
assert.Equal(t, e1.Err.DevInfo, e3.Err.DevInfo)
assert.Equal(t, "Diff Details", e3.Err.Details)
assert.Equal(t, e1.Err.Err, e3.Err.Err)
assert.Equal(t, e1.Err.DevInfo, e4.Err.DevInfo)
assert.Equal(t, e1.Err.Details, e4.Err.Details)
assert.EqualError(t, e4.Err.Err, "some other error")
assert.NotEqual(t, e1, e2)
assert.NotEqual(t, e1, e3)
assert.NotEqual(t, e1, e4)
assert.NotEqual(t, e2, e3)
assert.NotEqual(t, e2, e4)
assert.NotEqual(t, e3, e4)
e := e1.Unwrap()
assert.Equal(t, e1.Err, e)
assert.EqualError(t, e1, "This is a test")
}
+26
View File
@@ -0,0 +1,26 @@
package protocol
// Extensions are discussed in §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions).
// For a list of commonly supported extensions, see §10. Defined Extensions
// (https://www.w3.org/TR/webauthn/#sctn-defined-extensions).
// AuthenticationExtensionsClientOutputs represents the IDL of the same name. It is a map of extension identifier
// strings to their output values, returned by the client after a create() or get() call.
//
// Specification: §5.9. Authentication Extensions Client Outputs (https://www.w3.org/TR/webauthn/#iface-authentication-extensions-client-outputs)
type AuthenticationExtensionsClientOutputs map[string]any
const (
// ExtensionAppID is the FIDO AppID Extension identifier. It is used during authentication to allow credentials
// registered via the legacy FIDO U2F JavaScript API to be used with WebAuthn.
//
// Specification: §10.1. FIDO AppID Extension (https://www.w3.org/TR/webauthn/#sctn-appid-extension)
ExtensionAppID = "appid"
// ExtensionAppIDExclude is the FIDO AppID Exclusion Extension identifier. It is used during registration to
// exclude credentials previously registered via the legacy FIDO U2F JavaScript API.
//
// Specification: §10.2. FIDO AppID Exclusion Extension (https://www.w3.org/TR/webauthn/#sctn-appid-exclude-extension)
ExtensionAppIDExclude = "appidExclude"
)
+43
View File
@@ -0,0 +1,43 @@
package protocol
import (
"errors"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func AssertIsProtocolError(t *testing.T, err error, errType, errDetails, errInfo any) {
var e *Error
require.True(t, errors.As(err, &e))
switch et := errType.(type) {
case string:
assert.Equal(t, et, e.Type)
case *regexp.Regexp:
assert.Regexp(t, et, e.Type)
default:
t.Fatalf("%T is not a known type", errType)
}
switch ed := errDetails.(type) {
case string:
assert.Equal(t, ed, e.Details)
case *regexp.Regexp:
assert.Regexp(t, ed, e.Details)
default:
t.Fatalf("%T is not a known type", errDetails)
}
switch ed := errInfo.(type) {
case string:
assert.Equal(t, ed, e.DevInfo)
case *regexp.Regexp:
assert.Regexp(t, ed, e.DevInfo)
default:
t.Fatalf("%T is not a known type", errInfo)
}
}
+30
View File
@@ -0,0 +1,30 @@
package protocol
import (
"crypto/x509"
)
func init() {
initAndroidKeyHardwareRoots()
initAppleHardwareRoots()
}
func initAndroidKeyHardwareRoots() {
if attAndroidKeyHardwareRootsCertPool == nil {
attAndroidKeyHardwareRootsCertPool = x509.NewCertPool()
}
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot1)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot2)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot3)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot4)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot5)))
}
func initAppleHardwareRoots() {
if attAppleHardwareRootsCertPool == nil {
attAppleHardwareRootsCertPool = x509.NewCertPool()
}
attAppleHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAppleRoot1)))
}
+12
View File
@@ -0,0 +1,12 @@
package protocol
import "crypto/x509"
func init() {
if attAndroidKeyHardwareRootsCertPool == nil {
attAndroidKeyHardwareRootsCertPool = x509.NewCertPool()
}
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyIntermediateFAKE1)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyIntermediateFAKE2)))
}
+81
View File
@@ -0,0 +1,81 @@
package protocol
// isISO3166Alpha2 reports whether code is a valid ISO 3166-1 alpha-2 country code.
// Officially-assigned codes and user-assigned codes (AA, QMQZ, XAXZ, ZZ) are both
// accepted; the W3C WebAuthn test vectors use AA, so rejecting user-assigned codes
// would fail §16 conformance. Codes of the wrong length, wrong case, or containing
// non-letters are rejected.
func isISO3166Alpha2(code string) bool {
if _, ok := iso3166Alpha2Codes[code]; ok {
return true
}
return isISO3166Alpha2UserAssigned(code)
}
// isISO3166Alpha2UserAssigned reports whether code is a user-assignable code per
// ISO 3166-1 (AA, QMQZ, XAXZ, ZZ).
func isISO3166Alpha2UserAssigned(code string) bool {
if len(code) != 2 {
return false
}
switch code {
case "AA", "ZZ":
return true
}
switch code[0] {
case 'Q':
return code[1] >= 'M' && code[1] <= 'Z'
case 'X':
return code[1] >= 'A' && code[1] <= 'Z'
}
return false
}
var iso3166Alpha2Codes = map[string]struct{}{
"AD": {}, "AE": {}, "AF": {}, "AG": {}, "AI": {}, "AL": {}, "AM": {}, "AO": {},
"AQ": {}, "AR": {}, "AS": {}, "AT": {}, "AU": {}, "AW": {}, "AX": {}, "AZ": {},
"BA": {}, "BB": {}, "BD": {}, "BE": {}, "BF": {}, "BG": {}, "BH": {}, "BI": {},
"BJ": {}, "BL": {}, "BM": {}, "BN": {}, "BO": {}, "BQ": {}, "BR": {}, "BS": {},
"BT": {}, "BV": {}, "BW": {}, "BY": {}, "BZ": {},
"CA": {}, "CC": {}, "CD": {}, "CF": {}, "CG": {}, "CH": {}, "CI": {}, "CK": {},
"CL": {}, "CM": {}, "CN": {}, "CO": {}, "CR": {}, "CU": {}, "CV": {}, "CW": {},
"CX": {}, "CY": {}, "CZ": {},
"DE": {}, "DJ": {}, "DK": {}, "DM": {}, "DO": {}, "DZ": {},
"EC": {}, "EE": {}, "EG": {}, "EH": {}, "ER": {}, "ES": {}, "ET": {},
"FI": {}, "FJ": {}, "FK": {}, "FM": {}, "FO": {}, "FR": {},
"GA": {}, "GB": {}, "GD": {}, "GE": {}, "GF": {}, "GG": {}, "GH": {}, "GI": {},
"GL": {}, "GM": {}, "GN": {}, "GP": {}, "GQ": {}, "GR": {}, "GS": {}, "GT": {},
"GU": {}, "GW": {}, "GY": {},
"HK": {}, "HM": {}, "HN": {}, "HR": {}, "HT": {}, "HU": {},
"ID": {}, "IE": {}, "IL": {}, "IM": {}, "IN": {}, "IO": {}, "IQ": {}, "IR": {},
"IS": {}, "IT": {},
"JE": {}, "JM": {}, "JO": {}, "JP": {},
"KE": {}, "KG": {}, "KH": {}, "KI": {}, "KM": {}, "KN": {}, "KP": {}, "KR": {},
"KW": {}, "KY": {}, "KZ": {},
"LA": {}, "LB": {}, "LC": {}, "LI": {}, "LK": {}, "LR": {}, "LS": {}, "LT": {},
"LU": {}, "LV": {}, "LY": {},
"MA": {}, "MC": {}, "MD": {}, "ME": {}, "MF": {}, "MG": {}, "MH": {}, "MK": {},
"ML": {}, "MM": {}, "MN": {}, "MO": {}, "MP": {}, "MQ": {}, "MR": {}, "MS": {},
"MT": {}, "MU": {}, "MV": {}, "MW": {}, "MX": {}, "MY": {}, "MZ": {},
"NA": {}, "NC": {}, "NE": {}, "NF": {}, "NG": {}, "NI": {}, "NL": {}, "NO": {},
"NP": {}, "NR": {}, "NU": {}, "NZ": {},
"OM": {},
"PA": {}, "PE": {}, "PF": {}, "PG": {}, "PH": {}, "PK": {}, "PL": {}, "PM": {},
"PN": {}, "PR": {}, "PS": {}, "PT": {}, "PW": {}, "PY": {},
"QA": {},
"RE": {}, "RO": {}, "RS": {}, "RU": {}, "RW": {},
"SA": {}, "SB": {}, "SC": {}, "SD": {}, "SE": {}, "SG": {}, "SH": {}, "SI": {},
"SJ": {}, "SK": {}, "SL": {}, "SM": {}, "SN": {}, "SO": {}, "SR": {}, "SS": {},
"ST": {}, "SV": {}, "SX": {}, "SY": {}, "SZ": {},
"TC": {}, "TD": {}, "TF": {}, "TG": {}, "TH": {}, "TJ": {}, "TK": {}, "TL": {},
"TM": {}, "TN": {}, "TO": {}, "TR": {}, "TT": {}, "TV": {}, "TW": {}, "TZ": {},
"UA": {}, "UG": {}, "UM": {}, "US": {}, "UY": {}, "UZ": {},
"VA": {}, "VC": {}, "VE": {}, "VG": {}, "VI": {}, "VN": {}, "VU": {},
"WF": {}, "WS": {},
"YE": {}, "YT": {},
"ZA": {}, "ZM": {}, "ZW": {},
}
+39
View File
@@ -0,0 +1,39 @@
package protocol
import "testing"
func TestIsISO3166Alpha2(t *testing.T) {
testCases := []struct {
name string
code string
want bool
}{
{"Assigned-US", "US", true},
{"Assigned-AU", "AU", true},
{"Assigned-DE", "DE", true},
{"Assigned-ZW", "ZW", true},
{"UserAssigned-AA", "AA", true},
{"UserAssigned-ZZ", "ZZ", true},
{"UserAssigned-QM", "QM", true},
{"UserAssigned-QZ", "QZ", true},
{"UserAssigned-XA", "XA", true},
{"UserAssigned-XZ", "XZ", true},
{"NotUserAssigned-QA", "QA", true},
{"NotUserAssigned-QL", "QL", false},
{"LowerCase-us", "us", false},
{"MixedCase-Us", "Us", false},
{"Alpha3-USA", "USA", false},
{"Empty", "", false},
{"SingleChar-U", "U", false},
{"Numeric-01", "01", false},
{"Whitespace", " US", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := isISO3166Alpha2(tc.code); got != tc.want {
t.Errorf("isISO3166Alpha2(%q) = %v, want %v", tc.code, got, tc.want)
}
})
}
}
+132
View File
@@ -0,0 +1,132 @@
package protocol
import (
"context"
"crypto/x509"
"fmt"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/metadata"
)
// ValidateMetadata validates the metadata for the given authenticator.
//
//nolint:gocyclo
func ValidateMetadata(ctx context.Context, mds metadata.Provider, aaguid uuid.UUID, attestationType, attestationFormat string, x5cs []any) (protoErr *Error) {
if mds == nil {
return nil
}
if AttestationFormat(attestationFormat) == AttestationFormatNone {
return nil
}
var (
entry *metadata.Entry
err error
)
if entry, err = mds.GetEntry(ctx, aaguid); err != nil {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. Error occurred retrieving the metadata entry: %+v", aaguid, err))
}
if entry == nil {
if aaguid == uuid.Nil && mds.GetValidateEntryPermitZeroAAGUID(ctx) {
return nil
}
if mds.GetValidateEntry(ctx) {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. The authenticator has no registered metadata.", aaguid))
}
return nil
}
if attestationType != "" && attestationType != stmtTypNone && mds.GetValidateAttestationTypes(ctx) {
found := false
for _, atype := range entry.MetadataStatement.AttestationTypes {
if string(atype) == attestationType {
found = true
break
}
}
if !found {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. The attestation type '%s' is not known to be used by this authenticator.", aaguid.String(), attestationType))
}
}
if mds.GetValidateStatus(ctx) {
if err = mds.ValidateStatusReports(ctx, entry.StatusReports); err != nil {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. Error occurred validating the authenticator status: %+v", aaguid, err))
}
}
if mds.GetValidateTrustAnchor(ctx) {
if len(x5cs) == 0 {
return nil
}
var (
x5c, parsed *x509.Certificate
x5cis []*x509.Certificate
raw []byte
ok bool
)
for i, x5cAny := range x5cs {
if raw, ok = x5cAny.([]byte); !ok {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to parse attestation certificate from x5c during attestation validation for Authenticator Attestation GUID '%s'.", aaguid)).WithInfo(fmt.Sprintf("The %s certificate in the attestation was type '%T' but '[]byte' was expected", loopOrdinalNumber(i), x5cAny))
}
if parsed, err = x509.ParseCertificate(raw); err != nil {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to parse attestation certificate from x5c during attestation validation for Authenticator Attestation GUID '%s'.", aaguid)).WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)).WithError(err)
}
if x5c == nil {
x5c = parsed
} else {
x5cis = append(x5cis, parsed)
}
}
if attestationType == string(metadata.AttCA) {
if protoErr = tpmParseAIKAttCA(x5c, x5cis); protoErr != nil {
return ErrMetadata.WithDetails(protoErr.Details).WithInfo(protoErr.DevInfo).WithError(protoErr)
}
}
if x5c != nil && x5c.Subject.CommonName != x5c.Issuer.CommonName {
if !entry.MetadataStatement.AttestationTypes.HasBasicFull() {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to validate attestation statement signature during attestation validation for Authenticator Attestation GUID '%s'. Attestation was provided in the full format but the authenticator doesn't support the full attestation format.", aaguid))
}
if _, err = x5c.Verify(entry.MetadataStatement.Verifier(x5cis)); err != nil {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to validate attestation statement signature during attestation validation for Authenticator Attestation GUID '%s'. The attestation certificate could not be verified due to an error validating the trust chain against the Metadata Service.", aaguid)).WithError(err)
}
}
}
return nil
}
func loopOrdinalNumber(n int) string {
n++
if n > 9 && n < 20 {
return fmt.Sprintf("%dth", n)
}
switch n % 10 {
case 1:
return fmt.Sprintf("%dst", n)
case 2:
return fmt.Sprintf("%dnd", n)
case 3:
return fmt.Sprintf("%drd", n)
default:
return fmt.Sprintf("%dth", n)
}
}
+263
View File
@@ -0,0 +1,263 @@
package protocol
import (
"context"
"fmt"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/testing/mocks"
)
func TestValidateMetadata(t *testing.T) {
aaguid := uuid.MustParse("0865c31d-05dc-4fb1-adce-3227bfb19967")
testCases := []struct {
name string
setup func(t *testing.T) metadata.Provider
aaguid uuid.UUID
attestationType string
attestationFormat string
x5cs []any
err *Error
}{
{
name: "ShouldReturnNilForNilProvider",
setup: func(t *testing.T) metadata.Provider { return nil },
attestationFormat: "packed",
},
{
name: "ShouldReturnNilForNoneFormat",
setup: func(t *testing.T) metadata.Provider { return mocks.NewMockMetadataProvider(gomock.NewController(t)) },
attestationFormat: "none",
},
{
name: "ShouldFailWhenGetEntryReturnsError",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("db error"))
return mds
},
aaguid: aaguid,
attestationFormat: "packed",
err: &Error{Type: "invalid_metadata", Details: "", DevInfo: "Failed to validate authenticator metadata for Authenticator Attestation GUID '0865c31d-05dc-4fb1-adce-3227bfb19967'. Error occurred retrieving the metadata entry: db error"},
},
{
name: "ShouldReturnNilWhenEntryNilAndValidationNotRequired",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
mds.EXPECT().GetValidateEntry(gomock.Any()).Return(false)
return mds
},
aaguid: aaguid,
attestationFormat: "packed",
},
{
name: "ShouldFailWhenEntryNilAndValidationRequired",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
mds.EXPECT().GetValidateEntry(gomock.Any()).Return(true)
return mds
},
aaguid: aaguid,
attestationFormat: "packed",
err: &Error{Type: "invalid_metadata", Details: "", DevInfo: "Failed to validate authenticator metadata for Authenticator Attestation GUID '0865c31d-05dc-4fb1-adce-3227bfb19967'. The authenticator has no registered metadata."},
},
{
name: "ShouldReturnNilForZeroAAGUIDWhenPermitted",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
mds.EXPECT().GetValidateEntryPermitZeroAAGUID(gomock.Any()).Return(true)
return mds
},
aaguid: uuid.Nil,
attestationFormat: "packed",
},
{
name: "ShouldFailWhenAttestationTypeMismatch",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
entry := &metadata.Entry{
MetadataStatement: metadata.Statement{
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
},
}
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(entry, nil)
mds.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(true)
return mds
},
aaguid: aaguid,
attestationType: "wrong-type",
attestationFormat: "packed",
err: &Error{Type: "invalid_metadata", Details: "", DevInfo: "Failed to validate authenticator metadata for Authenticator Attestation GUID '0865c31d-05dc-4fb1-adce-3227bfb19967'. The attestation type 'wrong-type' is not known to be used by this authenticator."},
},
{
name: "ShouldFailWhenStatusValidationFails",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
entry := &metadata.Entry{
MetadataStatement: metadata.Statement{
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
},
StatusReports: []metadata.StatusReport{{Status: metadata.Revoked}},
}
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(entry, nil)
mds.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(false)
mds.EXPECT().GetValidateStatus(gomock.Any()).Return(true)
mds.EXPECT().ValidateStatusReports(gomock.Any(), gomock.Any()).Return(fmt.Errorf("revoked"))
return mds
},
aaguid: aaguid,
attestationType: string(metadata.BasicFull),
attestationFormat: "packed",
err: &Error{Type: "invalid_metadata", Details: "", DevInfo: "Failed to validate authenticator metadata for Authenticator Attestation GUID '0865c31d-05dc-4fb1-adce-3227bfb19967'. Error occurred validating the authenticator status: revoked"},
},
{
name: "ShouldReturnNilWhenTrustAnchorValidationWithNoX5Cs",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
entry := &metadata.Entry{
MetadataStatement: metadata.Statement{
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
},
}
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(entry, nil)
mds.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(false)
mds.EXPECT().GetValidateStatus(gomock.Any()).Return(false)
mds.EXPECT().GetValidateTrustAnchor(gomock.Any()).Return(true)
return mds
},
aaguid: aaguid,
attestationType: string(metadata.BasicFull),
attestationFormat: "packed",
x5cs: nil,
},
{
name: "ShouldFailWhenX5CNotBytes",
setup: func(t *testing.T) metadata.Provider {
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
entry := &metadata.Entry{
MetadataStatement: metadata.Statement{
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
},
}
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(entry, nil)
mds.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(false)
mds.EXPECT().GetValidateStatus(gomock.Any()).Return(false)
mds.EXPECT().GetValidateTrustAnchor(gomock.Any()).Return(true)
return mds
},
aaguid: aaguid,
attestationType: string(metadata.BasicFull),
attestationFormat: "packed",
x5cs: []any{"not-bytes"},
err: &Error{Type: "invalid_metadata", Details: "Failed to parse attestation certificate from x5c during attestation validation for Authenticator Attestation GUID '0865c31d-05dc-4fb1-adce-3227bfb19967'.", DevInfo: "The 1st certificate in the attestation was type 'string' but '[]byte' was expected"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mds := tc.setup(t)
assert.Equal(t, tc.err, ValidateMetadata(context.Background(), mds, tc.aaguid, tc.attestationType, tc.attestationFormat, tc.x5cs))
})
}
}
func TestLoopOrdinalNumber(t *testing.T) {
testCases := []struct {
name string
n int
expected string
}{
{
name: "ShouldReturn1st",
n: 0,
expected: "1st",
},
{
name: "ShouldReturn2nd",
n: 1,
expected: "2nd",
},
{
name: "ShouldReturn3rd",
n: 2,
expected: "3rd",
},
{
name: "ShouldReturn4th",
n: 3,
expected: "4th",
},
{
name: "ShouldReturn10th",
n: 9,
expected: "10th",
},
{
name: "ShouldReturn11th",
n: 10,
expected: "11th",
},
{
name: "ShouldReturn12th",
n: 11,
expected: "12th",
},
{
name: "ShouldReturn13th",
n: 12,
expected: "13th",
},
{
name: "ShouldReturn21st",
n: 20,
expected: "21st",
},
{
name: "ShouldReturn22nd",
n: 21,
expected: "22nd",
},
{
name: "ShouldReturn23rd",
n: 22,
expected: "23rd",
},
{
name: "ShouldReturn100th",
n: 99,
expected: "100th",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, loopOrdinalNumber(tc.n))
})
}
}
+305
View File
@@ -0,0 +1,305 @@
package protocol
// CredentialCreation is the top-level request object for credential registration. It wraps
// [PublicKeyCredentialCreationOptions] and an optional mediation requirement. This is the object that should be
// serialized and sent to the client to initiate the navigator.credentials.create() call.
//
// Specification: §5.4. Options for Credential Creation (https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions)
type CredentialCreation struct {
Response PublicKeyCredentialCreationOptions `json:"publicKey"`
Mediation CredentialMediationRequirement `json:"mediation,omitempty"`
}
// CredentialAssertion is the top-level request object for credential assertion (login). It wraps
// [PublicKeyCredentialRequestOptions] and an optional mediation requirement. This is the object that should be
// serialized and sent to the client to initiate the navigator.credentials.get() call.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options)
type CredentialAssertion struct {
Response PublicKeyCredentialRequestOptions `json:"publicKey"`
Mediation CredentialMediationRequirement `json:"mediation,omitempty"`
}
// PublicKeyCredentialCreationOptions represents the IDL of the same name.
//
// In order to create a Credential via create(), the caller specifies a few parameters in a
// PublicKeyCredentialCreationOptions object.
//
// WebAuthn Level 3: hints,attestationFormats.
//
// Specification: §5.4. Options for Credential Creation (https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions)
type PublicKeyCredentialCreationOptions struct {
RelyingParty RelyingPartyEntity `json:"rp"`
User UserEntity `json:"user"`
Challenge URLEncodedBase64 `json:"challenge"`
Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"`
Timeout int `json:"timeout,omitempty"`
CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"`
AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"`
Hints []PublicKeyCredentialHints `json:"hints,omitempty"`
Attestation ConveyancePreference `json:"attestation,omitempty"`
AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"`
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
}
// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion.
// Its challenge member MUST be present, while its other members are OPTIONAL.
//
// WebAuthn Level 3: hints.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options)
type PublicKeyCredentialRequestOptions struct {
Challenge URLEncodedBase64 `json:"challenge"`
Timeout int `json:"timeout,omitempty"`
RelyingPartyID string `json:"rpId,omitempty"`
AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"`
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
Hints []PublicKeyCredentialHints `json:"hints,omitempty"`
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
}
// CredentialDescriptor represents the PublicKeyCredentialDescriptor IDL.
//
// This dictionary contains the attributes that are specified by a caller when referring to a public key credential as
// an input parameter to the create() or get() methods. It mirrors the fields of the PublicKeyCredential object returned
// by the latter methods.
//
// Specification: §5.10.3. Credential Descriptor (https://www.w3.org/TR/webauthn/#credential-dictionary)
type CredentialDescriptor struct {
// The valid credential types.
Type CredentialType `json:"type"`
// CredentialID The ID of a credential to allow/disallow.
CredentialID URLEncodedBase64 `json:"id"`
// The authenticator transports that can be used.
Transport []AuthenticatorTransport `json:"transports,omitempty"`
// AttestationType is the attestation type from the originating Credential (one of "basic_full",
// "basic_surrogate", "attca", "anonca", "ecdaa", "none"). Used internally only; not serialized.
AttestationType string `json:"-"`
// AttestationFormat is the attestation statement format from the originating Credential (one of "packed",
// "tpm", "android-key", "android-safetynet", "fido-u2f", "apple", "compound", "none"). Used internally only;
// not serialized. Prior releases overloaded [CredentialDescriptor.AttestationType] with this value; callers
// that construct descriptors directly should populate this field instead.
AttestationFormat string `json:"-"`
}
func (c CredentialDescriptor) SignalUnknownCredential(rpid string) *SignalUnknownCredential {
return &SignalUnknownCredential{
CredentialID: c.CredentialID,
RPID: rpid,
}
}
// CredentialType represents the PublicKeyCredentialType IDL and is used with the CredentialDescriptor IDL.
//
// This enumeration defines the valid credential types. It is an extension point; values can be added to it in the
// future, as more credential types are defined. The values of this enumeration are used for versioning the
// Authentication Assertion and attestation structures according to the type of the authenticator.
//
// Currently one credential type is defined, namely "public-key".
//
// Specification: §5.8.2. Credential Type Enumeration (https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype)
//
// Specification: §5.8.3. Credential Descriptor (https://www.w3.org/TR/webauthn/#dictionary-credential-descriptor)
type CredentialType string
const (
// PublicKeyCredentialType - Currently one credential type is defined, namely "public-key".
PublicKeyCredentialType CredentialType = "public-key"
)
// AuthenticationExtensions represents the AuthenticationExtensionsClientInputs IDL. This member contains additional
// parameters requesting additional processing by the client and authenticator.
//
// Specification: §5.7.1. Authentication Extensions Client Inputs (https://www.w3.org/TR/webauthn/#iface-authentication-extensions-client-inputs)
type AuthenticationExtensions map[string]any
// AuthenticatorSelection represents the AuthenticatorSelectionCriteria IDL.
//
// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements
// regarding authenticator attributes.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dictionary-authenticatorSelection)
type AuthenticatorSelection struct {
// AuthenticatorAttachment If this member is present, eligible authenticators are filtered to only
// authenticators attached with the specified AuthenticatorAttachment enum.
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
// RequireResidentKey this member describes the Relying Party's requirements regarding resident
// credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident
// public key credential source when creating a public key credential.
RequireResidentKey *bool `json:"requireResidentKey,omitempty"`
// ResidentKey this member describes the Relying Party's requirements regarding resident
// credentials per Webauthn Level 2.
ResidentKey ResidentKeyRequirement `json:"residentKey,omitempty"`
// UserVerification This member describes the Relying Party's requirements regarding user verification for
// the create() operation. Eligible authenticators are filtered to only those capable of satisfying this
// requirement.
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
}
// ConveyancePreference is the type representing the AttestationConveyancePreference IDL.
//
// WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding attestation
// conveyance during credential generation.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#enum-attestation-convey)
type ConveyancePreference string
const (
// PreferNoAttestation is a ConveyancePreference value.
//
// This value indicates that the Relying Party is not interested in authenticator attestation. For example, in order
// to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to
// save a round trip to an Attestation CA or Anonymization CA.
//
// This is the default value.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-none)
PreferNoAttestation ConveyancePreference = none
// PreferIndirectAttestation is a ConveyancePreference value.
//
// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation
// statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace the
// authenticator-generated attestation statements with attestation statements generated by an Anonymization CA, in
// order to protect the users privacy, or to assist Relying Parties with attestation verification in a
// heterogeneous ecosystem.
//
// Note: There is no guarantee that the Relying Party will obtain a verifiable attestation statement in this case.
// For example, in the case that the authenticator employs self attestation.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-indirect)
PreferIndirectAttestation ConveyancePreference = "indirect"
// PreferDirectAttestation is a ConveyancePreference value.
//
// This value indicates that the Relying Party wants to receive the attestation statement as generated by the
// authenticator.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-direct)
PreferDirectAttestation ConveyancePreference = "direct"
// PreferEnterpriseAttestation is a ConveyancePreference value.
//
// This value indicates that the Relying Party wants to receive an attestation statement that may include uniquely
// identifying information. This is intended for controlled deployments within an enterprise where the organization
// wishes to tie registrations to specific authenticators. User agents MUST NOT provide such an attestation unless
// the user agent or authenticator configuration permits it for the requested RP ID.
//
// If permitted, the user agent SHOULD signal to the authenticator (at invocation time) that enterprise
// attestation is requested, and convey the resulting AAGUID and attestation statement, unaltered, to the Relying
// Party.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-enterprise)
PreferEnterpriseAttestation ConveyancePreference = "enterprise"
)
// AttestationFormat is an internal representation of the relevant inputs for registration.
//
// Specification: §5.4 Options for Credential Creation (https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-attestationformats)
// Registry: https://www.iana.org/assignments/webauthn/webauthn.xhtml
type AttestationFormat string
const (
// AttestationFormatPacked is the "packed" attestation statement format is a WebAuthn-optimized format for
// attestation. It uses a very compact but still extensible encoding method. This format is implementable by
// authenticators with limited resources (i.e., secure elements).
AttestationFormatPacked AttestationFormat = "packed"
// AttestationFormatTPM is the TPM attestation statement format returns an attestation statement in the same format
// as the packed attestation statement format, although the rawData and signature fields are computed differently.
AttestationFormatTPM AttestationFormat = "tpm"
// AttestationFormatAndroidKey is the attestation statement format for platform authenticators on versions "N", and
// later, which may provide this proprietary "hardware attestation" statement.
AttestationFormatAndroidKey AttestationFormat = "android-key"
// AttestationFormatAndroidSafetyNet is the attestation statement format that Android-based platform authenticators
// MAY produce an attestation statement based on the Android SafetyNet API.
AttestationFormatAndroidSafetyNet AttestationFormat = "android-safetynet"
// AttestationFormatFIDOUniversalSecondFactor is the attestation statement format that is used with FIDO U2F
// authenticators.
AttestationFormatFIDOUniversalSecondFactor AttestationFormat = "fido-u2f"
// AttestationFormatApple is the attestation statement format that is used with Apple devices' platform
// authenticators.
AttestationFormatApple AttestationFormat = "apple"
// AttestationFormatCompound is used to pass multiple, self-contained attestation statements in a single ceremony.
AttestationFormatCompound AttestationFormat = "compound"
// AttestationFormatNone is the attestation statement format that is used to replace any authenticator-provided
// attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information.
AttestationFormatNone AttestationFormat = none
)
type PublicKeyCredentialHints string
const (
// PublicKeyCredentialHintSecurityKey is a PublicKeyCredentialHint that indicates that the Relying Party believes
// that users will satisfy this request with a physical security key. For example, an enterprise Relying Party may
// set this hint if they have issued security keys to their employees and will only accept those authenticators for
// registration and authentication.
//
// For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the
// authenticatorAttachment SHOULD be set to cross-platform.
PublicKeyCredentialHintSecurityKey PublicKeyCredentialHints = "security-key"
// PublicKeyCredentialHintClientDevice is a PublicKeyCredentialHint that indicates that the Relying Party believes
// that users will satisfy this request with a platform authenticator attached to the client device.
//
// For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the
// authenticatorAttachment SHOULD be set to platform.
PublicKeyCredentialHintClientDevice PublicKeyCredentialHints = "client-device"
// PublicKeyCredentialHintHybrid is a PublicKeyCredentialHint that indicates that the Relying Party believes that
// users will satisfy this request with general-purpose authenticators such as smartphones. For example, a consumer
// Relying Party may believe that only a small fraction of their customers possesses dedicated security keys. This
// option also implies that the local platform authenticator should not be promoted in the UI.
//
// For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the
// authenticatorAttachment SHOULD be set to cross-platform.
PublicKeyCredentialHintHybrid PublicKeyCredentialHints = "hybrid"
)
func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte {
var allowedCredentialIDs = make([][]byte, len(a.AllowedCredentials))
for i, credential := range a.AllowedCredentials {
allowedCredentialIDs[i] = credential.CredentialID
}
return allowedCredentialIDs
}
// Extensions is a generic type for WebAuthn extensions. The actual contents are defined by each individual extension.
//
// Specification: §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions)
type Extensions any
// ServerResponse is a response from a FIDO conformance server.
type ServerResponse struct {
// Status indicates whether the operation succeeded or failed.
Status ServerResponseStatus `json:"status"`
// Message provides additional details about an error if Status is "failed".
Message string `json:"errorMessage"`
}
// ServerResponseStatus is the status code returned by a FIDO conformance server.
type ServerResponseStatus string
const (
// StatusOk indicates the server operation was successful.
StatusOk ServerResponseStatus = "ok"
// StatusFailed indicates the server operation failed.
StatusFailed ServerResponseStatus = "failed"
)
+16
View File
@@ -0,0 +1,16 @@
package protocol
import "github.com/go-webauthn/webauthn/protocol/webauthncose"
//go:generate msgp
//msgp:replace webauthncose.COSEAlgorithmIdentifier with:int
//msgp:replace CredentialType with:string
//msgp:clearomitted
// CredentialParameter is the credential type and algorithm
// that the relying party wants the authenticator to create.
type CredentialParameter struct {
Type CredentialType `json:"type" msg:"typ,omitempty"`
Algorithm webauthncose.COSEAlgorithmIdentifier `json:"alg" msg:"alg,omitempty"`
}
+223
View File
@@ -0,0 +1,223 @@
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
package protocol
import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *CredentialParameter) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 2 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "typ":
{
var zb0002 string
zb0002, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Type")
return
}
z.Type = CredentialType(zb0002)
}
zb0001Mask |= 0x1
case "alg":
{
var zb0003 int
zb0003, err = dc.ReadInt()
if err != nil {
err = msgp.WrapError(err, "Algorithm")
return
}
z.Algorithm = webauthncose.COSEAlgorithmIdentifier(zb0003)
}
zb0001Mask |= 0x2
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x3 {
if (zb0001Mask & 0x1) == 0 {
z.Type = ""
}
if (zb0001Mask & 0x2) == 0 {
z.Algorithm = 0
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z CredentialParameter) EncodeMsg(en *msgp.Writer) (err error) {
// check for omitted fields
zb0001Len := uint32(2)
var zb0001Mask uint8 /* 2 bits */
_ = zb0001Mask
if z.Type == "" {
zb0001Len--
zb0001Mask |= 0x1
}
if z.Algorithm == 0 {
zb0001Len--
zb0001Mask |= 0x2
}
// variable map header, size zb0001Len
err = en.Append(0x80 | uint8(zb0001Len))
if err != nil {
return
}
// skip if no fields are to be emitted
if zb0001Len != 0 {
if (zb0001Mask & 0x1) == 0 { // if not omitted
// write "typ"
err = en.Append(0xa3, 0x74, 0x79, 0x70)
if err != nil {
return
}
err = en.WriteString(string(z.Type))
if err != nil {
err = msgp.WrapError(err, "Type")
return
}
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// write "alg"
err = en.Append(0xa3, 0x61, 0x6c, 0x67)
if err != nil {
return
}
err = en.WriteInt(int(z.Algorithm))
if err != nil {
err = msgp.WrapError(err, "Algorithm")
return
}
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z CredentialParameter) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// check for omitted fields
zb0001Len := uint32(2)
var zb0001Mask uint8 /* 2 bits */
_ = zb0001Mask
if z.Type == "" {
zb0001Len--
zb0001Mask |= 0x1
}
if z.Algorithm == 0 {
zb0001Len--
zb0001Mask |= 0x2
}
// variable map header, size zb0001Len
o = append(o, 0x80|uint8(zb0001Len))
// skip if no fields are to be emitted
if zb0001Len != 0 {
if (zb0001Mask & 0x1) == 0 { // if not omitted
// string "typ"
o = append(o, 0xa3, 0x74, 0x79, 0x70)
o = msgp.AppendString(o, string(z.Type))
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// string "alg"
o = append(o, 0xa3, 0x61, 0x6c, 0x67)
o = msgp.AppendInt(o, int(z.Algorithm))
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *CredentialParameter) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 2 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "typ":
{
var zb0002 string
zb0002, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Type")
return
}
z.Type = CredentialType(zb0002)
}
zb0001Mask |= 0x1
case "alg":
{
var zb0003 int
zb0003, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Algorithm")
return
}
z.Algorithm = webauthncose.COSEAlgorithmIdentifier(zb0003)
}
zb0001Mask |= 0x2
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x3 {
if (zb0001Mask & 0x1) == 0 {
z.Type = ""
}
if (zb0001Mask & 0x2) == 0 {
z.Algorithm = 0
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z CredentialParameter) Msgsize() (s int) {
s = 1 + 4 + msgp.StringPrefixSize + len(string(z.Type)) + 4 + msgp.IntSize
return
}
@@ -0,0 +1,123 @@
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
package protocol
import (
"bytes"
"testing"
"github.com/tinylib/msgp/msgp"
)
func TestMarshalUnmarshalCredentialParameter(t *testing.T) {
v := CredentialParameter{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgCredentialParameter(b *testing.B) {
v := CredentialParameter{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgCredentialParameter(b *testing.B) {
v := CredentialParameter{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalCredentialParameter(b *testing.B) {
v := CredentialParameter{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodeCredentialParameter(t *testing.T) {
v := CredentialParameter{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodeCredentialParameter Msgsize() is inaccurate")
}
vn := CredentialParameter{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodeCredentialParameter(b *testing.B) {
v := CredentialParameter{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodeCredentialParameter(b *testing.B) {
v := CredentialParameter{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
+314
View File
@@ -0,0 +1,314 @@
package protocol
import (
"bytes"
"encoding/json"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tinylib/msgp/msgp"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func TestPublicKeyCredentialRequestOptions_GetAllowedCredentialIDs(t *testing.T) {
type fields struct {
Challenge URLEncodedBase64
Timeout int
RelyingPartyID string
AllowedCredentials []CredentialDescriptor
UserVerification UserVerificationRequirement
Extensions AuthenticationExtensions
}
testCases := []struct {
name string
fields fields
expected [][]byte
}{
{
"CorrectCredentialIDs",
fields{
Challenge: URLEncodedBase64([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
Timeout: 60,
AllowedCredentials: []CredentialDescriptor{
{
Type: PublicKeyCredentialType, CredentialID: []byte("1234"), Transport: []AuthenticatorTransport{"usb"},
},
},
RelyingPartyID: "test.org",
UserVerification: VerificationPreferred,
Extensions: AuthenticationExtensions{},
},
[][]byte{
[]byte("1234"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
options := &PublicKeyCredentialRequestOptions{
Challenge: tc.fields.Challenge,
Timeout: tc.fields.Timeout,
RelyingPartyID: tc.fields.RelyingPartyID,
AllowedCredentials: tc.fields.AllowedCredentials,
UserVerification: tc.fields.UserVerification,
Extensions: tc.fields.Extensions,
}
assert.Equal(t, tc.expected, options.GetAllowedCredentialIDs())
})
}
}
func TestCredentialDescriptor_SignalUnknownCredential(t *testing.T) {
testCases := []struct {
name string
rpid string
have *CredentialDescriptor
expected *SignalUnknownCredential
expectedJSON string
}{
{
"ShouldHandleStandard",
"example.com",
&CredentialDescriptor{
CredentialID: URLEncodedBase64("1234"),
},
&SignalUnknownCredential{
CredentialID: URLEncodedBase64("1234"),
RPID: "example.com",
},
`{"credentialId":"MTIzNA","rpId":"example.com"}`,
},
{
"ShouldHandleNoID",
"example.com",
&CredentialDescriptor{},
&SignalUnknownCredential{
RPID: "example.com",
},
`{"credentialId":null,"rpId":"example.com"}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := tc.have.SignalUnknownCredential(tc.rpid)
assert.Equal(t, tc.expected, actual)
data, err := json.Marshal(actual)
require.NoError(t, err)
assert.Equal(t, tc.expectedJSON, string(data))
})
}
}
func TestCredentialParameter_MsgpRoundTrip(t *testing.T) {
testCases := []struct {
name string
original CredentialParameter
}{
{"BothFieldsSet", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
{"RS256", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgRS256}},
{"Ed25519", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgEdDSA}},
{"TypeOnly", CredentialParameter{Type: PublicKeyCredentialType}},
{"AlgorithmOnly", CredentialParameter{Algorithm: webauthncose.AlgES256}},
{"BothOmitted", CredentialParameter{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := tc.original.MarshalMsg(nil)
require.NoError(t, err)
var decoded CredentialParameter
left, err := decoded.UnmarshalMsg(data)
require.NoError(t, err)
assert.Empty(t, left)
assert.Equal(t, tc.original, decoded)
assert.LessOrEqual(t, len(data), tc.original.Msgsize())
var buf bytes.Buffer
require.NoError(t, msgp.Encode(&buf, tc.original))
var streamDecoded CredentialParameter
require.NoError(t, msgp.Decode(&buf, &streamDecoded))
assert.Equal(t, tc.original, streamDecoded)
})
}
}
func TestCredentialParameter_MsgpOmitEmpty(t *testing.T) {
testCases := []struct {
name string
value CredentialParameter
wantLen int
}{
{"BothPresent", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}, 2},
{"TypeOnly", CredentialParameter{Type: PublicKeyCredentialType}, 1},
{"AlgorithmOnly", CredentialParameter{Algorithm: webauthncose.AlgES256}, 1},
{"BothOmitted", CredentialParameter{}, 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := tc.value.MarshalMsg(nil)
require.NoError(t, err)
size, _, err := msgp.ReadMapHeaderBytes(data)
require.NoError(t, err)
assert.Equal(t, uint32(tc.wantLen), size)
})
}
}
func TestCredentialParameter_MsgpUnmarshalSkipsUnknownKeys(t *testing.T) {
t.Run("AlongsideKnown", func(t *testing.T) {
original := CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}
data, err := original.MarshalMsg(nil)
require.NoError(t, err)
size, rest, err := msgp.ReadMapHeaderBytes(data)
require.NoError(t, err)
spliced := msgp.AppendMapHeader(nil, size+1)
spliced = msgp.AppendString(spliced, "xyz")
spliced = msgp.AppendBool(spliced, true)
spliced = append(spliced, rest...)
var decoded CredentialParameter
left, err := decoded.UnmarshalMsg(spliced)
require.NoError(t, err)
assert.Empty(t, left)
assert.Equal(t, original, decoded)
})
t.Run("OnlyUnknown", func(t *testing.T) {
tiny := []byte{0x81, 0xa3, 'x', 'y', 'z', 0xc3}
var decoded CredentialParameter
left, err := decoded.UnmarshalMsg(tiny)
require.NoError(t, err)
assert.Empty(t, left)
assert.Equal(t, CredentialParameter{}, decoded)
var streamDecoded CredentialParameter
require.NoError(t, msgp.Decode(bytes.NewReader(tiny), &streamDecoded))
assert.Equal(t, CredentialParameter{}, streamDecoded)
})
}
func TestCredentialParameter_DecodeMsgInvalidTypes(t *testing.T) {
t.Run("NotAMap", func(t *testing.T) {
var c CredentialParameter
_, err := c.UnmarshalMsg(msgpString("not a map"))
require.Error(t, err)
var c2 CredentialParameter
require.Error(t, msgp.Decode(bytes.NewReader(msgpString("not a map")), &c2))
})
testCases := []struct {
name string
data []byte
wantSub string
}{
{"TypeAsInt", msgpOneFieldMap("typ", msgpInt64(42)), "Type"},
{"TypeAsBool", msgpOneFieldMap("typ", msgpBool(true)), "Type"},
{"AlgorithmAsString", msgpOneFieldMap("alg", msgpString("not an int")), "Algorithm"},
{"AlgorithmAsBool", msgpOneFieldMap("alg", msgpBool(true)), "Algorithm"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var c CredentialParameter
_, err := c.UnmarshalMsg(tc.data)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantSub)
var c2 CredentialParameter
streamErr := msgp.Decode(bytes.NewReader(tc.data), &c2)
require.Error(t, streamErr)
assert.Contains(t, streamErr.Error(), tc.wantSub)
})
}
}
func TestCredentialParameter_MsgpEncodeErrorPaths(t *testing.T) {
v := CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}
data, err := v.MarshalMsg(nil)
require.NoError(t, err)
exerciseEncodeMsgErrorPaths(t, v, data)
}
type failingWriter struct {
limit int
count int
}
func (w *failingWriter) Write(p []byte) (int, error) {
remaining := w.limit - w.count
if remaining <= 0 {
return 0, errors.New("failingWriter: exhausted")
}
if len(p) > remaining {
w.count = w.limit
return remaining, errors.New("failingWriter: exhausted")
}
w.count += len(p)
return len(p), nil
}
func exerciseEncodeMsgErrorPaths(t *testing.T, enc msgp.Encodable, marshalled []byte) {
t.Helper()
for limit := 0; limit <= len(marshalled); limit++ {
fw := &failingWriter{limit: limit}
wr := msgp.NewWriterSize(fw, 18)
err := enc.EncodeMsg(wr)
if err == nil {
err = wr.Flush()
}
if limit < len(marshalled) {
require.Errorf(t, err, "EncodeMsg should fail when underlying writer errors after %d bytes", limit)
} else {
require.NoError(t, err)
}
}
}
func msgpOneFieldMap(key string, value []byte) []byte {
b := msgp.AppendMapHeader(nil, 1)
b = msgp.AppendString(b, key)
return append(b, value...)
}
func msgpBool(v bool) []byte { return msgp.AppendBool(nil, v) }
func msgpInt64(v int64) []byte { return msgp.AppendInt64(nil, v) }
func msgpString(v string) []byte { return msgp.AppendString(nil, v) }
+51
View File
@@ -0,0 +1,51 @@
package protocol
// NewSignalAllAcceptedCredentials creates a new SignalAllAcceptedCredentials struct that can simply be encoded with
// json.Marshal.
func NewSignalAllAcceptedCredentials(rpid string, user AllAcceptedCredentialsUser) *SignalAllAcceptedCredentials {
if user == nil {
return nil
}
credentials := user.WebAuthnCredentialIDs()
ids := make([]URLEncodedBase64, len(credentials))
for i, id := range credentials {
ids[i] = id
}
return &SignalAllAcceptedCredentials{
AllAcceptedCredentialIDs: ids,
RPID: rpid,
UserID: user.WebAuthnID(),
}
}
// SignalAllAcceptedCredentials is a struct which represents the CDDL of the same name.
type SignalAllAcceptedCredentials struct {
AllAcceptedCredentialIDs []URLEncodedBase64 `json:"allAcceptedCredentialIds"`
RPID string `json:"rpId"`
UserID URLEncodedBase64 `json:"userId"`
}
// SignalCurrentUserDetails is a struct which represents the CDDL of the same name.
type SignalCurrentUserDetails struct {
DisplayName string `json:"displayName"`
Name string `json:"name"`
RPID string `json:"rpId"`
UserID URLEncodedBase64 `json:"userId"`
}
// SignalUnknownCredential is a struct which represents the CDDL of the same name.
type SignalUnknownCredential struct {
CredentialID URLEncodedBase64 `json:"credentialId"`
RPID string `json:"rpId"`
}
// AllAcceptedCredentialsUser is an interface that can be implemented by a user to provide information about their
// accepted credentials.
type AllAcceptedCredentialsUser interface {
WebAuthnID() []byte
WebAuthnCredentialIDs() [][]byte
}
+65
View File
@@ -0,0 +1,65 @@
package protocol
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewSignalAllAcceptedCredentials(t *testing.T) {
testCases := []struct {
name string
rpid string
have AllAcceptedCredentialsUser
expected *SignalAllAcceptedCredentials
expectedJSON string
}{
{
"ShouldHandleNil",
"example.com",
nil,
nil,
"null",
},
{
"ShouldHandleStandard",
"example.com",
&signalUser{
id: []byte("123"),
credentials: [][]byte{[]byte("456"), []byte("123")},
},
&SignalAllAcceptedCredentials{
AllAcceptedCredentialIDs: []URLEncodedBase64{[]byte("456"), []byte("123")},
RPID: "example.com",
UserID: []byte("123"),
},
`{"allAcceptedCredentialIds":["NDU2","MTIz"],"rpId":"example.com","userId":"MTIz"}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := NewSignalAllAcceptedCredentials(tc.rpid, tc.have)
assert.Equal(t, tc.expected, actual)
data, err := json.Marshal(actual)
assert.NoError(t, err)
assert.Equal(t, tc.expectedJSON, string(data))
})
}
}
type signalUser struct {
id []byte
credentials [][]byte
}
func (u *signalUser) WebAuthnID() []byte {
return u.id
}
func (u *signalUser) WebAuthnCredentialIDs() [][]byte {
return u.credentials
}
@@ -0,0 +1,680 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
"github.com/go-webauthn/webauthn/testing/mocks"
)
// WebAuthn Level 3 §16 End-to-End Test Vectors
//
// These tests exercise the full cradle-to-grave path:
// - Registration: ParseCredentialCreationResponseBody → ParsedCredentialCreationData.Verify
// - Authentication: ParseCredentialRequestResponseBody → ParsedCredentialAssertionData.Verify
//
// The spec test vectors that support full attestation (packed with x5c) are validated with a
// custom metadata.Provider that trusts the spec's synthetic attestation CA certificate.
//
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors
func TestSpecVectors_Registration_E2E(t *testing.T) {
testCases := []struct {
name string
attestationObject string
clientDataJSON string
credentialID string
challenge string
format string
credParams []CredentialParameter
rpTopOrigins []string
rpTopOriginVerificationMode TopOriginVerificationMode
allowCrossOrigin bool
mds metadata.Provider
err string
}{
//nolint:gosec
{
// §16.2 None Attestation - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256
name: "NoneES256",
attestationObject: "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c453551227d",
credentialID: "f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4",
challenge: "00c30fb78531c464d2b6771dab8d7b603c01162f2fa486bea70f283ae556e130",
format: "none",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.3 Self Attestation (Packed) - ES256
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-self-es256
name: "PackedSelfES256",
attestationObject: "a363666d74667061636b65646761747453746d74a263616c672663736967584630440220067a20754ab925005dbf378097c92120031581c73228d1fb4f5b881bcd7da98302207fc7b147558c7c0eba3af18bd9d121fa3d3a26d17fe3f220272178f473b6006d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f307859485667227d",
credentialID: "455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c",
challenge: "7869c2b772d4b58eba9378cf8f29e26cf935aa77df0da89fa99c0bdc0a76f7e5",
format: "packed",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.4 None Attestation - ES256 - Cross Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-crossOrigin
name: "NoneES256CrossOriginNotAllowed",
attestationObject: "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54500000000883f4f6014f19c09d87aa38123be48d000206e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57a501020326200121582022200a473f90b11078851550d03b4e44a2279f8c4eca27b3153dedfe03e4e97d225820cbd0be95e746ad6f5a8191be11756e4c0420e72f65b466d39bc56b8b123a9c6e",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a224f2d57717a514e5463554a484930437257576e7951504859647862694332674872434d475666704c4f306b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a207a5a7175457444523944577170573574425754467567227d",
credentialID: "6e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57",
challenge: "3be5aacd03537142472340ab5969f240f1d87716e20b6807ac230655fa4b3b49",
format: "none",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: false,
err: "Error validating cross origin flag",
},
//nolint:gosec
{
// §16.4 None Attestation - ES256 - Cross Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-crossOrigin
name: "NoneES256CrossOrigin",
attestationObject: "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54500000000883f4f6014f19c09d87aa38123be48d000206e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57a501020326200121582022200a473f90b11078851550d03b4e44a2279f8c4eca27b3153dedfe03e4e97d225820cbd0be95e746ad6f5a8191be11756e4c0420e72f65b466d39bc56b8b123a9c6e",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a224f2d57717a514e5463554a484930437257576e7951504859647862694332674872434d475666704c4f306b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a207a5a7175457444523944577170573574425754467567227d",
credentialID: "6e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57",
challenge: "3be5aacd03537142472340ab5969f240f1d87716e20b6807ac230655fa4b3b49",
format: "none",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: true,
},
//nolint:gosec
{
// §16.5 None Attestation - ES256 - Top Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-topOrigin
name: "NoneES256TopOriginNotAllowed",
attestationObject: "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b5410000000097586fd09799a76401c200455099ef2a0020b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1a5010203262001215820a1c47c1d82da4ebe82cd72207102b380670701993bc35398ae2e5726427fe01d22582086c1080d82987028c7f54ecb1b01185de243b359294a0ed210cd47480f0adc88",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a225468394d595a68706e6a504254786b68555f53646667364f4e58665672454673587a72636b7151664a2d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22746f704f726967696e223a2268747470733a2f2f6578616d706c652e636f6d227d",
credentialID: "b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1",
challenge: "4e1f4c6198699e33c14f192153f49d7e0e8e3577d5ac416c5f3adc92a41f27e5",
format: "none",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOrigins: []string{"https://example.com/"},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: false,
err: "Error validating cross origin flag",
},
//nolint:gosec
{
// §16.5 None Attestation - ES256 - Top Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-topOrigin
name: "NoneES256TopOrigin",
attestationObject: "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b5410000000097586fd09799a76401c200455099ef2a0020b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1a5010203262001215820a1c47c1d82da4ebe82cd72207102b380670701993bc35398ae2e5726427fe01d22582086c1080d82987028c7f54ecb1b01185de243b359294a0ed210cd47480f0adc88",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a225468394d595a68706e6a504254786b68555f53646667364f4e58665672454673587a72636b7151664a2d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22746f704f726967696e223a2268747470733a2f2f6578616d706c652e636f6d227d",
credentialID: "b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1",
challenge: "4e1f4c6198699e33c14f192153f49d7e0e8e3577d5ac416c5f3adc92a41f27e5",
format: "none",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOrigins: []string{"https://example.com/"},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: true,
},
//nolint:gosec
{
// §16.6 None Attestation - ES256 - Long Credential ID
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-long-credential-id
name: "NoneES256LongCredentialID",
attestationObject: "a363666d74646e6f6e656761747453746d74a0686175746844617461590483bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b549000000008f3360c2cd1b0ac14ffe0795c5d2638e03ff3a761a4e1674ad6c4305869435c0eee9c286172c229bb91b48b4ada140c0863417031305cce5b4a27a88d7fe728a5f5a627de771b4b40e77f187980c124f9fe832d7136010436a056cce716680587d23187cf1fc2c62ae86fc3e508ee9617ffc74fbc10488ec16ec5e9096328669a898709b655e549738c666c1ae6281dc3b5f733c251d3eefb76ee70a3805ca91bcc18e49c8dc7f63ebcb486ba8c3d6ab52b88ff72c6a5bb47c32f3ee8683a3ddc8abf60870448ec8a21b5bdcb183c7dead870255575a6df96eb1b6a2a1019780cba9e4887b17ff1164bbbcc10eb0d86ed75984cd3fa3419103024507dfd9ce8f92c56af7914cb0bb50b87ba82a312bb7dcd93028dbdcd6adb266979667158335171e3682d37755701edbf9d872846a291d49e57ef09da1ec637f5052ed2aa7407f7e61827468e94b461844f4c67be5fa9c6055a566f8fdfc29d4bf78a9ff275f552cc68ba543fa3962eea36fd1ea8453764577d021d0a181efc1f6100ab2e4110039e21ee16970bda7432b6134492155afc126295b3a2eccd12c66a68e340969e995e3e8c9c476e395cfc21203414110779474f1c9797406637dbe414f132519d3bf0ce4f01734ef0e1a12c3ad604ff15d766b1624db6a5a7ccbff7bc35c9908df94aba277e0af48f04ff3d16381c47e5a37ed3988a67a3b1ecaa926336b33391fff04128f869991c9fabd905b6fe3ceef5f8b630ec1c5d2636d5b1961ad5ca5004170f6f5e482792aad989b0287fe91e5c479403397152f1fa56aa79b156eb47e6c8ea3eb175c34cfb38ad8e772874639b1023d4d01395c94e55831671cc022aa6fa1e02a02c2e4abc776f6960e51f83b71a8c0f207b6a347573977812c9aa5480b0011aa739bd4b76c18c000cc4757cceccb920f007c40c00e37e5ab21476cd9f6054a8fffb55a108f5c706e2cea2049d81fd321ff47d2a5761b0800955ab1d4f4889f55a84e2601c684f17a4ade7453ea49591d0b59c8d9a765052f62219cf6ef4a5dd9539f0617d6ebbebce7c000455475d18449e25c49ef9a1e3efe18c09082ebe2058d7c347defaa92f0664553b805c7d76bbfce5f330aca220ac90a789380fc479ea0d8793205813cca590a912f699ad52f991a1bc0a503c3ec4b2a696719e3c26591a87127f7305cc7e72f4c8e39355ebb06a5b1042990f38710ee7aa612ee4374bb82e878585a70a96c2a6b47f101a4ff154be4fd76a3167577a5cc54d9167c154c69ac35485e44cc898b719e1be3cc9c0fb5624b8f8a0dae10947a41bf848b6c1bb33d1006ec077d7e286e3f2a7b4843716390119449fe2721e81a5ed2333d331c7120765da58fadae73c19d9a8c4509cf8ac1e9d98b799a5274509069739b5823f3fb496663820033426988eefca53e580e0f9e0dfe0992fc2e53a97e053639f98577058f995bdbd41cefdba50102032620012158203b8176b7504489cc593046d7988abb7905a742de6ac2cdc748a873c663e90cb12258201436d5edc9a75f23999eef9d5950a5c2455514ee1014084720f841a06b828a11",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22455250484a6c7a50586d5553516f4c364858675a7036464d75464f61704d322d7830682d587a5859374777222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
credentialID: "3a761a4e1674ad6c4305869435c0eee9c286172c229bb91b48b4ada140c0863417031305cce5b4a27a88d7fe728a5f5a627de771b4b40e77f187980c124f9fe832d7136010436a056cce716680587d23187cf1fc2c62ae86fc3e508ee9617ffc74fbc10488ec16ec5e9096328669a898709b655e549738c666c1ae6281dc3b5f733c251d3eefb76ee70a3805ca91bcc18e49c8dc7f63ebcb486ba8c3d6ab52b88ff72c6a5bb47c32f3ee8683a3ddc8abf60870448ec8a21b5bdcb183c7dead870255575a6df96eb1b6a2a1019780cba9e4887b17ff1164bbbcc10eb0d86ed75984cd3fa3419103024507dfd9ce8f92c56af7914cb0bb50b87ba82a312bb7dcd93028dbdcd6adb266979667158335171e3682d37755701edbf9d872846a291d49e57ef09da1ec637f5052ed2aa7407f7e61827468e94b461844f4c67be5fa9c6055a566f8fdfc29d4bf78a9ff275f552cc68ba543fa3962eea36fd1ea8453764577d021d0a181efc1f6100ab2e4110039e21ee16970bda7432b6134492155afc126295b3a2eccd12c66a68e340969e995e3e8c9c476e395cfc21203414110779474f1c9797406637dbe414f132519d3bf0ce4f01734ef0e1a12c3ad604ff15d766b1624db6a5a7ccbff7bc35c9908df94aba277e0af48f04ff3d16381c47e5a37ed3988a67a3b1ecaa926336b33391fff04128f869991c9fabd905b6fe3ceef5f8b630ec1c5d2636d5b1961ad5ca5004170f6f5e482792aad989b0287fe91e5c479403397152f1fa56aa79b156eb47e6c8ea3eb175c34cfb38ad8e772874639b1023d4d01395c94e55831671cc022aa6fa1e02a02c2e4abc776f6960e51f83b71a8c0f207b6a347573977812c9aa5480b0011aa739bd4b76c18c000cc4757cceccb920f007c40c00e37e5ab21476cd9f6054a8fffb55a108f5c706e2cea2049d81fd321ff47d2a5761b0800955ab1d4f4889f55a84e2601c684f17a4ade7453ea49591d0b59c8d9a765052f62219cf6ef4a5dd9539f0617d6ebbebce7c000455475d18449e25c49ef9a1e3efe18c09082ebe2058d7c347defaa92f0664553b805c7d76bbfce5f330aca220ac90a789380fc479ea0d8793205813cca590a912f699ad52f991a1bc0a503c3ec4b2a696719e3c26591a87127f7305cc7e72f4c8e39355ebb06a5b1042990f38710ee7aa612ee4374bb82e878585a70a96c2a6b47f101a4ff154be4fd76a3167577a5cc54d9167c154c69ac35485e44cc898b719e1be3cc9c0fb5624b8f8a0dae10947a41bf848b6c1bb33d1006ec077d7e286e3f2a7b4843716390119449fe2721e81a5ed2333d331c7120765da58fadae73c19d9a8c4509cf8ac1e9d98b799a5274509069739b5823f3fb496663820033426988eefca53e580e0f9e0dfe0992fc2e53a97e053639f98577058f995bdbd41cefdb",
challenge: "1113c7265ccf5e65124282fa1d7819a7a14cb8539aa4cdbec7487e5f35d8ec6c",
format: "none",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.7 Packed Attestation - ES256 (Full Attestation with x5c and MDS)
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-es256
name: "PackedES256WithMDS",
attestationObject: "a363666d74667061636b65646761747453746d74a363616c6726637369675847304502203f19ec4b229f46ab8c45eff29b904ff10c0390dc40bf1216f04a78f4ceba3425022100fe7041a32759aff05a0f9f26c70a999c7a284451ba89234a1d3483c25e21925b637835638159022530820221308201c8a00302010202110088c220f83c8ef1feafe94deae45faad0300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004a91ba4389409dd38a428141940ca8feb1ac0d7b4350558104a3777a49322f3798440f378b3398ab2d3bb7bf91322c92eb23556f59ad0a836fec4c7663b0e4dc3a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414a589ba72d060842ab11f74fb246bdedab16f9b9b301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d040302034700304402201726b9d85ecd8a5ed51163722ca3a20886fd9b242a0aa0453d442116075defd502207ef471e530ac87961a88a7f0d0c17b091ffc6b9238d30f79f635b417be5910e768617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d00000000876ca4f52071c3e9b25509ef2cdf7ed60020c9a6f5b3462d02873fea0c56862234f99f081728084e511bb7760201a89054a5a50102032620012158201cf27f25da591208a4239c2e324f104f585525479a29edeedd830f48e77aeae522582059e4b7da6c0106e206ce390c93ab98a15a5ec3887e57f0cc2bece803b920c423",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a227752684b58393334424634543345663153324831706c61325a725751475046746877365356756d56494249222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20396138624e596a4b436757724258552d66436c316167227d",
credentialID: "c9a6f5b3462d02873fea0c56862234f99f081728084e511bb7760201a89054a5",
challenge: "c1184a5fddf8045e13dc47f54b61f5a656b666b59018f16d870e9256e9952012",
format: "packed",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
mds: specTestMDSProvider(t),
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.8 Packed Attestation - ES384 (Full Attestation with x5c and MDS)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-es384
name: "PackedES384WithMDS",
attestationObject: "a363666d74667061636b65646761747453746d74a363616c67266373696758473045022100c56ecc970b7843833e0f461fde26233f61eb395161d481558c08b9c6ed61675b022029f5e05033705cd0f9b0a07e149468ec308a4f84906409efdceb1da20a7518d6637835638159022530820221308201c7a00302010202103d0a5588bb87ebb1d4cee4a1807c1b7c300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000417e5cc91d676d370e36aa7de40c25aacb45a3845f13d2932088ece2270b9b431241c219c22d0c256c9438ade00f2c05e62f8ef906b9b997ae9f3c460c2db66f5a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414c7c8dd95382a2230e4c0dd3664338fa908169a9c301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020348003045022054068cc9ae038937b7c468c307edb9c6927ffdeb6a20070c483eb40330f99f10022100cf41953919c3c04693d6b1f42a613753f204e70e85fc6e9b17036170b83596e068617574684461746158c5bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55900000000e950dcda3bdae1d087cda380a897848b0020953ae2dd9f28b1a1d5802c83e1f65833bb9769a08de82d812bc27c13fc6f06a9a5010203382220022158304866bd8b01da789e9eb806e5eab05ae5a638542296ab057a2f1bbce9b58f8a08b9171390b58a37ac7fffc2c5f45857da2258302a0b024c7f4b72072a1f96bd30a7261aae9571dd39870eb29e55c0941c6b08e89629a1ea1216aa64ce57c2807bf3901a",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22566e7344437a3459613848526164314674352d65445962785f574e4854615071336c76626a624e356f4d4d222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
credentialID: "953ae2dd9f28b1a1d5802c83e1f65833bb9769a08de82d812bc27c13fc6f06a9",
challenge: "567b030b3e186bc1d169dd45b79f9e0d86f1fd63474da3eade5bdb8db379a0c3",
format: "packed",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES384}},
mds: specTestMDSProvider(t),
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.9 Packed Attestation - ES512 (Full Attestation with x5c and MDS)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-es512
name: "PackedES512WithMDS",
attestationObject: "a363666d74667061636b65646761747453746d74a363616c67266373696758473045022100ce158f6c04aa5c14c0dd3e1103cf93664896fb5c337a66dbd7dba31546ff0d41022071cbcd0d3b8e218a6d05374e0ef8031329d25059002ac1603d02d5fd8a0a29cb637835638159022730820223308201c8a0030201020211008a128b7ebe52b993835779e6d9b81355300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004940b68885291536e2f7c60c05acfb252e7eebcf4304425dd93ab7b1962f20492bf18dc0f12862599e81fb764ac92151f9a78fcbb35d7a26c8c52949b18133c06a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604143ffad863abcd3dc5717b8a252189f41af97e7f31301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020349003046022100832c8b64c4f0188bd32e1bec63e13301cdc03165d3ef840d1f3dabb9a5719f83022100add57a9d5bedec98f29222dfc97ea795d055ee13a02a153d02be9ce00aedeb9168617574684461746158e9bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d0000000039d8ce6a3cf61025775083a738e5c2540020d17d5af7e3f37c56622a67c8462c9e1c6336dfccb8b61d359dc47378dba58ce4a5010203382320032158420083240a2c3ad21a3dc0a6daa3d8bc05a46d7cd9825ba010ae2a22686c2d6d663d7d5f678987fb1e767542e63dc197ae915e25f8ee284651af29066910a2cc083f50225842017337df47ab5cce5d716ef8caffa97a3012689b1f326ea6c43a1ba9596c72f71f0122390143552b42be772b4c35ffb961220c743b486a601ea4cb6d5412f5b078d3",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22547549677a5a4b7766684646484c544341635631573968356849354a4b707353313545317869646b33435f536a713149434d722d577448656a366e676a55714f3676366b33374d7a683373437646415f5231303744424f5570326737717654795233677039376a5064516c496d465659644977484d47673562385f63305f4a46767941343572733431314d6e614b72524f2d6a424750636e636935304a684f5151656e4b796c4134684d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206f3371566a4f4c327454576d3447786b7a495f516767227d",
credentialID: "d17d5af7e3f37c56622a67c8462c9e1c6336dfccb8b61d359dc47378dba58ce4",
challenge: "4ee220cd92b07e11451cb4c201c5755bd879848e492a9b12d79135c62764dc2fd28ead4808cafe5ad1de8fa9e08d4a8eeafea4dfb333877b02bc503f475d3b0c1394a7683baaf4f2477829f7b8cf750948985558748c073068396fcfdcd3f245bf2038e6bb38d7532768aad13be8c118f727722e7426139041e9caca503884c5",
format: "packed",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES512}},
mds: specTestMDSProvider(t),
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.10 Packed Attestation - RS256 (Full Attestation with x5c and MDS)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-rs256
name: "PackedRS256WithMDS",
attestationObject: "a363666d74667061636b65646761747453746d74a363616c672663736967584730450221008b8c5c6ea8c142c032e0be69e1353d44461c5c9109941cdda951b976eb95b6b302204d52f406c19e254b3ff9589bd18070fb055ac8db12fdd0a6734bea9d7168e900637835638159022630820222308201c7a00302010202101f6fb7a5ece81b45896b983a995da5f3300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004b7b36b7542a11120b443c794d0c99fdc25a06b76586413d81e086163ef6fe147a557afc34e2861d9057d6d465d4705a0310550bdeeb5f35ee35b9425ab859981a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414fb37b647bccfb9e54d989eaaacc1633868703fb3301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020349003046022100b86bc129d92afca7d9869a39f70f139a305b4073a39eb654d81424bed5757d91022100cf9f7c60cab7c4a7d3e7f0020f281a93d4fd0a9f95121b989f56932a68885fba68617574684461746159021bbfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000428f8878298b9862a36ad8c7527bfef20020992a18acc83f67533600c1138a4b4c4bd236de13629cf025ed17cb00b00b74dfa4010303390100205901b403fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012143010001",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2276716a776477414a76566679774e3976367039304f69666b7468752d6b6a79474c48717465705f49354b59222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
credentialID: "992a18acc83f67533600c1138a4b4c4bd236de13629cf025ed17cb00b00b74df",
challenge: "bea8f0770009bd57f2c0df6fea9f743a27e4b61bbe923c862c7aad7a9fc8e4a6",
format: "packed",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgRS256}},
mds: specTestMDSProvider(t),
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.11 Packed Attestation - EdDSA
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-eddsa
name: "PackedEdDSAWithMDS",
attestationObject: "a363666d74667061636b65646761747453746d74a363616c67266373696758483046022100d83f60bd80269537583218858aefb03ac57d45fa06e42feaae332d187f62da9f022100a02bd3cb6f7e1d283c93bad1f3f4b5a4c0494463da7fdbf256949116754d1f17637835638159022730820223308201c8a003020102021100b2cfc9ea33c8643b0e1a760463eaf164300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004dd2b7a564b73b8c0b81c4c62e521925c4d1198ec9f583dbf1eebe364b65cd9c29a9bdf346aaa81fb6b9507e5249a52fdaf8e39e26b0b7dc45992a7e233b70f70a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604140ae27546bc7eccb1b4b597bd354f0c0b1f1f8f8e301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020349003046022100a0d434ecb5fc3bfd7da5f41904517ad2836249f561bd834ba7a438a8ab7a4ce8022100fac845bb7a02513b58e9f319654dbe49b0f02b95835bac568c71f8a18cdde9ab6861757468446174615881bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54100000000d5aa33581e8ca478e20fe713f5d32ff20020ce9f840ed96599580cd140fbc7bb3230633f50f61041aff73308ae71caa8a2bda401010327200621582044e06ddd331c36a8dc667bab52bcae63486c916aa5e339e6acebaa84934bf832",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22714b763532723347734e396a526d733576616e6f6f306f303459557a656c6e7878586d5a426e6254733730222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20425f44543567375a445f2d394f544c59583549764551227d",
credentialID: "ce9f840ed96599580cd140fbc7bb3230633f50f61041aff73308ae71caa8a2bd",
challenge: "a8abf9dabdc6b0df63466b39bda9e8a34a34e185337a59f1c579990676d3b3bd",
format: "packed",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgEdDSA}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
{
// §16.13 TPM Attestation - ES256
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-tpm-es256
name: "TPMES256",
attestationObject: "a363666d746374706d6761747453746d74a663616c67266373696758463044022066e5826a652091030fd444e33c3eca2bc6dc548cf3045013addb38aa6457a21002203f3a5c95c9e707d0e555041bcc8698ee4ebc04e26cc8bae459705471789851766376657263322e30637835638159023a30820236308201dca0030201020210311fc42da0ab10c43a9b1bf3a75e34e2300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004c54e3f109094f60d7699b7db5d838569ffd1f3e1c9e897cd9eb40063f9402e3e9937e936cf1fcd5eb743ff443c97ab2edcd7c8e0e6cf6cfd413b8ab19fffa769a381d33081d0300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604145f546cb6973d4981e80fcdc7463859f5879680e4301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e30100603551d250409300706056781050803305e0603551d110101ff04543052a450304e314c3014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a3030303030303030301e060567810502020c15576562417574686e207465737420766563746f7273300a06082a8648ce3d0403020348003045022063c9a2797b8066f1db34dd609f1ab6695607e7a98e9ff8090a68853c9a9fc949022100a55831a39f5b8a2aa9a68837829cabf43fea2a5cea4859ae851cac78e6ac3e97677075624172656158560023000b0004000000000010001000030010002041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b0020d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d076863657274496e666f5869ff544347801700000020277d0e05579dd013215a62273f7f3a3e7e191ead2654a3036d75a5a3ee37a6b0000000000000000011111111222222223300000000000000000022000b9c42d8aad5939331b9af3711af179f17123178098c9a7d0ca89fcd1fc800f3c7000068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d000000004b92a377fc5f6107c4c85c190adbfd990020ec27bec7521c894bbb821105ea3724c90e770cf1fa354157ef18d0f18f78bea9a501020326200121582041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b225820d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d07",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a227a38677333787a753648595343716950413254776b51475452677a376c364d587376344a427054356f706b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
credentialID: "ec27bec7521c894bbb821105ea3724c90e770cf1fa354157ef18d0f18f78bea9",
challenge: "cfc82cdf1ceee876120aa88f0364f0910193460cfb97a317b2fe090694f9a299",
format: "tpm",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.14 Android Key Attestation - ES256
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-android-key-es256
name: "AndroidKeyES256",
attestationObject: "a363666d746b616e64726f69642d6b65796761747453746d74a363616c67266373696758483046022100e95512982aa3f216cff2e87c8ec57057b8529f674eaabeccaa27fd03d8779f19022100afb6bf459da4a826f00d01fc6b60712ff31dc4eb331619c8f874bb17e4314e94637835638159026e3082026a30820210a00302010202101ff91f76b63f44812f998b250b0286bf300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000499169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accfdd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6ba381a83081a5300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604141ac81e50641e8d1339ab9f7eb25f0cd5aac054b0301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e3045060a2b06010401d679020111043730350202012c0a01000201000a01000420b435028d7b6a8f83bb461d41c19b053a9d3cdb30351a4f374cd4cde8dbefb606040030003000300a06082a8648ce3d040302034800304502202d27f0ca39d2f519fc8f49c6d96dfc793059e211ff80516a50398cf1eac2a322022100d482a88c740f64cf6a98ccc6c8b9f5e1e533fa5e509f0a7b4c3a02f964a8eba768617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000ade9705e1ce7085b899a540d02199bf800200a4729519788b6ed8a2d772b494e186244d8c798c052960dbc8c10c915176795a501020326200121582099169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accf225820dd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6b",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2250654877747a5a647a4e345f384d76795869625f7037725f682d385162494438686c334541746d57414641222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205656316351755232714c4d5f616d50666f487a4c3067227d",
credentialID: "0a4729519788b6ed8a2d772b494e186244d8c798c052960dbc8c10c915176795",
challenge: "3de1f0b7365dccde3ff0cbf25e26ffa7baff87ef106c80fc865dc402d9960050",
format: "android-key",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
err: "Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN",
},
{
// §16.15 Apple Anonymous Attestation - ES256 - Top Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-apple-es256
name: "AppleAnonymousES256",
attestationObject: "a363666d74656170706c656761747453746d74a1637835638159025c30820258308201fea0030201020210394275613d5310b81a29ce90f48b61c1300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d030107034200048a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761af728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136ca38196308193300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e0416041412f1ce6c0ae39b403bfc9200317bc183a4e4d766301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e303306092a864886f76364080204263024a1220420d7a86e7233fb843eb0eeb407d8b76ff7e4f82d218cf5dbb461d752073f5cb29a300a06082a8648ce3d0403020348003045022070f5c2ede3000e9dae358d412b26a4acbf18f4cdeb80f5b13fcd564d090c39ec022100f672e2c3dbe117c9b1490b3c660abf5dcd74398187082dacb58b6744de4aca6068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54900000000748210a20076616a733b2114336fc38400209c4a5886af9283d9be3e9ec55978dedfdce2e3b365cab193ae850c16238fafb8a50102032620012158208a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761a225820f728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136c",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22395f61494954685341486431414a7a34774a6239714a316775616e37576c4464676432596d4b396142676b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20546a4c506e704f6158515572464e6362483274545a41227d",
credentialID: "9c4a5886af9283d9be3e9ec55978dedfdce2e3b365cab193ae850c16238fafb8",
challenge: "f7f688213852007775009cf8c096fda89d60b9a9fb5a50dd81dd9898af5a0609",
format: "apple",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.16 FIDO U2F Attestation - ES256
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-fido-u2f-es256
name: "FIDOU2FES256",
attestationObject: "a363666d74686669646f2d7532666761747453746d74a26373696758473045022100f41887a20063bb26867cb9751978accea5b81791a68f4f4dd6ea1fb6a5c086c302204e5e00aa3895777e6608f1f375f95450045da3da57a0e4fd451df35a31d2d98a637835638159022530820221308201c7a003020102021004f66dc6542ea7719dea416d325a2401300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000456fffa7093dede46aefeefb6e520c7ccc78967636e2f92582ba71455f64e93932dff3be4e0d4ef68e3e3b73aa087e26a0a0a30b02dc2aa2309db4c3a2fc936dea360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414420822eb1908b5cd3911017fbcad4641c05e05a3301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d040302034800304502200d0b777f0a0b181ad2830275acc3150fd6092430bcd034fd77beb7bdf8c2d546022100d4864edd95daa3927080855df199f1717299b24a5eecefbd017455a9b934d8f668617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54100000000afb3c2efc054df425013d5c88e79c3c10020a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4a5010203262001215820b0d62de6b30f86f0bac7a9016951391c2e31849e2e64661cbd2b13cd7d5508ad225820503b0bda2a357a9a4b34475a28e65b660b4898a9e3e9bbf0820d43494297edd0",
clientDataJSON: "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22344851334b5a4335797155486f696666786e73414e344445557955344452715177672d4237583049444159222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
credentialID: "a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4",
challenge: "e074372990b9caa507a227dfc67b003780c45325380d1a90c20f81ed7d080c06",
format: "fido-u2f",
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
body := specTestBuildRegistrationJSON(t, tc.credentialID, tc.attestationObject, tc.clientDataJSON)
pcc, err := ParseCredentialCreationResponseBody(bytes.NewReader(body))
require.NoError(t, err)
challenge := base64.RawURLEncoding.EncodeToString(specTestDecodeHex(t, tc.challenge))
_, err = pcc.Verify(challenge, specTestRPID, []string{specTestOrigin}, tc.rpTopOrigins, tc.rpTopOriginVerificationMode, tc.allowCrossOrigin, false, true, tc.mds, tc.credParams)
if tc.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
assert.Equal(t, tc.format, pcc.Response.AttestationObject.Format)
assert.Equal(t, specTestDecodeHex(t, tc.credentialID), pcc.Response.AttestationObject.AuthData.AttData.CredentialID)
})
}
}
func TestSpecVectors_Authentication_E2E(t *testing.T) {
testCases := []struct {
name string
authenticatorData string
clientDataJSON string
signature string
userHandle string
credentialID string
challenge string
credentialPubKey string
appID string
rpTopOrigins []string
rpTopOriginVerificationMode TopOriginVerificationMode
allowCrossOrigin bool
err string
}{
//nolint:gosec
{
// §16.2 None Attestation - ES256 (Authentication)
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256
name: "NoneES256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87",
credentialID: "f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4",
challenge: "39c0e7521417ba54d43e8dc95174f423dee9bf3cd804ff6d65c857c9abf4d408",
credentialPubKey: "a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.3 Self Attestation (Packed) - ES256 (Authentication)
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-self-es256
name: "PackedSelfES256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d433151227d",
signature: "304402203310b9431903c401f1be2bdc8d23a4007682dbbddcf846994947b7f465daf84002204e94dd00047b316061b3b99772b7efd95994a83ef584b3b6b825ea3550251b66",
credentialID: "455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c",
challenge: "4478a10b1352348dd160c1353b0d469b5db19eb91c27f7dfa6fed39fe26af20b",
credentialPubKey: "a5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.4 None Attestation - ES256 - Cross Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-crossOrigin
name: "NoneES256CrossOriginNotAllowed",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50500000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a226832716c463771445f65356c5f505f62796b7945377135645650674547685f49584a6b655737736e4d5463222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a2039327063545644304162792d713464746d6a36656667227d",
signature: "3046022100eb12fcf23b12764c0f122e22371fab92e283879fd798f38ee1841c951b6e40e7022100c76237ff9db77b3c56f30837cda6a09acfa2e915544e609c0733b1184036d1cf",
credentialID: "6e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57",
challenge: "876aa517ba83fdee65fcffdbca4c84eeae5d54f8041a1fc85c991e5bbb273137",
credentialPubKey: "a501020326200121582022200a473f90b11078851550d03b4e44a2279f8c4eca27b3153dedfe03e4e97d225820cbd0be95e746ad6f5a8191be11756e4c0420e72f65b466d39bc56b8b123a9c6e",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: false,
err: "Error validating cross origin flag",
},
//nolint:gosec
{
// §16.4 None Attestation - ES256 - Cross Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-crossOrigin
name: "NoneES256CrossOrigin",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50500000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a226832716c463771445f65356c5f505f62796b7945377135645650674547685f49584a6b655737736e4d5463222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a2039327063545644304162792d713464746d6a36656667227d",
signature: "3046022100eb12fcf23b12764c0f122e22371fab92e283879fd798f38ee1841c951b6e40e7022100c76237ff9db77b3c56f30837cda6a09acfa2e915544e609c0733b1184036d1cf",
credentialID: "6e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57",
challenge: "876aa517ba83fdee65fcffdbca4c84eeae5d54f8041a1fc85c991e5bbb273137",
credentialPubKey: "a501020326200121582022200a473f90b11078851550d03b4e44a2279f8c4eca27b3153dedfe03e4e97d225820cbd0be95e746ad6f5a8191be11756e4c0420e72f65b466d39bc56b8b123a9c6e",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: true,
},
//nolint:gosec
{
// §16.5 None Attestation - ES256 - Top Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-topOrigin
name: "NoneES256TopOrigin",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50500000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22315570636a4b53324b6f34377379486a7372787a68572d466f51465132796b3572426c584f6573656f4759222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22746f704f726967696e223a2268747470733a2f2f6578616d706c652e636f6d222c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205569466f4a4d56525148444146574669347678557051227d",
signature: "3045022100b5a70c81780d5fcc9a4f2ae9caae99058f8accaf58b91fb59329646c28ac6ffc022012e101c165db3c8e9957f0c54dd6ca9b56bc3bd2f280bd2faa6c1d02c6e5c171",
credentialID: "b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1",
challenge: "d54a5c8ca4b62a8e3bb321e3b2bc73856f85a10150db2939ac195739eb1ea066",
credentialPubKey: "a5010203262001215820a1c47c1d82da4ebe82cd72207102b380670701993bc35398ae2e5726427fe01d22582086c1080d82987028c7f54ecb1b01185de243b359294a0ed210cd47480f0adc88",
rpTopOrigins: []string{"https://example.com/"},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: false,
err: "Error validating cross origin flag",
},
{
// §16.5 None Attestation - ES256 - Top Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-topOrigin
name: "NoneES256TopOrigin",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50500000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22315570636a4b53324b6f34377379486a7372787a68572d466f51465132796b3572426c584f6573656f4759222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22746f704f726967696e223a2268747470733a2f2f6578616d706c652e636f6d222c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205569466f4a4d56525148444146574669347678557051227d",
signature: "3045022100b5a70c81780d5fcc9a4f2ae9caae99058f8accaf58b91fb59329646c28ac6ffc022012e101c165db3c8e9957f0c54dd6ca9b56bc3bd2f280bd2faa6c1d02c6e5c171",
credentialID: "b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1",
challenge: "d54a5c8ca4b62a8e3bb321e3b2bc73856f85a10150db2939ac195739eb1ea066",
credentialPubKey: "a5010203262001215820a1c47c1d82da4ebe82cd72207102b380670701993bc35398ae2e5726427fe01d22582086c1080d82987028c7f54ecb1b01185de243b359294a0ed210cd47480f0adc88",
rpTopOrigins: []string{"https://example.com/"},
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
allowCrossOrigin: true,
},
//nolint:gosec
{
// §16.6 None Attestation - ES256 - Long Credential ID
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-none-es256-long-credential-id
name: "NoneES256LongCredentialID",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22377833727057334f53505a307045664d396a75566d53574d36485a4935634f573875384d6f647047446a73222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "304502203ecef83fb12a0cae7841055f9f87103a99fd14b424194bbf06c4623d3ee6e3fd022100d2ace346db262b1374a6b70faa51f518a42ddca13a4125ce6f5052a75bac9fb6",
credentialID: "3a761a4e1674ad6c4305869435c0eee9c286172c229bb91b48b4ada140c0863417031305cce5b4a27a88d7fe728a5f5a627de771b4b40e77f187980c124f9fe832d7136010436a056cce716680587d23187cf1fc2c62ae86fc3e508ee9617ffc74fbc10488ec16ec5e9096328669a898709b655e549738c666c1ae6281dc3b5f733c251d3eefb76ee70a3805ca91bcc18e49c8dc7f63ebcb486ba8c3d6ab52b88ff72c6a5bb47c32f3ee8683a3ddc8abf60870448ec8a21b5bdcb183c7dead870255575a6df96eb1b6a2a1019780cba9e4887b17ff1164bbbcc10eb0d86ed75984cd3fa3419103024507dfd9ce8f92c56af7914cb0bb50b87ba82a312bb7dcd93028dbdcd6adb266979667158335171e3682d37755701edbf9d872846a291d49e57ef09da1ec637f5052ed2aa7407f7e61827468e94b461844f4c67be5fa9c6055a566f8fdfc29d4bf78a9ff275f552cc68ba543fa3962eea36fd1ea8453764577d021d0a181efc1f6100ab2e4110039e21ee16970bda7432b6134492155afc126295b3a2eccd12c66a68e340969e995e3e8c9c476e395cfc21203414110779474f1c9797406637dbe414f132519d3bf0ce4f01734ef0e1a12c3ad604ff15d766b1624db6a5a7ccbff7bc35c9908df94aba277e0af48f04ff3d16381c47e5a37ed3988a67a3b1ecaa926336b33391fff04128f869991c9fabd905b6fe3ceef5f8b630ec1c5d2636d5b1961ad5ca5004170f6f5e482792aad989b0287fe91e5c479403397152f1fa56aa79b156eb47e6c8ea3eb175c34cfb38ad8e772874639b1023d4d01395c94e55831671cc022aa6fa1e02a02c2e4abc776f6960e51f83b71a8c0f207b6a347573977812c9aa5480b0011aa739bd4b76c18c000cc4757cceccb920f007c40c00e37e5ab21476cd9f6054a8fffb55a108f5c706e2cea2049d81fd321ff47d2a5761b0800955ab1d4f4889f55a84e2601c684f17a4ade7453ea49591d0b59c8d9a765052f62219cf6ef4a5dd9539f0617d6ebbebce7c000455475d18449e25c49ef9a1e3efe18c09082ebe2058d7c347defaa92f0664553b805c7d76bbfce5f330aca220ac90a789380fc479ea0d8793205813cca590a912f699ad52f991a1bc0a503c3ec4b2a696719e3c26591a87127f7305cc7e72f4c8e39355ebb06a5b1042990f38710ee7aa612ee4374bb82e878585a70a96c2a6b47f101a4ff154be4fd76a3167577a5cc54d9167c154c69ac35485e44cc898b719e1be3cc9c0fb5624b8f8a0dae10947a41bf848b6c1bb33d1006ec077d7e286e3f2a7b4843716390119449fe2721e81a5ed2333d331c7120765da58fadae73c19d9a8c4509cf8ac1e9d98b799a5274509069739b5823f3fb496663820033426988eefca53e580e0f9e0dfe0992fc2e53a97e053639f98577058f995bdbd41cefdb",
challenge: "ef1deba56dce48f674a447ccf63b9599258ce87648e5c396f2ef0ca1da460e3b",
credentialPubKey: "a50102032620012158203b8176b7504489cc593046d7988abb7905a742de6ac2cdc748a873c663e90cb12258201436d5edc9a75f23999eef9d5950a5c2455514ee1014084720f841a06b828a11",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.7 Packed Attestation - ES256 (Full Attestation with x5c and MDS)
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-es256
name: "PackedES256Full",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2273524276704770587676463446524841565833496d4b4130453958773858306b526a44426c4d6668726255222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20415a4d77794d78496244382d756775464e7036723851227d",
signature: "30450220694969d3ee928de6f02ef23a9c644d7d779916451734a94b432542f498a1ebe90221008b0819c824218a97152cd099c55bfb1477b29d900a49a64018314f9bfccda163",
credentialID: "c9a6f5b3462d02873fea0c56862234f99f081728084e511bb7760201a89054a5",
challenge: "b1106fa46a57bef1781511c0557dc898a03413d5f0f17d244630c194c7e1adb5",
credentialPubKey: "a50102032620012158201cf27f25da591208a4239c2e324f104f585525479a29edeedd830f48e77aeae522582059e4b7da6c0106e206ce390c93ab98a15a5ec3887e57f0cc2bece803b920c423",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.8 Packed Attestation - ES384 (Full Attestation with x5c and MDS)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-es384
name: "PackedES384",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225f304844306c32396957623759654b4f39655277516545333753614649454574646941726f4b307446464d222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "3065023100e4efbb46745ed00e67c4d51ab2bacab2af62ffa8b7c5fecec6d7d9bf2582275034a713a3dd731685eee81adfaf6aa63f0230161655353f07e018a3c2539f8de7c8c4cf88d4c32d2be29fe4e76fa096ecc9458bbfe0895d57129ab324130e6f0692db",
credentialID: "953ae2dd9f28b1a1d5802c83e1f65833bb9769a08de82d812bc27c13fc6f06a9",
challenge: "ff41c3d25dbd8966fb61e28ef5e47041e137ed268520412d76202ba0ad2d1453",
credentialPubKey: "a5010203382220022158304866bd8b01da789e9eb806e5eab05ae5a638542296ab057a2f1bbce9b58f8a08b9171390b58a37ac7fffc2c5f45857da2258302a0b024c7f4b72072a1f96bd30a7261aae9571dd39870eb29e55c0941c6b08e89629a1ea1216aa64ce57c2807bf3901a",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.9 Packed Attestation - ES512 (Full Attestation with x5c and MDS)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-es512
name: "PackedES512",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22434e4d5a4447334c5055384d746c6d674d7a763136684a4e337a61677a5450564945734e65694b6f7a436279355046703067416f5848657a2d794c67386366306d6f66557669306c3653313565416a6471716d31635637394f6d72616b7a6e544253706f666278644c3479484777525234476b66563630546855473374793536714a4d334b6577635a6b7679354e376134574674434f7a7671416f71553745445a6a7a6c7149454569436b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "3081870242009bda02fe384e77bcb9fb42b07c395b7a53ec9d9616dd0308ab8495c2141c8364c7d16e212a4a4fb8e3987ff6c99eafd64d8484fd28c3fc7968f658a9033d1bb1b802416383e9f3ee20c691b66620299fef36bea2df4d39c92b2ead92f58e7b79ab0d9864d2ebf3b0dcc66ea13234492ccee6e9d421db43c959bcb94c162dc9494136c9f6",
credentialID: "d17d5af7e3f37c56622a67c8462c9e1c6336dfccb8b61d359dc47378dba58ce4",
challenge: "08d3190c6dcb3d4f0cb659a0333bf5ea124ddf36a0cd33d5204b0d7a22a8cc26f2e4f169d200285c77b3fb22e0f1c7f49a87d4be2d25e92d797808ddaaa9b5715efd3a6ada9339d3052a687dbc5d2f8c871b0451e0691f57ad138541b7b72e7aa8933729ec1c664bf2e4dedae1616d08ecefa80a2a53b103663ce5a881048829",
credentialPubKey: "a5010203382320032158420083240a2c3ad21a3dc0a6daa3d8bc05a46d7cd9825ba010ae2a22686c2d6d663d7d5f678987fb1e767542e63dc197ae915e25f8ee284651af29066910a2cc083f50225842017337df47ab5cce5d716ef8caffa97a3012689b1f326ea6c43a1ba9596c72f71f0122390143552b42be772b4c35ffb961220c743b486a601ea4cb6d5412f5b078d3",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.10 Packed Attestation - RS256 (Full Attestation with x5c and MDS)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-rs256
name: "PackedRS256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224b56395a39667150356978617970346e596d7834794e6f33617562597a5333536d75757459423462784d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "01063d52d7c39b4d432fc7063c5d93e582bdcb16889cd71f888d67d880ea730a428498d3bc8e1ee11f2b1ecbe6c292b118c55ffaaddefa8cad0a54dd137c51f1eec673f1bb6c4d1789d6826a222b22d0f585fc901fdc933212e579d199b89d672aa44891333e6a1355536025e82b25590256c3538229b55737083b2f6b9377e49e2472f11952f79fdd0da180b5ffd901b4049a8f081bb40711bef76c62aed943571f2d0575304cb549d68d8892f95086a30f93716aee818f8dc06e96c0d5e0ed4cfa9fd8773d90464b68cf140f7986666ff9c9e3302acd0535d60d769f465e2ab57ef8aabc89fccfef7ba32a64154a8b3d26be2298f470b8cc5377dbe3dfd4b0b45f8f01e63bde6cfc76b62771f9b70aa27cf40152cad93aa5acd784fd4b90f676e2ea828d0bf2400aebbaae4153e5838f537f88b6228346782a93a899be66ec77de45b3efcf311da6321c92e6b0cd11bfe653bf3e98cee8e341f02d67dbb6f9c98d9e8178090cfb5b70fbc6d541599ac794ae2f1d4de1286ec8de8c2daf7b1d15c8438e90d924df5c19045220a4c8438c1b979bbe016cf3d0eeec23c3999d4882cc645b776de930756612cdc6dd398160ff02a6",
credentialID: "992a18acc83f67533600c1138a4b4c4bd236de13629cf025ed17cb00b00b74df",
challenge: "295f59f5fa8fe62c5aca9e27626c78c8da376ae6d8cd2dd29aebad601e1bc4c5",
credentialPubKey: "a4010303390100205901b403fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012143010001",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.11 Packed Attestation - EdDSA (Authentication)
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-packed-eddsa
name: "PackedEdDSA",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50100000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2269566c583442786a4f6d6d44534b4c596f7870557439736e364d48454f7943413135726947514a6e763949222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "f5c59c7e46c34f6f8cc197101ddf9934fa2595f68eb1913a637e8419eb9ba4cfdfc48f85393bc0d40b011f0d6fecb097d6607525713223a0dc0d453993dae00b",
credentialID: "ce9f840ed96599580cd140fbc7bb3230633f50f61041aff73308ae71caa8a2bd",
challenge: "895957e01c633a698348a2d8a31a54b7db27e8c1c43b2080d79ae2190267bfd2",
credentialPubKey: "a401010327200621582044e06ddd331c36a8dc667bab52bcae63486c916aa5e339e6acebaa84934bf832",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.13 TPM Attestation - ES256
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-tpm-es256
name: "TPMES256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2241416b375a7349645731364a393642776768474a422d6f2d554330304f7a464c6a46705531693279417673222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "3045022060dc76b1607ec716c6e5eba8d056695ed6bc47b2e3d7a729c34e759e3ab66aa0022100d010a9e8fddcb64c439dfdca628ddb33cf245d567d157d9f66f942601bed9b38",
credentialID: "ec27bec7521c894bbb821105ea3724c90e770cf1fa354157ef18d0f18f78bea9",
challenge: "00093b66c21d5b5e89f7a07082118907ea3e502d343b314b8c5a54d62db202fb",
credentialPubKey: "a501020326200121582041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b225820d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d07",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.14 Android Key Attestation - ES256
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-android-key-es256
name: "AndroidKeyES256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22354f344679703238375851525a55447954746d74786971756851645742534b45545f702d36685433723459222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a2071784a78422d5f78677277794d4c3631386472536e41227d",
signature: "3045022100a2b5e37da43ceb63566f6e2817c6cbef261073d0cadfd213ff6229bd33ddea6c02203d77eb3202fc92b9b5bb843ef082c7766f8c7f23194c92708ff2f3d1de765a8f",
credentialID: "0a4729519788b6ed8a2d772b494e186244d8c798c052960dbc8c10c915176795",
challenge: "e4ee05ca9dbced74116540f24ed9adc62aae8507560522844ffa7eea14f7af86",
credentialPubKey: "a501020326200121582099169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accf225820dd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6b",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.15 Apple Anonymous Attestation - ES256 - Top Origin
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-apple-es256
name: "AppleAnonymousES256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22302d73705a4751654a76375149304136637433676b37476353366b416a442d6432445f503030656d625155222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "3046022100ee35db795ce28044e1f8231d68b3d79a9882f7415aa35c1b5ac74d24251073c8022100dcc65691650a412d0ceef843710c09827acf26c7845bddac07eec95863e7fc4c",
credentialID: "9c4a5886af9283d9be3e9ec55978dedfdce2e3b365cab193ae850c16238fafb8",
challenge: "d3eb2964641e26fed023403a72dde093b19c4ba9008c3f9dd83fcfd347a66d05",
credentialPubKey: "a50102032620012158208a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761a225820f728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136c",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
//nolint:gosec
{
// §16.16 FIDO U2F Attestation - ES256
// See: https://w3c.github.io/webauthn/#sctn-test-vectors-fido-u2f-es256
name: "FIDOU2FES256",
authenticatorData: "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50100000000",
clientDataJSON: "7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a222d5178684b59485954316d554f4e3461554139326b6d36537a49532d2d4f417362694e5650774249564455222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
signature: "304402206172459958fea907b7292b92f555034bfd884895f287a76200c1ba287239137002204727b166147e26a21bbc2921d192ebfed569b79438538e5c128b5e28e6926dd7",
credentialID: "a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4",
challenge: "f90c612981d84f599438de1a500f76926e92cc84bef8e02c6e23553f00485435",
credentialPubKey: "a5010203262001215820b0d62de6b30f86f0bac7a9016951391c2e31849e2e64661cbd2b13cd7d5508ad225820503b0bda2a357a9a4b34475a28e65b660b4898a9e3e9bbf0820d43494297edd0",
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
body := specTestBuildAssertionJSON(t, tc.credentialID, tc.authenticatorData, tc.clientDataJSON, tc.signature, tc.userHandle)
par, err := ParseCredentialRequestResponseBody(bytes.NewReader(body))
require.NoError(t, err)
challenge := base64.RawURLEncoding.EncodeToString(specTestDecodeHex(t, tc.challenge))
credPubKey := specTestDecodeHex(t, tc.credentialPubKey)
err = par.Verify(challenge, specTestRPID, tc.appID, []string{specTestOrigin}, tc.rpTopOrigins, tc.rpTopOriginVerificationMode, tc.allowCrossOrigin, false, true, credPubKey)
if tc.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
func TestSpecVectors_CACertificate(t *testing.T) {
assert.Equal(t, "WebAuthn test vectors", specTestCACert.Subject.CommonName)
assert.Equal(t, "W3C", specTestCACert.Subject.Organization[0])
assert.Equal(t, "Authenticator Attestation CA", specTestCACert.Subject.OrganizationalUnit[0])
assert.True(t, specTestCACert.IsCA)
}
// Supporting constants, types, and functions.
const (
specTestOrigin = "https://example.org"
// §16.1 Attestation Root Certificate used in tests.
specTestCACertHex = "30820207308201ada003020102021100ed7f905d8bd0b414d1784913170a90b6300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a3062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d030107034200043269300e5ff7b699015f70cf80a8763bf705bc2e2af0c1b39cff718b7c35880ca30f319078d91b03389a006fdfc8a1dcd84edfa07d30aa13474a248a0dab5baaa3423040300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e0416041445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d04030203480030450220483063b6bb08dcc83da33a02c11d2f42203176893554d138c614a36908724cc8022100f5ef2c912d4500b3e2f5b591d0622491e9f220dfd1f9734ec484bb7e90887663"
)
var (
specTestCACert *x509.Certificate
)
func specTestHexToBase64URL(t *testing.T, hexStr string) string {
t.Helper()
return base64.RawURLEncoding.EncodeToString(specTestDecodeHex(t, hexStr))
}
func specTestBuildRegistrationJSON(t *testing.T, credentialIDHex, attestationObjectHex, clientDataJSONHex string) []byte {
t.Helper()
id := specTestHexToBase64URL(t, credentialIDHex)
attObj := specTestHexToBase64URL(t, attestationObjectHex)
cdj := specTestHexToBase64URL(t, clientDataJSONHex)
response := map[string]any{
"id": id,
"rawId": id,
"type": "public-key",
"response": map[string]any{
"attestationObject": attObj,
"clientDataJSON": cdj,
},
}
data, err := json.Marshal(response)
require.NoError(t, err)
return data
}
func specTestBuildAssertionJSON(t *testing.T, credentialIDHex, authenticatorDataHex, clientDataJSONHex, signatureHex, userHandleHex string) []byte {
t.Helper()
id := specTestHexToBase64URL(t, credentialIDHex)
resp := map[string]any{
"authenticatorData": specTestHexToBase64URL(t, authenticatorDataHex),
"clientDataJSON": specTestHexToBase64URL(t, clientDataJSONHex),
"signature": specTestHexToBase64URL(t, signatureHex),
}
if userHandleHex != "" {
resp["userHandle"] = specTestHexToBase64URL(t, userHandleHex)
}
response := map[string]any{
"id": id,
"rawId": id,
"type": "public-key",
"response": resp,
}
data, err := json.Marshal(response)
require.NoError(t, err)
return data
}
// specTestMDSProvider returns a metadata.Provider that trusts the spec's synthetic attestation
// CA certificate (§16.1) for the packed attestation AAGUIDs.
func specTestMDSProvider(t *testing.T) metadata.Provider {
t.Helper()
knownAAGUIDs := map[uuid.UUID]bool{
uuid.Must(uuid.FromBytes(specTestDecodeHex(t, "876ca4f52071c3e9b25509ef2cdf7ed6"))): true, // ES256.
uuid.Must(uuid.FromBytes(specTestDecodeHex(t, "e950dcda3bdae1d087cda380a897848b"))): true, // ES384.
uuid.Must(uuid.FromBytes(specTestDecodeHex(t, "39d8ce6a3cf61025775083a738e5c254"))): true, // ES512.
uuid.Must(uuid.FromBytes(specTestDecodeHex(t, "428f8878298b9862a36ad8c7527bfef2"))): true, // RS256.
uuid.Must(uuid.FromBytes(specTestDecodeHex(t, "d5aa33581e8ca478e20fe713f5d32ff2"))): true, // EdDSA.
}
entry := &metadata.Entry{
MetadataStatement: metadata.Statement{
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
AttestationRootCertificates: []*x509.Certificate{specTestCACert},
},
StatusReports: []metadata.StatusReport{
{Status: metadata.FidoCertified},
},
}
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).DoAndReturn(func(_ interface{}, aaguid uuid.UUID) (*metadata.Entry, error) {
if knownAAGUIDs[aaguid] {
return entry, nil
}
return nil, nil
}).AnyTimes()
mds.EXPECT().GetValidateEntry(gomock.Any()).Return(false).AnyTimes()
mds.EXPECT().GetValidateEntryPermitZeroAAGUID(gomock.Any()).Return(true).AnyTimes()
mds.EXPECT().GetValidateTrustAnchor(gomock.Any()).Return(true).AnyTimes()
mds.EXPECT().GetValidateStatus(gomock.Any()).Return(false).AnyTimes()
mds.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(true).AnyTimes()
return mds
}
func init() {
data, err := hex.DecodeString(specTestCACertHex)
if err != nil {
panic(err)
}
cert, err := x509.ParseCertificate(data)
if err != nil {
panic(err)
}
specTestCACert = cert
attAndroidKeyHardwareRootsCertPool.AddCert(cert)
attAppleHardwareRootsCertPool.AddCert(cert)
tpmManufacturers = append(tpmManufacturers, tpmManufacturer{"00000000", "WebAuthn Test", "WebAuthnTest"})
}
+270
View File
@@ -0,0 +1,270 @@
package protocol
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func mustParseX509Certificate(der []byte) *x509.Certificate {
cert, err := x509.ParseCertificate(der)
if err != nil {
panic(err)
}
return cert
}
func mustParseX509CertificatePEM(raw []byte) *x509.Certificate {
block, rest := pem.Decode(raw)
if len(rest) > 0 || block == nil || block.Type != "CERTIFICATE" {
panic("Invalid PEM Certificate")
}
return mustParseX509Certificate(block.Bytes)
}
func attStatementParseX5CS(attStatement map[string]any, key string) (x5c []any, x5cs []*x509.Certificate, err error) {
var ok bool
if x5c, ok = attStatement[key].([]any); !ok {
return nil, nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value")
}
if len(x5c) == 0 {
return nil, nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value: empty array")
}
if x5cs, err = parseX5C(x5c); err != nil {
return nil, nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value: error occurred parsing values").WithError(err)
}
return x5c, x5cs, nil
}
func parseX5C(x5c []any) (x5cs []*x509.Certificate, err error) {
x5cs = make([]*x509.Certificate, len(x5c))
var (
raw []byte
ok bool
)
for i, t := range x5c {
if raw, ok = t.([]byte); !ok {
return nil, fmt.Errorf("x5c[%d] is not a byte array", i)
}
if x5cs[i], err = x509.ParseCertificate(raw); err != nil {
return nil, fmt.Errorf("x5c[%d] is not a valid certificate: %w", i, err)
}
}
return x5cs, nil
}
// attStatementCertChainVerify allows verifying an attestation statement certificate chain and optionally allows
// mangling the not after value for purpose of just validating the attestation lineage. If you set mangleNotAfter to
// true this function should only be considered safe for determining lineage, and not hte validity of a chain in
// general.
//
// WARNING: Setting mangleNotAfter=true weakens security by accepting expired certificates.
func attStatementCertChainVerify(certs []*x509.Certificate, roots *x509.CertPool, mangleNotAfter bool, mangleNotAfterSafeTime time.Time) (chains [][]*x509.Certificate, err error) {
if len(certs) == 0 {
return nil, errors.New("empty chain")
}
leaf := certs[0]
for _, cert := range certs {
if !cert.IsCA {
leaf = certInsecureConditionalNotAfterMangle(cert, mangleNotAfter, mangleNotAfterSafeTime)
break
}
}
var (
intermediates *x509.CertPool
)
staticRoots := roots != nil
intermediates = x509.NewCertPool()
if roots == nil {
if roots, err = x509.SystemCertPool(); err != nil || roots == nil {
roots = x509.NewCertPool()
}
}
for _, cert := range certs {
if cert == leaf {
continue
}
if isSelfSigned(cert) && !staticRoots {
roots.AddCert(certInsecureConditionalNotAfterMangle(cert, mangleNotAfter, mangleNotAfterSafeTime))
} else {
intermediates.AddCert(certInsecureConditionalNotAfterMangle(cert, mangleNotAfter, mangleNotAfterSafeTime))
}
}
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
}
return leaf.Verify(opts)
}
func isSelfSigned(c *x509.Certificate) bool {
if !c.IsCA {
return false
}
return c.CheckSignatureFrom(c) == nil
}
// This function is used to intentionally but conditionally mangle the certificate not after value to exclude it from
// the verification process. This should only be used in instances where all you care about is which certificates
// performed the signing.
//
// WARNING: Setting mangle=true weakens security by accepting expired certificates.
func certInsecureConditionalNotAfterMangle(cert *x509.Certificate, mangle bool, safe time.Time) (out *x509.Certificate) {
if !mangle || cert.NotAfter.After(time.Now().Add(time.Minute)) {
return cert
}
out = &x509.Certificate{}
*out = *cert
out.NotAfter = safe
return out
}
func verifyAttestationECDSAPublicKeyMatch(att AttestationObject, cert *x509.Certificate) (attPublicKeyData webauthncose.EC2PublicKeyData, err error) {
var (
key any
ok bool
publicKey, attPublicKey *ecdsa.PublicKey
)
if key, err = webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey); err != nil {
return attPublicKeyData, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err)).WithError(err)
}
if attPublicKeyData, ok = key.(webauthncose.EC2PublicKeyData); !ok {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Attestation public key is not ECDSA")
}
if publicKey, ok = cert.PublicKey.(*ecdsa.PublicKey); !ok {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Credential public key is not ECDSA")
}
if attPublicKey, err = attPublicKeyData.ToECDSA(); err != nil {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Error converting public key to ECDSA").WithError(err)
}
if !attPublicKey.Equal(publicKey) {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Certificate public key does not match public key in authData")
}
return attPublicKeyData, nil
}
// ValidateRPID performs non-exhaustive checks to ensure the string is most likely a domain string as
// relying-party ID's are required to be. Effectively this can be an IP, localhost, or a string that contains a period.
// The relying-party ID must not contain scheme, port, path, query, or fragment components.
//
// See: https://www.w3.org/TR/webauthn/#rp-id
//
//nolint:gocyclo
func ValidateRPID(value string) (err error) {
if len(value) == 0 {
return errors.New("empty value provided")
}
if ip := net.ParseIP(value); ip != nil {
return nil
}
var rpid *url.URL
if rpid, err = url.Parse(value); err != nil {
return err
}
if rpid.Scheme != "" && rpid.Opaque != "" && rpid.Path == "" {
return errors.New("the port component must be empty")
}
if rpid.Scheme != "" {
if rpid.Host != "" && rpid.Path != "" {
return errors.New("the path component must be empty")
}
if rpid.Host != "" && rpid.RawQuery != "" {
return errors.New("the query component must be empty")
}
if rpid.Host != "" && rpid.Fragment != "" {
return errors.New("the fragment component must be empty")
}
if rpid.Host != "" && rpid.Port() != "" {
return errors.New("the port component must be empty")
}
return errors.New("the scheme component must be empty")
}
if rpid.RawQuery != "" {
return errors.New("the query component must be empty")
}
if rpid.RawFragment != "" || rpid.Fragment != "" {
return errors.New("the fragment component must be empty")
}
if rpid.Host == "" {
if strings.Contains(rpid.Path, "/") {
return errors.New("the path component must be empty")
}
}
if value != "localhost" && !strings.Contains(rpid.Path, ".") {
return errors.New("the domain component must actually be a domain")
}
return nil
}
// IsAttestationFormatString reports whether s is one of the WebAuthn-defined attestation statement format
// identifiers. Used to detect and migrate records from prior releases which stored
// the format string in the AttestationType field.
func IsAttestationFormatString(s string) bool {
switch AttestationFormat(s) {
case AttestationFormatPacked,
AttestationFormatTPM,
AttestationFormatAndroidKey,
AttestationFormatAndroidSafetyNet,
AttestationFormatFIDOUniversalSecondFactor,
AttestationFormatApple,
AttestationFormatCompound,
AttestationFormatNone:
return true
default:
return false
}
}
+615
View File
@@ -0,0 +1,615 @@
package protocol
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func TestValidateRPID(t *testing.T) {
testCases := []struct {
name string
value string
err string
}{
{
name: "ValidRPIDDomain",
value: "example.com",
},
{
name: "ValidRPIDLocalHost",
value: "localhost",
},
{
name: "ValidRPIDUsingIPv4",
value: "127.0.0.1",
},
{
name: "ValidRPIDUsingIPv4Alt",
value: "1.1.1.1",
},
{
name: "ValidRPIDUsingIPv6",
value: "2001:DB8:0:0:8:800:200C:417A",
},
{
name: "ValidRPIDUsingIPv6Alt",
value: "::1",
},
{
name: "InvalidRPIDNotDomain",
value: "example",
err: "the domain component must actually be a domain",
},
{
name: "InvalidRPIDScheme",
value: "https://example.com",
err: "the scheme component must be empty",
},
{
name: "InvalidRPIDPort",
value: "example.com:1234",
err: "the port component must be empty",
},
{
name: "InvalidRPIDPortWithScheme",
value: "https://example.com:1234",
err: "the port component must be empty",
},
{
name: "InvalidRPIDPath",
value: "example.com/example",
err: "the path component must be empty",
},
{
name: "InvalidRPIDQuery",
value: "example.com?abc=123",
err: "the query component must be empty",
},
{
name: "InvalidRPIDFragment",
value: "example.com#abc=123",
err: "the fragment component must be empty",
},
{
name: "InvalidRPIDPathWithScheme",
value: "https://example.com/example",
err: "the path component must be empty",
},
{
name: "InvalidRPIDQueryWithScheme",
value: "https://example.com?abc=123",
err: "the query component must be empty",
},
{
name: "InvalidRPIDFragmentWithScheme",
value: "https://example.com#abc=123",
err: "the fragment component must be empty",
},
{
name: "InvalidEmpty",
value: "",
err: "empty value provided",
},
{
name: "InvalidURI",
value: "https://example\x00.com",
err: "parse \"https://example\\x00.com\": net/url: invalid control character in URL",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateRPID(tc.value)
if tc.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
func TestMustParseX509Certificate(t *testing.T) {
t.Run("ShouldPanic", func(t *testing.T) {
assert.Panics(t, func() {
mustParseX509Certificate([]byte("not a certificate"))
})
})
}
func TestMustParseX509CertificatePEM(t *testing.T) {
t.Run("ShouldPanicInvalidPEM", func(t *testing.T) {
assert.Panics(t, func() {
mustParseX509CertificatePEM([]byte("not a pem"))
})
})
}
func TestAttStatementParseX5CS(t *testing.T) {
cert := testUtilsGenerateSelfSignedCert(t)
testCases := []struct {
name string
have map[string]any
expected struct {
count int
err string
}
}{
{
name: "ShouldFailNotArray",
have: map[string]any{"x5c": "not an array"},
expected: struct {
count int
err string
}{
err: "Error retrieving x5c value",
},
},
{
name: "ShouldFailEmptyArray",
have: map[string]any{"x5c": []any{}},
expected: struct {
count int
err string
}{
err: "Error retrieving x5c value: empty array",
},
},
{
name: "ShouldFailParseError",
have: map[string]any{"x5c": []any{[]byte("not a cert")}},
expected: struct {
count int
err string
}{
err: "Error retrieving x5c value: error occurred parsing values",
},
},
{
name: "ShouldSucceed",
have: map[string]any{"x5c": []any{cert.Raw}},
expected: struct {
count int
err string
}{
count: 1,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
x5c, x5cs, err := attStatementParseX5CS(tc.have, "x5c")
if tc.expected.err == "" {
assert.NoError(t, err)
assert.Len(t, x5c, tc.expected.count)
assert.Len(t, x5cs, tc.expected.count)
} else {
assert.Nil(t, x5c)
assert.Nil(t, x5cs)
assert.EqualError(t, err, tc.expected.err)
}
})
}
}
func TestParseX5C(t *testing.T) {
cert := testUtilsGenerateSelfSignedCert(t)
testCases := []struct {
name string
have []any
expected struct {
count int
err string
}
}{
{
name: "ShouldFailNotByteArray",
have: []any{"not bytes"},
expected: struct {
count int
err string
}{
err: "x5c[0] is not a byte array",
},
},
{
name: "ShouldFailInvalidCert",
have: []any{[]byte("invalid cert der")},
expected: struct {
count int
err string
}{
err: "x5c[0] is not a valid certificate: x509: malformed certificate",
},
},
{
name: "ShouldSucceed",
have: []any{cert.Raw},
expected: struct {
count int
err string
}{
count: 1,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
x5cs, err := parseX5C(tc.have)
if tc.expected.err == "" {
assert.NoError(t, err)
assert.Len(t, x5cs, tc.expected.count)
} else {
assert.Nil(t, x5cs)
assert.EqualError(t, err, tc.expected.err)
}
})
}
}
func TestAttStatementCertChainVerify(t *testing.T) {
ca := testUtilsGenerateSelfSignedCert(t)
leaf := testUtilsGenerateLeafCert(t, ca)
testCases := []struct {
name string
have struct {
certs []*x509.Certificate
roots *x509.CertPool
}
expected struct {
empty bool
err string
}
}{
{
name: "ShouldFailEmptyChain",
have: struct {
certs []*x509.Certificate
roots *x509.CertPool
}{},
expected: struct {
empty bool
err string
}{
empty: true,
err: "empty chain",
},
},
{
name: "ShouldVerifyChainWithNilRoots",
have: struct {
certs []*x509.Certificate
roots *x509.CertPool
}{
certs: []*x509.Certificate{leaf, ca},
},
expected: struct {
empty bool
err string
}{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
chains, err := attStatementCertChainVerify(tc.have.certs, tc.have.roots, false, time.Time{})
if tc.expected.err == "" {
assert.NoError(t, err)
assert.NotEmpty(t, chains)
} else {
assert.EqualError(t, err, tc.expected.err)
if tc.expected.empty {
assert.Nil(t, chains)
}
}
})
}
}
func TestVerifyAttestationECDSAPublicKeyMatch(t *testing.T) {
eccKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
coseKeyBytes, err := webauthncbor.Marshal(webauthncose.EC2PublicKeyData{
PublicKeyData: webauthncose.PublicKeyData{
KeyType: int64(webauthncose.EllipticKey),
Algorithm: int64(webauthncose.AlgES256),
},
Curve: int64(webauthncose.P256),
XCoord: padP256Coord(eccKey.X),
YCoord: padP256Coord(eccKey.Y),
})
require.NoError(t, err)
okpKeyBytes, err := webauthncbor.Marshal(webauthncose.OKPPublicKeyData{
PublicKeyData: webauthncose.PublicKeyData{
KeyType: int64(webauthncose.OctetKey),
Algorithm: int64(webauthncose.AlgEdDSA),
},
Curve: 1,
XCoord: make([]byte, 32),
})
require.NoError(t, err)
matchingCert := testUtilsGenerateCertWithKey(t, &eccKey.PublicKey)
differentECCKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
differentCert := testUtilsGenerateCertWithKey(t, &differentECCKey.PublicKey)
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
rsaCert := testUtilsGenerateCertWithRSAKey(t, &rsaKey.PublicKey)
testCases := []struct {
name string
have struct {
credentialPublicKey []byte
cert *x509.Certificate
}
expected struct {
algorithm int64
err string
}
}{
{
name: "ShouldSucceed",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: coseKeyBytes,
cert: matchingCert,
},
expected: struct {
algorithm int64
err string
}{
algorithm: int64(webauthncose.AlgES256),
},
},
{
name: "ShouldFailInvalidPublicKey",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: []byte("invalid"),
cert: matchingCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Error parsing public key: Unsupported Public Key Type",
},
},
{
name: "ShouldFailNotECDSAKey",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: okpKeyBytes,
cert: matchingCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Attestation public key is not ECDSA",
},
},
{
name: "ShouldFailCertNotECDSA",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: coseKeyBytes,
cert: rsaCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Credential public key is not ECDSA",
},
},
{
name: "ShouldFailKeyMismatch",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: coseKeyBytes,
cert: differentCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Certificate public key does not match public key in authData",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
att := AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
CredentialPublicKey: tc.have.credentialPublicKey,
},
},
}
result, err := verifyAttestationECDSAPublicKeyMatch(att, tc.have.cert)
if tc.expected.err == "" {
assert.NoError(t, err)
assert.Equal(t, tc.expected.algorithm, result.Algorithm)
} else {
assert.EqualError(t, err, tc.expected.err)
}
})
}
}
func testUtilsGenerateSelfSignedCert(t *testing.T) *x509.Certificate {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}
func testUtilsGenerateLeafCert(t *testing.T, ca *x509.Certificate) *x509.Certificate {
t.Helper()
leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// We need the CA's private key to sign. Generate a new CA key pair for signing.
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// Recreate the CA cert with the new key so we can sign the leaf.
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
require.NoError(t, err)
caCert, err := x509.ParseCertificate(caDER)
require.NoError(t, err)
*ca = *caCert
leafTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "Test Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, caCert, &leafKey.PublicKey, caKey)
require.NoError(t, err)
leafCert, err := x509.ParseCertificate(leafDER)
require.NoError(t, err)
return leafCert
}
func testUtilsGenerateCertWithKey(t *testing.T, pub *ecdsa.PublicKey) *x509.Certificate {
t.Helper()
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "Test Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, caKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}
// padP256Coord left-pads a big.Int coordinate to 32 bytes (the fixed width for P-256).
// big.Int.Bytes() drops leading zeroes, which would cause COSE EC2 key validation to
// reject coordinates shorter than 32 bytes.
func padP256Coord(v *big.Int) []byte {
const p256ByteLen = 32
b := v.Bytes()
if len(b) >= p256ByteLen {
return b
}
padded := make([]byte, p256ByteLen)
copy(padded[p256ByteLen-len(b):], b)
return padded
}
func testUtilsGenerateCertWithRSAKey(t *testing.T, pub *rsa.PublicKey) *x509.Certificate {
t.Helper()
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: "Test RSA Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, caKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}
@@ -0,0 +1,33 @@
package webauthncbor
import "github.com/fxamacker/cbor/v2"
const nestedLevelsAllowed = 4
// ctap2CBORDecMode is the cbor.DecMode following the CTAP2 canonical CBOR encoding form
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
var ctap2CBORDecMode, _ = cbor.DecOptions{
DupMapKey: cbor.DupMapKeyEnforcedAPF,
MaxNestedLevels: nestedLevelsAllowed,
IndefLength: cbor.IndefLengthForbidden,
TagsMd: cbor.TagsForbidden,
}.DecMode()
var ctap2CBOREncMode, _ = cbor.CTAP2EncOptions().EncMode()
// Unmarshal parses the CBOR-encoded data into the value pointed to by v
// following the CTAP2 canonical CBOR encoding form.
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
func Unmarshal(data []byte, v any) error {
// TODO (james-d-elliott): investigate the specific use case for Unmarshal vs UnmarshalFirst to determine the edge cases where this may be useful.
_, err := ctap2CBORDecMode.UnmarshalFirst(data, v)
return err
}
// Marshal encodes the value pointed to by v
// following the CTAP2 canonical CBOR encoding form.
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
func Marshal(v any) ([]byte, error) {
return ctap2CBOREncMode.Marshal(v)
}
+137
View File
@@ -0,0 +1,137 @@
package webauthncose
const (
keyCannotDisplay = "Cannot display key"
)
const ecCoordSize = 32
// COSEAlgorithmIdentifier is a number identifying a cryptographic algorithm. The algorithm identifiers SHOULD be values
// registered in the IANA COSE Algorithms registry [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for
// instance, -7 for "ES256" and -257 for "RS256".
//
// Specification: §5.8.5. Cryptographic Algorithm Identifier (https://www.w3.org/TR/webauthn/#sctn-alg-identifier)
type COSEAlgorithmIdentifier int
const (
// AlgES256 ECDSA with SHA-256.
AlgES256 COSEAlgorithmIdentifier = -7
// AlgEdDSA EdDSA.
AlgEdDSA COSEAlgorithmIdentifier = -8
// AlgESP256 is ECDSA using P-256 curve with pre-hashed SHA-256 input.
AlgESP256 COSEAlgorithmIdentifier = -9
// AlgEd25519 is EdDSA using the Ed25519 curve specifically. Unlike [AlgEdDSA] which is the generic EdDSA
// identifier, this explicitly specifies the Ed25519 curve.
AlgEd25519 COSEAlgorithmIdentifier = -19
// AlgES384 ECDSA with SHA-384.
AlgES384 COSEAlgorithmIdentifier = -35
// AlgES512 ECDSA with SHA-512.
AlgES512 COSEAlgorithmIdentifier = -36
// AlgPS256 RSASSA-PSS with SHA-256.
AlgPS256 COSEAlgorithmIdentifier = -37
// AlgPS384 RSASSA-PSS with SHA-384.
AlgPS384 COSEAlgorithmIdentifier = -38
// AlgPS512 RSASSA-PSS with SHA-512.
AlgPS512 COSEAlgorithmIdentifier = -39
// AlgES256K is ECDSA using secp256k1 curve and SHA-256.
AlgES256K COSEAlgorithmIdentifier = -47
// AlgMLDSA44 is ML-DSA with parameter set ML-DSA-44 (FIPS 204).
AlgMLDSA44 COSEAlgorithmIdentifier = -48
// AlgMLDSA65 is ML-DSA with parameter set ML-DSA-65 (FIPS 204).
AlgMLDSA65 COSEAlgorithmIdentifier = -49
// AlgMLDSA87 is ML-DSA with parameter set ML-DSA-87 (FIPS 204).
AlgMLDSA87 COSEAlgorithmIdentifier = -50
// AlgESP384 is ECDSA using P-384 curve with pre-hashed SHA-384 input.
AlgESP384 COSEAlgorithmIdentifier = -51
// AlgESP512 is ECDSA using P-521 curve with pre-hashed SHA-512 input.
AlgESP512 COSEAlgorithmIdentifier = -52
// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256.
AlgRS256 COSEAlgorithmIdentifier = -257
// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384.
AlgRS384 COSEAlgorithmIdentifier = -258
// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512.
AlgRS512 COSEAlgorithmIdentifier = -259
// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1.
AlgRS1 COSEAlgorithmIdentifier = -65535
)
// COSEKeyType is The Key type derived from the IANA COSE AuthData.
type COSEKeyType int
const (
// KeyTypeReserved is a reserved value.
KeyTypeReserved COSEKeyType = iota
// OctetKey is an Octet Key.
OctetKey
// EllipticKey is an Elliptic Curve Public Key.
EllipticKey
// RSAKey is an RSA Public Key.
RSAKey
// Symmetric Keys.
Symmetric
// HSSLMS is the public key for HSS/LMS hash-based digital signature.
HSSLMS
// WalnutDSA is the public key for Walnut Digital Signature Algorithm.
WalnutDSA
// AKP is the key type for algorithm key pairs (i.e. ML-DSA).
AKP
)
// COSEEllipticCurve is an enumerator that represents the COSE Elliptic Curves.
//
// Specification: https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
type COSEEllipticCurve int
const (
// EllipticCurveReserved is the COSE EC Reserved value.
EllipticCurveReserved COSEEllipticCurve = iota
// P256 represents NIST P-256 also known as secp256r1.
P256
// P384 represents NIST P-384 also known as secp384r1.
P384
// P521 represents NIST P-521 also known as secp521r1.
P521
// X25519 for use w/ ECDH only.
X25519
// X448 for use w/ ECDH only.
X448
// Ed25519 for use w/ EdDSA only.
Ed25519
// Ed448 for use w/ EdDSA only.
Ed448
// Secp256k1 is the SECG secp256k1 curve.
Secp256k1
)
@@ -0,0 +1,10 @@
package webauthncose
import (
"crypto/ed25519"
"crypto/x509"
)
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
return x509.MarshalPKIXPublicKey(pub)
}
@@ -0,0 +1,7 @@
package webauthncose
import "math/big"
type ECDSASignature struct {
R, S *big.Int
}
+13
View File
@@ -0,0 +1,13 @@
package webauthncose
import "sync/atomic"
var allowBERIntegers atomic.Bool
// SetExperimentalInsecureAllowBERIntegers allows credentials which have BER integer encoding for their signatures
// which do not conform to the specification. This is an experimental option that may be removed without any notice
// and could potentially lead to zero-day exploits due to the ambiguity of encoding practices. This is not a recommended
// option.
func SetExperimentalInsecureAllowBERIntegers(value bool) {
allowBERIntegers.Store(value)
}
@@ -0,0 +1,492 @@
package webauthncose
import (
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"hash"
"math"
"math/big"
"github.com/go-webauthn/x/encoding/asn1"
"github.com/google/go-tpm/tpm2"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
)
// PublicKeyData The public key portion of a Relying Party-specific credential key pair, generated
// by an authenticator and returned to a Relying Party at registration time. We unpack this object
// using fxamacker's cbor library ("github.com/fxamacker/cbor/v2") which is why there are cbor tags
// included. The tag field values correspond to the IANA COSE keys that give their respective
// values.
//
// Specification: §6.4.1.1. Examples of credentialPublicKey Values Encoded in COSE_Key Format (https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples)
type PublicKeyData struct {
// Decode the results to int by default.
_struct bool `cbor:",keyasint" json:"public_key"` //nolint:govet,staticcheck
// The type of key created. Should be OKP, EC2, or RSA.
KeyType int64 `cbor:"1,keyasint" json:"kty"`
// A COSEAlgorithmIdentifier for the algorithm used to derive the key signature.
Algorithm int64 `cbor:"3,keyasint" json:"alg"`
}
type EC2PublicKeyData struct {
PublicKeyData
// If the key type is EC2, the curve on which we derive the signature from.
Curve int64 `cbor:"-1,keyasint,omitempty" json:"crv"`
// A byte string 32 bytes in length that holds the x coordinate of the key.
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
// A byte string 32 bytes in length that holds the y coordinate of the key.
YCoord []byte `cbor:"-3,keyasint,omitempty" json:"y"`
}
type RSAPublicKeyData struct {
PublicKeyData
// Represents the modulus parameter for the RSA algorithm.
Modulus []byte `cbor:"-1,keyasint,omitempty" json:"n"`
// Represents the exponent parameter for the RSA algorithm.
Exponent []byte `cbor:"-2,keyasint,omitempty" json:"e"`
}
type OKPPublicKeyData struct {
PublicKeyData
Curve int64
// A byte string that holds the x coordinate of the key.
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
}
// Verify Octet Key Pair (OKP) Public Key Signature.
func (k *OKPPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
if err := validateOKPPublicKey(k); err != nil {
return false, err
}
var key ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(key, k.XCoord)
return ed25519.Verify(key, data, sig), nil
}
// Verify Elliptic Curve Public Key Signature.
func (k *EC2PublicKeyData) Verify(data []byte, sig []byte) (valid bool, err error) {
if err = validateEC2PublicKey(k); err != nil {
return false, err
}
pubkey := &ecdsa.PublicKey{
Curve: ec2AlgCurve(k.Algorithm),
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}
h := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.Algorithm))
h.Write(data)
e := &ECDSASignature{}
var opts []asn1.UnmarshalOpt
if allowBERIntegers.Load() {
opts = append(opts, asn1.WithUnmarshalAllowBERIntegers(true))
}
if _, err = asn1.Unmarshal(sig, e, opts...); err != nil {
return false, ErrSigNotProvidedOrInvalid
}
return ecdsa.Verify(pubkey, h.Sum(nil), e.R, e.S), nil
}
// ToECDSA converts the EC2PublicKeyData to an ecdsa.PublicKey.
func (k *EC2PublicKeyData) ToECDSA() (key *ecdsa.PublicKey, err error) {
if err = validateEC2PublicKey(k); err != nil {
return nil, err
}
return &ecdsa.PublicKey{
Curve: ec2AlgCurve(k.Algorithm),
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}, nil
}
// Verify RSA Public Key Signature.
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (valid bool, err error) {
if err = validateRSAPublicKey(k); err != nil {
return false, err
}
e, _ := parseRSAPublicKeyDataExponent(k)
pubkey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(k.Modulus),
E: e,
}
coseAlg := COSEAlgorithmIdentifier(k.Algorithm)
algDetail, ok := COSESignatureAlgorithmDetails[coseAlg]
if !ok {
return false, ErrUnsupportedAlgorithm
}
hash := algDetail.hash
h := hash.New()
h.Write(data)
switch coseAlg {
case AlgPS256, AlgPS384, AlgPS512:
err = rsa.VerifyPSS(pubkey, hash, h.Sum(nil), sig, nil)
return err == nil, err
case AlgRS1, AlgRS256, AlgRS384, AlgRS512:
err = rsa.VerifyPKCS1v15(pubkey, hash, h.Sum(nil), sig)
return err == nil, err
default:
return false, ErrUnsupportedAlgorithm
}
}
// ParsePublicKey figures out what kind of COSE material was provided and create the data for the new key.
func ParsePublicKey(keyBytes []byte) (publicKey any, err error) {
pk := PublicKeyData{}
if err = webauthncbor.Unmarshal(keyBytes, &pk); err != nil {
return nil, ErrUnsupportedKey
}
switch COSEKeyType(pk.KeyType) {
case OctetKey:
var o OKPPublicKeyData
if err = webauthncbor.Unmarshal(keyBytes, &o); err != nil {
return nil, err
}
o.PublicKeyData = pk
if err = validateOKPPublicKey(&o); err != nil {
return nil, err
}
return o, nil
case EllipticKey:
var e EC2PublicKeyData
if err = webauthncbor.Unmarshal(keyBytes, &e); err != nil {
return nil, err
}
e.PublicKeyData = pk
if err = validateEC2PublicKey(&e); err != nil {
return nil, err
}
return e, nil
case RSAKey:
var r RSAPublicKeyData
if err = webauthncbor.Unmarshal(keyBytes, &r); err != nil {
return nil, err
}
r.PublicKeyData = pk
if err = validateRSAPublicKey(&r); err != nil {
return nil, err
}
return r, nil
default:
return nil, ErrUnsupportedKey
}
}
// ParseFIDOPublicKey is only used when the appID extension is configured by the assertion response.
func ParseFIDOPublicKey(keyBytes []byte) (data EC2PublicKeyData, err error) {
key, err := ecdh.P256().NewPublicKey(keyBytes)
if err != nil {
return data, fmt.Errorf("failed to parse FIDO public key: %w", err)
}
// Raw bytes for an uncompressed P-256 point: 0x04 || x(32) || y(32).
raw := key.Bytes()
return EC2PublicKeyData{
PublicKeyData: PublicKeyData{
KeyType: int64(EllipticKey),
Algorithm: int64(AlgES256),
},
Curve: int64(P256),
XCoord: raw[1 : 1+ecCoordSize],
YCoord: raw[1+ecCoordSize:],
}, nil
}
func VerifySignature(key any, data []byte, sig []byte) (bool, error) {
switch k := key.(type) {
case OKPPublicKeyData:
return k.Verify(data, sig)
case EC2PublicKeyData:
return k.Verify(data, sig)
case RSAPublicKeyData:
return k.Verify(data, sig)
default:
return false, ErrUnsupportedKey
}
}
func DisplayPublicKey(cpk []byte) string {
parsedKey, err := ParsePublicKey(cpk)
if err != nil {
return keyCannotDisplay
}
var data []byte
switch k := parsedKey.(type) {
case RSAPublicKeyData:
var e int
if e, err = parseRSAPublicKeyDataExponent(&k); err != nil {
return keyCannotDisplay
}
rKey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(k.Modulus),
E: e,
}
if data, err = x509.MarshalPKIXPublicKey(rKey); err != nil {
return keyCannotDisplay
}
case EC2PublicKeyData:
curve := ec2AlgCurve(k.Algorithm)
if curve == nil {
return keyCannotDisplay
}
eKey := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}
if data, err = x509.MarshalPKIXPublicKey(eKey); err != nil {
return keyCannotDisplay
}
case OKPPublicKeyData:
if len(k.XCoord) != ed25519.PublicKeySize {
return keyCannotDisplay
}
var oKey ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(oKey, k.XCoord)
if data, err = marshalEd25519PublicKey(oKey); err != nil {
return keyCannotDisplay
}
default:
return "Cannot display key of this type"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
})
return string(pemBytes)
}
func (k *EC2PublicKeyData) TPMCurveID() tpm2.TPMECCCurve {
switch COSEEllipticCurve(k.Curve) {
case P256:
return tpm2.TPMECCNistP256 // TPM_ECC_NIST_P256.
case P384:
return tpm2.TPMECCNistP384 // TPM_ECC_NIST_P384.
case P521:
return tpm2.TPMECCNistP521 // TPM_ECC_NIST_P521.
default:
return tpm2.TPMECCNone // TPM_ECC_NONE.
}
}
func ec2AlgCurve(coseAlg int64) elliptic.Curve {
switch COSEAlgorithmIdentifier(coseAlg) {
case AlgES512, AlgESP512:
return elliptic.P521()
case AlgES384, AlgESP384:
return elliptic.P384()
case AlgES256, AlgESP256:
return elliptic.P256()
default:
return nil
}
}
// SigAlgFromCOSEAlg return which signature algorithm is being used from the COSE Key.
func SigAlgFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) x509.SignatureAlgorithm {
d, ok := COSESignatureAlgorithmDetails[coseAlg]
if !ok {
return x509.UnknownSignatureAlgorithm
}
return d.sigAlg
}
// HasherFromCOSEAlg returns the Hashing interface to be used for a given COSE Algorithm.
func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) hash.Hash {
d, ok := COSESignatureAlgorithmDetails[coseAlg]
if !ok {
// default to SHA256? Why not.
return crypto.SHA256.New()
}
return d.hash.New()
}
var COSESignatureAlgorithmDetails = map[COSEAlgorithmIdentifier]struct {
name string
hash crypto.Hash
sigAlg x509.SignatureAlgorithm
}{
AlgRS1: {"SHA1-RSA", crypto.SHA1, x509.SHA1WithRSA},
AlgRS256: {"SHA256-RSA", crypto.SHA256, x509.SHA256WithRSA},
AlgRS384: {"SHA384-RSA", crypto.SHA384, x509.SHA384WithRSA},
AlgRS512: {"SHA512-RSA", crypto.SHA512, x509.SHA512WithRSA},
AlgPS256: {"SHA256-RSAPSS", crypto.SHA256, x509.SHA256WithRSAPSS},
AlgPS384: {"SHA384-RSAPSS", crypto.SHA384, x509.SHA384WithRSAPSS},
AlgPS512: {"SHA512-RSAPSS", crypto.SHA512, x509.SHA512WithRSAPSS},
AlgES256: {"ECDSA-SHA256", crypto.SHA256, x509.ECDSAWithSHA256},
AlgESP256: {"ECDSA-SHA256-Prehashed", crypto.SHA256, x509.ECDSAWithSHA256},
AlgES384: {"ECDSA-SHA384", crypto.SHA384, x509.ECDSAWithSHA384},
AlgESP384: {"ECDSA-SHA384-Prehashed", crypto.SHA384, x509.ECDSAWithSHA384},
AlgES512: {"ECDSA-SHA512", crypto.SHA512, x509.ECDSAWithSHA512},
AlgESP512: {"ECDSA-SHA512-Prehashed", crypto.SHA512, x509.ECDSAWithSHA512},
AlgEdDSA: {"EdDSA", crypto.SHA512, x509.PureEd25519},
AlgEd25519: {"Ed25519", crypto.SHA512, x509.PureEd25519},
}
type Error struct {
// Short name for the type of error that has occurred.
Type string `json:"type"`
// Additional details about the error.
Details string `json:"error"`
// Information to help debug the error.
DevInfo string `json:"debug"`
}
var (
ErrUnsupportedKey = &Error{
Type: "invalid_key_type",
Details: "Unsupported Public Key Type",
}
ErrUnsupportedAlgorithm = &Error{
Type: "unsupported_key_algorithm",
Details: "Unsupported public key algorithm",
}
ErrSigNotProvidedOrInvalid = &Error{
Type: "signature_not_provided_or_invalid",
Details: "Signature invalid or not provided",
}
)
func (err *Error) Error() string {
return err.Details
}
func (passedError *Error) WithDetails(details string) *Error {
err := *passedError
err.Details = details
return &err
}
func validateOKPPublicKey(k *OKPPublicKeyData) error {
if len(k.XCoord) != ed25519.PublicKeySize {
return ErrUnsupportedKey.WithDetails(fmt.Sprintf("OKP key x coordinate has invalid length %d, expected %d", len(k.XCoord), ed25519.PublicKeySize))
}
return nil
}
func validateEC2PublicKey(k *EC2PublicKeyData) error {
curve := ec2AlgCurve(k.Algorithm)
if curve == nil {
return ErrUnsupportedAlgorithm.WithDetails("Unsupported EC2 algorithm")
}
byteLen := (curve.Params().BitSize + 7) / 8
if len(k.XCoord) != byteLen || len(k.YCoord) != byteLen {
return ErrUnsupportedKey.WithDetails("EC2 key x or y coordinate has invalid length")
}
x := new(big.Int).SetBytes(k.XCoord)
y := new(big.Int).SetBytes(k.YCoord)
if !curve.IsOnCurve(x, y) {
return ErrUnsupportedKey.WithDetails("EC2 key point is not on curve")
}
return nil
}
func validateRSAPublicKey(k *RSAPublicKeyData) error {
n := new(big.Int).SetBytes(k.Modulus)
if n.Sign() <= 0 {
return ErrUnsupportedKey.WithDetails("RSA key contains zero or empty modulus")
}
if _, err := parseRSAPublicKeyDataExponent(k); err != nil {
return ErrUnsupportedKey.WithDetails(fmt.Sprintf("RSA key contains invalid exponent: %v", err))
}
return nil
}
func parseRSAPublicKeyDataExponent(k *RSAPublicKeyData) (exp int, err error) {
if k == nil {
return 0, fmt.Errorf("invalid key")
}
if len(k.Exponent) == 0 {
return 0, fmt.Errorf("invalid exponent length")
}
for _, b := range k.Exponent {
if exp > (math.MaxInt >> 8) {
return 0, ErrUnsupportedKey
}
exp = (exp << 8) | int(b)
}
if exp <= 0 {
return 0, ErrUnsupportedKey
}
return exp, nil
}
@@ -0,0 +1,680 @@
package webauthncose
import (
"crypto/ecdh"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-tpm/tpm2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
)
// TestOKPSignatureVerification is a compatibility test to ensure that removing
// a previously used dependency doesn't introduce new issues.
//
// Since OKPs are used to represent Ed25519 keys, this test largely ensures
// that the underlying Ed25519 signature verification passes.
func TestOKPSignatureVerification(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
data := []byte("Sample data to sign")
validSig := ed25519.Sign(priv, data)
invalidSig := []byte("invalid")
key := OKPPublicKeyData{
XCoord: pub,
}
// Test that a valid signature passes.
ok, err := key.Verify(data, validSig)
assert.True(t, ok)
assert.NoError(t, err)
ok, err = key.Verify(data, invalidSig)
assert.NoError(t, err)
assert.False(t, ok)
}
func TestP256SignatureVerification(t *testing.T) {
// Private/public key pair was generated with the following:
//
// $ openssl ecparam -genkey -name secp256r1 -noout -out private_key.pem
// $ openssl ec -in private_key.pem -noout -text
// Private-Key: (256 bit)
// priv:
// 48:7f:36:1d:df:d7:34:40:e7:07:f4:da:a6:77:5b:
// 37:68:59:e8:a3:c9:f2:9b:3b:b6:94:a1:29:27:c0:
// 21:3c
// pub:
// 04:f7:39:f8:c7:7b:32:f4:d5:f1:32:65:86:1f:eb:
// d7:6e:7a:9c:61:a1:14:0d:29:6b:8c:16:30:25:08:
// 87:03:16:c2:49:70:ad:78:11:cc:d9:da:7f:1b:88:
// f2:02:be:ba:c7:70:66:3e:f5:8b:a6:83:46:18:6d:
// d7:78:20:0d:d4
// ASN1 OID: prime256v1
// NIST CURVE: P-256
// ----.
pubX, err := hex.DecodeString("f739f8c77b32f4d5f13265861febd76e7a9c61a1140d296b8c16302508870316")
assert.NoError(t, err)
pubY, err := hex.DecodeString("c24970ad7811ccd9da7f1b88f202bebac770663ef58ba68346186dd778200dd4")
assert.NoError(t, err)
key := EC2PublicKeyData{
// These constants are from https://datatracker.ietf.org/doc/rfc9053/
// (see "ECDSA" and "Elliptic Curve Keys").
PublicKeyData: PublicKeyData{
KeyType: 2, // EC.
Algorithm: -7, // "ES256".
},
Curve: 1, // P-256.
XCoord: pubX,
YCoord: pubY,
}
data := []byte("webauthnFTW")
validSig, err := hex.DecodeString("3045022053584980793ee4ec01d583f303604c4f85a7e87df3fe9551962c5ab69a5ce27b022100c801fd6186ca4681e87fbbb97c5cb659f039473995a75a9a9dffea2708d6f8fb")
assert.NoError(t, err)
ok, err := VerifySignature(key, data, validSig)
assert.True(t, ok)
assert.NoError(t, err)
ok, err = VerifySignature(key, []byte("webauthnFTL"), validSig)
assert.NoError(t, err)
assert.False(t, ok)
}
func TestOKPDisplayPublicKey(t *testing.T) {
// Sample public key generated from ed25519.GenerateKey(rand.Reader).
var pub ed25519.PublicKey = []byte{0x7b, 0x88, 0x10, 0x24, 0xad, 0xc9, 0x82, 0xd3, 0x80, 0xb8, 0x77, 0x1e, 0x3b, 0x9b, 0xf8, 0xe4, 0xb3, 0x99, 0x8b, 0xc7, 0xd0, 0x58, 0x30, 0x66, 0x2, 0xce, 0x4d, 0xf, 0x2f, 0xe4, 0xb7, 0x81}
// The PEM encoded representation of the public key in PKIX, ASN.1 DER format.
expected := `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAe4gQJK3JgtOAuHceO5v45LOZi8fQWDBmAs5NDy/kt4E=
-----END PUBLIC KEY-----
`
key := OKPPublicKeyData{
XCoord: pub,
PublicKeyData: PublicKeyData{
KeyType: int64(OctetKey),
},
}
// Get the CBOR-encoded representation of the OKPPublicKeyData.
buf, _ := webauthncbor.Marshal(key)
actual := DisplayPublicKey(buf)
assert.Equal(t, expected, actual)
}
func TestRSAExponent(t *testing.T) {
testCases := []struct {
name string
have *RSAPublicKeyData
expected int
err string
}{
{
"ShouldHandle3ByteExponent",
&RSAPublicKeyData{
Exponent: []byte{0x01, 0x00, 0x01},
},
65537,
"",
},
{
"ShouldHandle3ByteExponentAlt",
&RSAPublicKeyData{
Exponent: []byte{0x01, 0x00, 0x02},
},
65538,
"",
},
{
"ShouldHandle3ByteExponentLarge",
&RSAPublicKeyData{
Exponent: []byte{0xff, 0xff, 0xff},
},
16777215,
"",
},
{
"ShouldHandle4ByteExponent",
&RSAPublicKeyData{
Exponent: []byte{0x01, 0x00, 0x02, 0xff},
},
16777983,
"",
},
{
"ShouldHandleZeroLength",
&RSAPublicKeyData{
Exponent: []byte{},
},
0,
"invalid exponent length",
},
{
"ShouldHandleNilExponent",
&RSAPublicKeyData{
Exponent: nil,
},
0,
"invalid exponent length",
},
{
"ShouldHandleNilKey",
nil,
0,
"invalid key",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := parseRSAPublicKeyDataExponent(tc.have)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
assert.Equal(t, 0, actual)
} else {
assert.Equal(t, tc.expected, actual)
assert.NoError(t, err)
}
})
}
}
func TestUnsupportedKeyMethods(t *testing.T) {
verified, err := VerifySignature(PublicKeyData{}, nil, nil)
assert.EqualError(t, err, "Unsupported Public Key Type")
assert.False(t, verified)
}
func TestParsePublicKey(t *testing.T) {
testCases := []struct {
name string
attestationObject string
clientDataJSON string
authenticatorData string
signature string
expected any
tpmcurve tpm2.TPMECCCurve
isEC2Key bool
isRSAKey bool
isOKPKey bool
}{
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256
"ShouldHandleTestVector17",
"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0xaf, 0xef, 0xa1, 0x6f, 0x97, 0xca, 0x9b, 0x2d, 0x23, 0xeb, 0x86, 0xcc, 0xb6, 0x40, 0x98, 0xd2, 0xd, 0xb9, 0x8, 0x56, 0x6, 0x2e, 0xb2, 0x49, 0xc3, 0x3a, 0x9b, 0x67, 0x2f, 0x26, 0xdf, 0x61}, YCoord: []uint8{0x93, 0xa, 0x56, 0xb8, 0x7a, 0x2f, 0xca, 0x66, 0x33, 0x4b, 0x3, 0x45, 0x8a, 0xbf, 0x87, 0x97, 0x17, 0xc1, 0x2c, 0xc6, 0x8e, 0xd7, 0x32, 0x90, 0xaf, 0x2e, 0x26, 0x64, 0x79, 0x6b, 0x92, 0x20}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-self-es256
"ShouldHandleTestVector19",
"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0xeb, 0x15, 0x1c, 0x81, 0x76, 0xb2, 0x25, 0xcc, 0x65, 0x15, 0x59, 0xfe, 0xcf, 0x7, 0xaf, 0x45, 0xf, 0xd8, 0x58, 0x2, 0x4, 0x66, 0x56, 0xb3, 0x4c, 0x18, 0xf6, 0xcf, 0x19, 0x38, 0x43, 0xc5}, YCoord: []uint8{0x92, 0x7b, 0x8a, 0xa4, 0x27, 0xa2, 0xbe, 0x1b, 0x88, 0x34, 0xd2, 0x33, 0xa2, 0xd3, 0x4f, 0x61, 0xf1, 0x3b, 0xfd, 0x44, 0x11, 0x9c, 0x32, 0x5d, 0x58, 0x96, 0xe1, 0x83, 0xfe, 0xe4, 0x84, 0xf2}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256-crossOrigin
"ShouldHandleTestVector21",
"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54500000000883f4f6014f19c09d87aa38123be48d000206e1050c0d2ca2f07c755cb2c66a74c64fa43065c18f938354d9915db2bd5ce57a501020326200121582022200a473f90b11078851550d03b4e44a2279f8c4eca27b3153dedfe03e4e97d225820cbd0be95e746ad6f5a8191be11756e4c0420e72f65b466d39bc56b8b123a9c6e",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a226832716c463771445f65356c5f505f62796b7945377135645650674547685f49584a6b655737736e4d5463222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a2039327063545644304162792d713464746d6a366566673d3d227d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50500000000",
"304402204396b14b216ed47920dc359e46aa0a1d4a912cf9d50f25a58ec236a11db4cf5e02204fdb59ff01656c4b0868e415436a464b0e30e94b02c719b995afaba9c917146b",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0x22, 0x20, 0xa, 0x47, 0x3f, 0x90, 0xb1, 0x10, 0x78, 0x85, 0x15, 0x50, 0xd0, 0x3b, 0x4e, 0x44, 0xa2, 0x27, 0x9f, 0x8c, 0x4e, 0xca, 0x27, 0xb3, 0x15, 0x3d, 0xed, 0xfe, 0x3, 0xe4, 0xe9, 0x7d}, YCoord: []uint8{0xcb, 0xd0, 0xbe, 0x95, 0xe7, 0x46, 0xad, 0x6f, 0x5a, 0x81, 0x91, 0xbe, 0x11, 0x75, 0x6e, 0x4c, 0x4, 0x20, 0xe7, 0x2f, 0x65, 0xb4, 0x66, 0xd3, 0x9b, 0xc5, 0x6b, 0x8b, 0x12, 0x3a, 0x9c, 0x6e}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256-topOrigin
"ShouldHandleTestVector23",
"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b5410000000097586fd09799a76401c200455099ef2a0020b8ad59b996047ab18e2ceb57206c362da57458793481f4a8ebf101c7ca7cc0f1a5010203262001215820a1c47c1d82da4ebe82cd72207102b380670701993bc35398ae2e5726427fe01d22582086c1080d82987028c7f54ecb1b01185de243b359294a0ed210cd47480f0adc88",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22315570636a4b53324b6f34377379486a7372787a68572d466f51465132796b3572426c584f6573656f4759222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a747275652c22746f704f726967696e223a2268747470733a2f2f6578616d706c652e636f6d222c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205569466f4a4d565251484441465746693476785570513d3d227d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50500000000",
"304402206a19613fa8cfacfc8027272aec5dae3555fea9f983d841581466678d71e6761a02207a9785ba22e48eb18525850357d9dc70795aaad2e6021159c4a4a183146eaa71",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0xa1, 0xc4, 0x7c, 0x1d, 0x82, 0xda, 0x4e, 0xbe, 0x82, 0xcd, 0x72, 0x20, 0x71, 0x2, 0xb3, 0x80, 0x67, 0x7, 0x1, 0x99, 0x3b, 0xc3, 0x53, 0x98, 0xae, 0x2e, 0x57, 0x26, 0x42, 0x7f, 0xe0, 0x1d}, YCoord: []uint8{0x86, 0xc1, 0x8, 0xd, 0x82, 0x98, 0x70, 0x28, 0xc7, 0xf5, 0x4e, 0xcb, 0x1b, 0x1, 0x18, 0x5d, 0xe2, 0x43, 0xb3, 0x59, 0x29, 0x4a, 0xe, 0xd2, 0x10, 0xcd, 0x47, 0x48, 0xf, 0xa, 0xdc, 0x88}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256-long-credential-id
"ShouldHandleTestVector25",
"a363666d74646e6f6e656761747453746d74a0686175746844617461590483bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b549000000008f3360c2cd1b0ac14ffe0795c5d2638e03ff3a761a4e1674ad6c4305869435c0eee9c286172c229bb91b48b4ada140c0863417031305cce5b4a27a88d7fe728a5f5a627de771b4b40e77f187980c124f9fe832d7136010436a056cce716680587d23187cf1fc2c62ae86fc3e508ee9617ffc74fbc10488ec16ec5e9096328669a898709b655e549738c666c1ae6281dc3b5f733c251d3eefb76ee70a3805ca91bcc18e49c8dc7f63ebcb486ba8c3d6ab52b88ff72c6a5bb47c32f3ee8683a3ddc8abf60870448ec8a21b5bdcb183c7dead870255575a6df96eb1b6a2a1019780cba9e4887b17ff1164bbbcc10eb0d86ed75984cd3fa3419103024507dfd9ce8f92c56af7914cb0bb50b87ba82a312bb7dcd93028dbdcd6adb266979667158335171e3682d37755701edbf9d872846a291d49e57ef09da1ec637f5052ed2aa7407f7e61827468e94b461844f4c67be5fa9c6055a566f8fdfc29d4bf78a9ff275f552cc68ba543fa3962eea36fd1ea8453764577d021d0a181efc1f6100ab2e4110039e21ee16970bda7432b6134492155afc126295b3a2eccd12c66a68e340969e995e3e8c9c476e395cfc21203414110779474f1c9797406637dbe414f132519d3bf0ce4f01734ef0e1a12c3ad604ff15d766b1624db6a5a7ccbff7bc35c9908df94aba277e0af48f04ff3d16381c47e5a37ed3988a67a3b1ecaa926336b33391fff04128f869991c9fabd905b6fe3ceef5f8b630ec1c5d2636d5b1961ad5ca5004170f6f5e482792aad989b0287fe91e5c479403397152f1fa56aa79b156eb47e6c8ea3eb175c34cfb38ad8e772874639b1023d4d01395c94e55831671cc022aa6fa1e02a02c2e4abc776f6960e51f83b71a8c0f207b6a347573977812c9aa5480b0011aa739bd4b76c18c000cc4757cceccb920f007c40c00e37e5ab21476cd9f6054a8fffb55a108f5c706e2cea2049d81fd321ff47d2a5761b0800955ab1d4f4889f55a84e2601c684f17a4ade7453ea49591d0b59c8d9a765052f62219cf6ef4a5dd9539f0617d6ebbebce7c000455475d18449e25c49ef9a1e3efe18c09082ebe2058d7c347defaa92f0664553b805c7d76bbfce5f330aca220ac90a789380fc479ea0d8793205813cca590a912f699ad52f991a1bc0a503c3ec4b2a696719e3c26591a87127f7305cc7e72f4c8e39355ebb06a5b1042990f38710ee7aa612ee4374bb82e878585a70a96c2a6b47f101a4ff154be4fd76a3167577a5cc54d9167c154c69ac35485e44cc898b719e1be3cc9c0fb5624b8f8a0dae10947a41bf848b6c1bb33d1006ec077d7e286e3f2a7b4843716390119449fe2721e81a5ed2333d331c7120765da58fadae73c19d9a8c4509cf8ac1e9d98b799a5274509069739b5823f3fb496663820033426988eefca53e580e0f9e0dfe0992fc2e53a97e053639f98577058f995bdbd41cefdba50102032620012158203b8176b7504489cc593046d7988abb7905a742de6ac2cdc748a873c663e90cb12258201436d5edc9a75f23999eef9d5950a5c2455514ee1014084720f841a06b828a11",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22377833727057334f53505a307045664d396a75566d53574d36485a4935634f573875384d6f647047446a73222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
"304502203ecef83fb12a0cae7841055f9f87103a99fd14b424194bbf06c4623d3ee6e3fd022100d2ace346db262b1374a6b70faa51f518a42ddca13a4125ce6f5052a75bac9fb6",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0x3b, 0x81, 0x76, 0xb7, 0x50, 0x44, 0x89, 0xcc, 0x59, 0x30, 0x46, 0xd7, 0x98, 0x8a, 0xbb, 0x79, 0x5, 0xa7, 0x42, 0xde, 0x6a, 0xc2, 0xcd, 0xc7, 0x48, 0xa8, 0x73, 0xc6, 0x63, 0xe9, 0xc, 0xb1}, YCoord: []uint8{0x14, 0x36, 0xd5, 0xed, 0xc9, 0xa7, 0x5f, 0x23, 0x99, 0x9e, 0xef, 0x9d, 0x59, 0x50, 0xa5, 0xc2, 0x45, 0x55, 0x14, 0xee, 0x10, 0x14, 0x8, 0x47, 0x20, 0xf8, 0x41, 0xa0, 0x6b, 0x82, 0x8a, 0x11}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-es256
"ShouldHandleTestVector27",
"a363666d74667061636b65646761747453746d74a363616c67266373696758473045022025fcee945801b94e63d7c029e6f761654cf02e7100d5364a3b90e03daa6276fc022100eabcdf4ce19feb0980e829c3b6137079b18e42f43ce5c3c573b83368794f354c637835638159022530820221308201c8a00302010202110088c220f83c8ef1feafe94deae45faad0300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004a91ba4389409dd38a428141940ca8feb1ac0d7b4350558104a3777a49322f3798440f378b3398ab2d3bb7bf91322c92eb23556f59ad0a836fec4c7663b0e4dc3a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414a589ba72d060842ab11f74fb246bdedab16f9b9b301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d040302034700304402201726b9d85ecd8a5ed51163722ca3a20886fd9b242a0aa0453d442116075defd502207ef471e530ac87961a88a7f0d0c17b091ffc6b9238d30f79f635b417be5910e768617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d00000000876ca4f52071c3e9b25509ef2cdf7ed60020c9a6f5b3462d02873fea0c56862234f99f081728084e511bb7760201a89054a5a50102032620012158201cf27f25da591208a4239c2e324f104f585525479a29edeedd830f48e77aeae522582059e4b7da6c0106e206ce390c93ab98a15a5ec3887e57f0cc2bece803b920c423",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2273524276704770587676463446524841565833496d4b4130453958773858306b526a44426c4d6668726255222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20415a4d77794d78496244382d756775464e70367238513d3d227d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
"30460221009d8d54895393894d37b9fa7bdfbcff05403de3cf0d6443ffb394fa239f101579022100c8871288f19c6c48a3b64c09d39868c12d16ed80ea4c5d8890288975c0272f50",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0x1c, 0xf2, 0x7f, 0x25, 0xda, 0x59, 0x12, 0x8, 0xa4, 0x23, 0x9c, 0x2e, 0x32, 0x4f, 0x10, 0x4f, 0x58, 0x55, 0x25, 0x47, 0x9a, 0x29, 0xed, 0xee, 0xdd, 0x83, 0xf, 0x48, 0xe7, 0x7a, 0xea, 0xe5}, YCoord: []uint8{0x59, 0xe4, 0xb7, 0xda, 0x6c, 0x1, 0x6, 0xe2, 0x6, 0xce, 0x39, 0xc, 0x93, 0xab, 0x98, 0xa1, 0x5a, 0x5e, 0xc3, 0x88, 0x7e, 0x57, 0xf0, 0xcc, 0x2b, 0xec, 0xe8, 0x3, 0xb9, 0x20, 0xc4, 0x23}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-es384
"ShouldHandleTestVector29",
"a363666d74667061636b65646761747453746d74a363616c67266373696758473045022100c56ecc970b7843833e0f461fde26233f61eb395161d481558c08b9c6ed61675b022029f5e05033705cd0f9b0a07e149468ec308a4f84906409efdceb1da20a7518d6637835638159022530820221308201c7a00302010202103d0a5588bb87ebb1d4cee4a1807c1b7c300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000417e5cc91d676d370e36aa7de40c25aacb45a3845f13d2932088ece2270b9b431241c219c22d0c256c9438ade00f2c05e62f8ef906b9b997ae9f3c460c2db66f5a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414c7c8dd95382a2230e4c0dd3664338fa908169a9c301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020348003045022054068cc9ae038937b7c468c307edb9c6927ffdeb6a20070c483eb40330f99f10022100cf41953919c3c04693d6b1f42a613753f204e70e85fc6e9b17036170b83596e068617574684461746158c5bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55900000000e950dcda3bdae1d087cda380a897848b0020953ae2dd9f28b1a1d5802c83e1f65833bb9769a08de82d812bc27c13fc6f06a9a5010203382220022158304866bd8b01da789e9eb806e5eab05ae5a638542296ab057a2f1bbce9b58f8a08b9171390b58a37ac7fffc2c5f45857da2258302a0b024c7f4b72072a1f96bd30a7261aae9571dd39870eb29e55c0941c6b08e89629a1ea1216aa64ce57c2807bf3901a",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225f304844306c32396957623759654b4f39655277516545333753614649454574646941726f4b307446464d222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
"3065023100e4efbb46745ed00e67c4d51ab2bacab2af62ffa8b7c5fecec6d7d9bf2582275034a713a3dd731685eee81adfaf6aa63f0230161655353f07e018a3c2539f8de7c8c4cf88d4c32d2be29fe4e76fa096ecc9458bbfe0895d57129ab324130e6f0692db",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -35}, Curve: 2, XCoord: []uint8{0x48, 0x66, 0xbd, 0x8b, 0x1, 0xda, 0x78, 0x9e, 0x9e, 0xb8, 0x6, 0xe5, 0xea, 0xb0, 0x5a, 0xe5, 0xa6, 0x38, 0x54, 0x22, 0x96, 0xab, 0x5, 0x7a, 0x2f, 0x1b, 0xbc, 0xe9, 0xb5, 0x8f, 0x8a, 0x8, 0xb9, 0x17, 0x13, 0x90, 0xb5, 0x8a, 0x37, 0xac, 0x7f, 0xff, 0xc2, 0xc5, 0xf4, 0x58, 0x57, 0xda}, YCoord: []uint8{0x2a, 0xb, 0x2, 0x4c, 0x7f, 0x4b, 0x72, 0x7, 0x2a, 0x1f, 0x96, 0xbd, 0x30, 0xa7, 0x26, 0x1a, 0xae, 0x95, 0x71, 0xdd, 0x39, 0x87, 0xe, 0xb2, 0x9e, 0x55, 0xc0, 0x94, 0x1c, 0x6b, 0x8, 0xe8, 0x96, 0x29, 0xa1, 0xea, 0x12, 0x16, 0xaa, 0x64, 0xce, 0x57, 0xc2, 0x80, 0x7b, 0xf3, 0x90, 0x1a}},
tpm2.TPMECCNistP384,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-es512
"ShouldHandleTestVector31",
"a363666d74667061636b65646761747453746d74a363616c67266373696758483046022100c48fcbd826bbc79680802026688d41ab6da8c3a1d22ab6cecf36c8d7695d22500221008767dfe591277e973078d5692c8c35cf9d579792822e7145c96a0ac4515df5b0637835638159022730820223308201c8a0030201020211008a128b7ebe52b993835779e6d9b81355300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004940b68885291536e2f7c60c05acfb252e7eebcf4304425dd93ab7b1962f20492bf18dc0f12862599e81fb764ac92151f9a78fcbb35d7a26c8c52949b18133c06a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604143ffad863abcd3dc5717b8a252189f41af97e7f31301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020349003046022100832c8b64c4f0188bd32e1bec63e13301cdc03165d3ef840d1f3dabb9a5719f83022100add57a9d5bedec98f29222dfc97ea795d055ee13a02a153d02be9ce00aedeb9168617574684461746158e9bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d0000000039d8ce6a3cf61025775083a738e5c2540020d17d5af7e3f37c56622a67c8462c9e1c6336dfccb8b61d359dc47378dba58ce4a5010203382320032158420083240a2c3ad21a3dc0a6daa3d8bc05a46d7cd9825ba010ae2a22686c2d6d663d7d5f678987fb1e767542e63dc197ae915e25f8ee284651af29066910a2cc083f50225842017337df47ab5cce5d716ef8caffa97a3012689b1f326ea6c43a1ba9596c72f71f0122390143552b42be772b4c35ffb961220c743b486a601ea4cb6d5412f5b078d3",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22434e4d5a4447334c5055384d746c6d674d7a763136684a4e337a61677a5450564945734e65694b6f7a436279355046703067416f5848657a2d794c67386366306d6f66557669306c3653313565416a6471716d31635637394f6d72616b7a6e544253706f666278644c3479484777525234476b66563630546855473374793536714a4d334b6577635a6b7679354e376134574674434f7a7671416f71553745445a6a7a6c7149454569436b222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
"3081870242009bda02fe384e77bcb9fb42b07c395b7a53ec9d9616dd0308ab8495c2141c8364c7d16e212a4a4fb8e3987ff6c99eafd64d8484fd28c3fc7968f658a9033d1bb1b802416383e9f3ee20c691b66620299fef36bea2df4d39c92b2ead92f58e7b79ab0d9864d2ebf3b0dcc66ea13234492ccee6e9d421db43c959bcb94c162dc9494136c9f6",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -36}, Curve: 3, XCoord: []uint8{0x0, 0x83, 0x24, 0xa, 0x2c, 0x3a, 0xd2, 0x1a, 0x3d, 0xc0, 0xa6, 0xda, 0xa3, 0xd8, 0xbc, 0x5, 0xa4, 0x6d, 0x7c, 0xd9, 0x82, 0x5b, 0xa0, 0x10, 0xae, 0x2a, 0x22, 0x68, 0x6c, 0x2d, 0x6d, 0x66, 0x3d, 0x7d, 0x5f, 0x67, 0x89, 0x87, 0xfb, 0x1e, 0x76, 0x75, 0x42, 0xe6, 0x3d, 0xc1, 0x97, 0xae, 0x91, 0x5e, 0x25, 0xf8, 0xee, 0x28, 0x46, 0x51, 0xaf, 0x29, 0x6, 0x69, 0x10, 0xa2, 0xcc, 0x8, 0x3f, 0x50}, YCoord: []uint8{0x1, 0x73, 0x37, 0xdf, 0x47, 0xab, 0x5c, 0xce, 0x5d, 0x71, 0x6e, 0xf8, 0xca, 0xff, 0xa9, 0x7a, 0x30, 0x12, 0x68, 0x9b, 0x1f, 0x32, 0x6e, 0xa6, 0xc4, 0x3a, 0x1b, 0xa9, 0x59, 0x6c, 0x72, 0xf7, 0x1f, 0x1, 0x22, 0x39, 0x1, 0x43, 0x55, 0x2b, 0x42, 0xbe, 0x77, 0x2b, 0x4c, 0x35, 0xff, 0xb9, 0x61, 0x22, 0xc, 0x74, 0x3b, 0x48, 0x6a, 0x60, 0x1e, 0xa4, 0xcb, 0x6d, 0x54, 0x12, 0xf5, 0xb0, 0x78, 0xd3}},
tpm2.TPMECCNistP521,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-rs256
"ShouldHandleTestVector33",
"a363666d74667061636b65646761747453746d74a363616c672663736967584730450221008b8c5c6ea8c142c032e0be69e1353d44461c5c9109941cdda951b976eb95b6b302204d52f406c19e254b3ff9589bd18070fb055ac8db12fdd0a6734bea9d7168e900637835638159022630820222308201c7a00302010202101f6fb7a5ece81b45896b983a995da5f3300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004b7b36b7542a11120b443c794d0c99fdc25a06b76586413d81e086163ef6fe147a557afc34e2861d9057d6d465d4705a0310550bdeeb5f35ee35b9425ab859981a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414fb37b647bccfb9e54d989eaaacc1633868703fb3301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020349003046022100b86bc129d92afca7d9869a39f70f139a305b4073a39eb654d81424bed5757d91022100cf9f7c60cab7c4a7d3e7f0020f281a93d4fd0a9f95121b989f56932a68885fba68617574684461746159021bbfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000428f8878298b9862a36ad8c7527bfef20020992a18acc83f67533600c1138a4b4c4bd236de13629cf025ed17cb00b00b74dfa4010303390100205901b403fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012143010001",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224b56395a39667150356978617970346e596d7834794e6f33617562597a5333536d75757459423462784d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
"01063d52d7c39b4d432fc7063c5d93e582bdcb16889cd71f888d67d880ea730a428498d3bc8e1ee11f2b1ecbe6c292b118c55ffaaddefa8cad0a54dd137c51f1eec673f1bb6c4d1789d6826a222b22d0f585fc901fdc933212e579d199b89d672aa44891333e6a1355536025e82b25590256c3538229b55737083b2f6b9377e49e2472f11952f79fdd0da180b5ffd901b4049a8f081bb40711bef76c62aed943571f2d0575304cb549d68d8892f95086a30f93716aee818f8dc06e96c0d5e0ed4cfa9fd8773d90464b68cf140f7986666ff9c9e3302acd0535d60d769f465e2ab57ef8aabc89fccfef7ba32a64154a8b3d26be2298f470b8cc5377dbe3dfd4b0b45f8f01e63bde6cfc76b62771f9b70aa27cf40152cad93aa5acd784fd4b90f676e2ea828d0bf2400aebbaae4153e5838f537f88b6228346782a93a899be66ec77de45b3efcf311da6321c92e6b0cd11bfe653bf3e98cee8e341f02d67dbb6f9c98d9e8178090cfb5b70fbc6d541599ac794ae2f1d4de1286ec8de8c2daf7b1d15c8438e90d924df5c19045220a4c8438c1b979bbe016cf3d0eeec23c3999d4882cc645b776de930756612cdc6dd398160ff02a6",
RSAPublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 3, Algorithm: -257}, Modulus: []uint8{0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, Exponent: []uint8{0x1, 0x0, 0x1}},
tpm2.TPMECCNistP256,
false,
true,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-ed25519
"ShouldHandleTestVector35",
"a363666d74667061636b65646761747453746d74a363616c672663736967584730450220730a54d4f76cb1f2b7dd4a5a6eee3374e3c8a60fb3c4daa527c9277e365b64aa0221008b31a04a28cc4148b14c42a916548ee7f430bc7629295b42ee93e5d1aaba8ee6637835638159022530820221308201c7a00302010202106e391f23f57150dc7a12dad18f2b43ad300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d03010703420004fb96a581a0b36742a8c45d6ffb5af1ef155524b50339445ec1109874045e0087db77edef91f3dc949927470d84b01627087b72c86b7c9d02e1389cba680ffc36a360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414ef2ce1a86caba85121130a16e8ce82a75d5a6653301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d0403020348003045022100ad4fa628cffba5a642a562cbfefe63efecce26d90c80114114d1745383e12f01022028af87ba3b0ff868a34b9458bc6973b27380a328dd87b7436651ffa823280bca6861757468446174615881bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54900000000164009ea09faae7c397bc3e2ad0e7ec00020c6cffa01b7fda368a7e0b29c1384a719820246bca894dd12914708743af0cecda401010327200621582089f81eba4a1f510cb243ff7fb9e9cf899bf627e49ce1ac3c3eae8adb2a8d7d7b",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224e35446169797479376f7a686c32463465744f4d763670706672504b41546f545170694856726d48686173222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
"4c873571377ac019f257d6bf07249f63ac2487483c51bc511ce0f0e3266c840cb07a09cdc445a2f963d8603a9f0f6cf9ce709d7fc6a96c7c51ea08d33776010c",
OKPPublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 1, Algorithm: -8}, Curve: 0, XCoord: []uint8{0x89, 0xf8, 0x1e, 0xba, 0x4a, 0x1f, 0x51, 0xc, 0xb2, 0x43, 0xff, 0x7f, 0xb9, 0xe9, 0xcf, 0x89, 0x9b, 0xf6, 0x27, 0xe4, 0x9c, 0xe1, 0xac, 0x3c, 0x3e, 0xae, 0x8a, 0xdb, 0x2a, 0x8d, 0x7d, 0x7b}},
tpm2.TPMECCNistP256,
false,
false,
true,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-tpm-es256
"ShouldHandleTestVector37",
"a363666d746374706d6761747453746d74a663616c67266373696758463044022066e5826a652091030fd444e33c3eca2bc6dc548cf3045013addb38aa6457a21002203f3a5c95c9e707d0e555041bcc8698ee4ebc04e26cc8bae459705471789851766376657263322e30637835638159023a30820236308201dca0030201020210311fc42da0ab10c43a9b1bf3a75e34e2300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004c54e3f109094f60d7699b7db5d838569ffd1f3e1c9e897cd9eb40063f9402e3e9937e936cf1fcd5eb743ff443c97ab2edcd7c8e0e6cf6cfd413b8ab19fffa769a381d33081d0300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604145f546cb6973d4981e80fcdc7463859f5879680e4301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e30100603551d250409300706056781050803305e0603551d110101ff04543052a450304e314c3014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a3030303030303030301e060567810502020c15576562417574686e207465737420766563746f7273300a06082a8648ce3d0403020348003045022063c9a2797b8066f1db34dd609f1ab6695607e7a98e9ff8090a68853c9a9fc949022100a55831a39f5b8a2aa9a68837829cabf43fea2a5cea4859ae851cac78e6ac3e97677075624172656158560023000b0004000000000010001000030010002041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b0020d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d076863657274496e666f5869ff544347801700000020277d0e05579dd013215a62273f7f3a3e7e191ead2654a3036d75a5a3ee37a6b0000000000000000011111111222222223300000000000000000022000b9c42d8aad5939331b9af3711af179f17123178098c9a7d0ca89fcd1fc800f3c7000068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54d000000004b92a377fc5f6107c4c85c190adbfd990020ec27bec7521c894bbb821105ea3724c90e770cf1fa354157ef18d0f18f78bea9a501020326200121582041202698c9d9753fb4bb3f27cd09fe6b8afdb76438ee2ae54d7c9dade10d864b225820d8735115cdb330a63ea1d6e43d5000f4bd56f99bce83ee1d73301fc270116d07",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2241416b375a7349645731364a393642776768474a422d6f2d554330304f7a464c6a46705531693279417673222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50d00000000",
"3045022060dc76b1607ec716c6e5eba8d056695ed6bc47b2e3d7a729c34e759e3ab66aa0022100d010a9e8fddcb64c439dfdca628ddb33cf245d567d157d9f66f942601bed9b38",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0x41, 0x20, 0x26, 0x98, 0xc9, 0xd9, 0x75, 0x3f, 0xb4, 0xbb, 0x3f, 0x27, 0xcd, 0x9, 0xfe, 0x6b, 0x8a, 0xfd, 0xb7, 0x64, 0x38, 0xee, 0x2a, 0xe5, 0x4d, 0x7c, 0x9d, 0xad, 0xe1, 0xd, 0x86, 0x4b}, YCoord: []uint8{0xd8, 0x73, 0x51, 0x15, 0xcd, 0xb3, 0x30, 0xa6, 0x3e, 0xa1, 0xd6, 0xe4, 0x3d, 0x50, 0x0, 0xf4, 0xbd, 0x56, 0xf9, 0x9b, 0xce, 0x83, 0xee, 0x1d, 0x73, 0x30, 0x1f, 0xc2, 0x70, 0x11, 0x6d, 0x7}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-android-key-es256
"ShouldHandleTestVector39",
"a363666d746b616e64726f69642d6b65796761747453746d74a363616c672663736967584630440220592bbc3c4c5f6158b52be1e085c92848986d7844245dfc9512e1a7e9ff7a2cd8022015bdd0852d3bd091e1c22da4211f4ccf0fdf4d912599d1c6630b1f310d3166f5637835638159026d3082026930820210a00302010202101ff91f76b63f44812f998b250b0286bf300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000499169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accfdd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6ba381a83081a5300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e041604141ac81e50641e8d1339ab9f7eb25f0cd5aac054b0301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e3045060a2b06010401d679020111043730350202012c0201000201000201000420b20e943e3a7544b3a438943b6d5655313a47ef1af34e00ff3261aeb9ed155817040030003000300a06082a8648ce3d040302034700304402206f4609c9ffc946c418cef04c64a0d07bcce78f329b99270b822f2a4d1e3b75330220093c8d18328f36ef157f296393bdc7721dd2bd67438ffeaa42f051a044b7457168617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000ade9705e1ce7085b899a540d02199bf800200a4729519788b6ed8a2d772b494e186244d8c798c052960dbc8c10c915176795a501020326200121582099169657036d089a2a9821a7d0063d341f1a4613389359636efab5f3cbf1accf225820dd91c55543176ea99b644406dd1dd63774b6af65ac759e06ff40b1c8ab02df6b",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22354f344679703238375851525a55447954746d74786971756851645742534b45545f702d36685433723459222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a2071784a78422d5f78677277794d4c3631386472536e413d3d227d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
"304502202060107d953b286aa1bf35e3e8c78b383fddab5591b2db17ffb23ed83fe7df20022100a99be0297cb0d9d38aa96f30b760a4e0749dab385acd2a51d0560caae570d225",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0x99, 0x16, 0x96, 0x57, 0x3, 0x6d, 0x8, 0x9a, 0x2a, 0x98, 0x21, 0xa7, 0xd0, 0x6, 0x3d, 0x34, 0x1f, 0x1a, 0x46, 0x13, 0x38, 0x93, 0x59, 0x63, 0x6e, 0xfa, 0xb5, 0xf3, 0xcb, 0xf1, 0xac, 0xcf}, YCoord: []uint8{0xdd, 0x91, 0xc5, 0x55, 0x43, 0x17, 0x6e, 0xa9, 0x9b, 0x64, 0x44, 0x6, 0xdd, 0x1d, 0xd6, 0x37, 0x74, 0xb6, 0xaf, 0x65, 0xac, 0x75, 0x9e, 0x6, 0xff, 0x40, 0xb1, 0xc8, 0xab, 0x2, 0xdf, 0x6b}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
{
// Test Vector: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-apple-es256
"ShouldHandleTestVector41",
"a363666d74656170706c656761747453746d74a1637835638159025c30820258308201fea0030201020210394275613d5310b81a29ce90f48b61c1300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d030107034200048a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761af728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136ca38196308193300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e0416041412f1ce6c0ae39b403bfc9200317bc183a4e4d766301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e303306092a864886f76364080204263024a122042097851a1a98b69c0614b26a94b70ec3aa07c061f89dbee23fbee01b6c42d718b0300a06082a8648ce3d040302034800304502207d541a5553f38b93b78b26a9dca58e64a7f8fac15ca206ae3ea32497cda375fb0221009137c6b75e767ec08224b29a7f703db4b745686dcc8a26b66e793688866d064f68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54900000000748210a20076616a733b2114336fc38400209c4a5886af9283d9be3e9ec55978dedfdce2e3b365cab193ae850c16238fafb8a50102032620012158208a3d5b1b4c543a706bf6e4b00afedb3c930b690dd286934fe2911f779cc7761a225820f728e1aa3b0ff66692192daa776b83ddf8e3340d2d9a0eabdfc324eb3e2f136c",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22302d73705a4751654a76375149304136637433676b37476353366b416a442d6432445f503030656d625155222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
"3046022100ee35db795ce28044e1f8231d68b3d79a9882f7415aa35c1b5ac74d24251073c8022100dcc65691650a412d0ceef843710c09827acf26c7845bddac07eec95863e7fc4c",
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: 1, XCoord: []uint8{0x8a, 0x3d, 0x5b, 0x1b, 0x4c, 0x54, 0x3a, 0x70, 0x6b, 0xf6, 0xe4, 0xb0, 0xa, 0xfe, 0xdb, 0x3c, 0x93, 0xb, 0x69, 0xd, 0xd2, 0x86, 0x93, 0x4f, 0xe2, 0x91, 0x1f, 0x77, 0x9c, 0xc7, 0x76, 0x1a}, YCoord: []uint8{0xf7, 0x28, 0xe1, 0xaa, 0x3b, 0xf, 0xf6, 0x66, 0x92, 0x19, 0x2d, 0xaa, 0x77, 0x6b, 0x83, 0xdd, 0xf8, 0xe3, 0x34, 0xd, 0x2d, 0x9a, 0xe, 0xab, 0xdf, 0xc3, 0x24, 0xeb, 0x3e, 0x2f, 0x13, 0x6c}},
tpm2.TPMECCNistP256,
true,
false,
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var (
keyBytes []byte
err error
)
keyBytes = MustExtractCBORKeyFromAttestationObject(t, tc.attestationObject)
result, err := ParsePublicKey(keyBytes)
assert.NoError(t, err)
assert.NotNil(t, result)
data := MustConstructSignedData(t, tc.clientDataJSON, tc.authenticatorData)
signature := MustDecodeHex(t, tc.signature)
switch key := result.(type) {
case EC2PublicKeyData:
assert.True(t, tc.isEC2Key)
assert.False(t, tc.isRSAKey)
assert.False(t, tc.isOKPKey)
assert.Equal(t, tc.tpmcurve, key.TPMCurveID())
ec, err := key.ToECDSA()
assert.NoError(t, err)
assert.NotNil(t, ec)
case RSAPublicKeyData:
assert.False(t, tc.isEC2Key)
assert.True(t, tc.isRSAKey)
assert.False(t, tc.isOKPKey)
case OKPPublicKeyData:
assert.False(t, tc.isEC2Key)
assert.False(t, tc.isRSAKey)
assert.True(t, tc.isOKPKey)
default:
t.Fatalf("Unexpected key type: %T", key)
}
ok, err := VerifySignature(result, data, signature)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, tc.expected, result)
SetExperimentalInsecureAllowBERIntegers(true)
ok, err = VerifySignature(result, data, signature)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, tc.expected, result)
SetExperimentalInsecureAllowBERIntegers(false)
display := DisplayPublicKey(keyBytes)
assert.NotEmpty(t, display)
assert.NotEqual(t, keyCannotDisplay, display)
})
}
}
func TestParsePublicKeyValidation(t *testing.T) {
mustMarshalCOSEKey := func(t *testing.T, kty, alg int64, extra map[int64]any) []byte {
t.Helper()
m := map[int64]any{1: kty, 3: alg}
for k, v := range extra {
m[k] = v
}
data, err := webauthncbor.Marshal(m)
require.NoError(t, err)
return data
}
// Generate valid key material for accept tests.
okpPub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
ec2Priv, err := ecdh.P256().GenerateKey(rand.Reader)
require.NoError(t, err)
ec2Pub := ec2Priv.PublicKey().Bytes()
ec2X := ec2Pub[1:33]
ec2Y := ec2Pub[33:65]
// COSE Key parameters per RFC 9052 §7 and RFC 9053 §2/§7:
// 1 (kty), 3 (alg), -1 (crv / n), -2 (x / e), -3 (y).
testCases := []struct {
name string
input []byte
err string
}{
{
"ShouldAcceptValidOKPKey",
mustMarshalCOSEKey(t, int64(OctetKey), int64(AlgEdDSA), map[int64]any{-2: []byte(okpPub)}),
"",
},
{
"ShouldAcceptValidEC2Key",
mustMarshalCOSEKey(t, int64(EllipticKey), int64(AlgES256), map[int64]any{-1: int64(P256), -2: ec2X, -3: ec2Y}),
"",
},
{
"ShouldAcceptValidRSAKey",
mustMarshalCOSEKey(t, int64(RSAKey), int64(AlgRS256), map[int64]any{-1: []byte{0xFF}, -2: []byte{0x01, 0x00, 0x01}}),
"",
},
{
"ShouldRejectOKPWithInvalidXCoordLength",
mustMarshalCOSEKey(t, int64(OctetKey), int64(AlgEdDSA), map[int64]any{-2: make([]byte, 16)}),
"OKP key x coordinate has invalid length 16, expected 32",
},
{
"ShouldRejectEC2WithUnsupportedAlgorithm",
mustMarshalCOSEKey(t, int64(EllipticKey), int64(999), map[int64]any{-1: int64(P256), -2: make([]byte, 32), -3: make([]byte, 32)}),
"Unsupported EC2 algorithm",
},
{
"ShouldRejectEC2WithInvalidCoordLength",
mustMarshalCOSEKey(t, int64(EllipticKey), int64(AlgES256), map[int64]any{-1: int64(P256), -2: make([]byte, 48), -3: make([]byte, 48)}),
"EC2 key x or y coordinate has invalid length",
},
{
"ShouldRejectEC2WithOffCurvePoint",
mustMarshalCOSEKey(t, int64(EllipticKey), int64(AlgES256), map[int64]any{-1: int64(P256), -2: make([]byte, 32), -3: make([]byte, 32)}),
"EC2 key point is not on curve",
},
{
"ShouldRejectRSAWithEmptyModulus",
mustMarshalCOSEKey(t, int64(RSAKey), int64(AlgRS256), map[int64]any{-1: []byte{}, -2: []byte{0x01, 0x00, 0x01}}),
"RSA key contains zero or empty modulus",
},
{
"ShouldRejectRSAWithEmptyExponent",
mustMarshalCOSEKey(t, int64(RSAKey), int64(AlgRS256), map[int64]any{-1: []byte{0xFF}, -2: []byte{}}),
"RSA key contains invalid exponent: invalid exponent length",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParsePublicKey(tc.input)
if tc.err != "" {
assert.Nil(t, result)
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
func MustExtractCBORKeyFromAttestationObject(t *testing.T, have string) []byte {
type AttObj struct {
AuthData []byte `cbor:"authData"`
}
raw := MustDecodeHex(t, have)
att := AttObj{}
require.NoError(t, cbor.Unmarshal(raw, &att))
// rpIdHash/flags/counter.
offset := 32 + 1 + 4
// AAGUID.
offset += 16
credLen := int(att.AuthData[offset])<<8 | int(att.AuthData[offset+1])
offset += 2 + credLen
return att.AuthData[offset:]
}
func TestFIDOPublicKey(t *testing.T) {
testCases := []struct {
name string
have string
clientDataJSON string
authenticatorData string
signature string
public bool
expected EC2PublicKeyData
tpmcurve tpm2.TPMECCCurve
err string
}{
{
// Test Vector: https://w3c.github.io/webauthn/#sctn-test-vectors-fido-u2f-es256
"ShouldHandleTestVector45",
"51bd002938fa10b83683ac2a2032d0a7338c7f65a90228cfd1f61b81ec7288d0",
"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a222d5178684b59485954316d554f4e3461554139326b6d36537a49532d2d4f417362694e5650774249564455222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d",
"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50100000000",
"304402206172459958fea907b7292b92f555034bfd884895f287a76200c1ba287239137002204727b166147e26a21bbc2921d192ebfed569b79438538e5c128b5e28e6926dd7",
false,
EC2PublicKeyData{PublicKeyData: PublicKeyData{_struct: false, KeyType: 2, Algorithm: -7}, Curve: int64(P256), XCoord: []uint8{0xb0, 0xd6, 0x2d, 0xe6, 0xb3, 0xf, 0x86, 0xf0, 0xba, 0xc7, 0xa9, 0x1, 0x69, 0x51, 0x39, 0x1c, 0x2e, 0x31, 0x84, 0x9e, 0x2e, 0x64, 0x66, 0x1c, 0xbd, 0x2b, 0x13, 0xcd, 0x7d, 0x55, 0x8, 0xad}, YCoord: []uint8{0x50, 0x3b, 0xb, 0xda, 0x2a, 0x35, 0x7a, 0x9a, 0x4b, 0x34, 0x47, 0x5a, 0x28, 0xe6, 0x5b, 0x66, 0xb, 0x48, 0x98, 0xa9, 0xe3, 0xe9, 0xbb, 0xf0, 0x82, 0xd, 0x43, 0x49, 0x42, 0x97, 0xed, 0xd0}},
tpm2.TPMECCNistP256,
"",
},
{
"ShouldNotParseTestFixture1AsPublicKey",
"51bd002938fa10b83683ac2a2032d0a7338c7f65a90228cfd1f61b81ec7288d0",
"",
"",
"",
true,
EC2PublicKeyData{},
0x00,
"failed to parse FIDO public key: crypto/ecdh: invalid public key",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var (
keyBytes []byte
result EC2PublicKeyData
err error
)
if tc.public {
keyBytes, err = hex.DecodeString(tc.have)
require.NoError(t, err)
result, err = ParseFIDOPublicKey(keyBytes)
} else {
keyBytes = DeriveEC2P256PublicKeyFromPrivateKey(t, tc.have)
result, err = ParseFIDOPublicKey(keyBytes)
}
assert.Equal(t, tc.expected, result)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
key, err := result.ToECDSA()
require.NoError(t, err)
assert.NotNil(t, key)
assert.Equal(t, tc.tpmcurve, result.TPMCurveID())
data := MustConstructSignedData(t, tc.clientDataJSON, tc.authenticatorData)
signature := MustDecodeHex(t, tc.signature)
ok, err := VerifySignature(result, data, signature)
assert.NoError(t, err)
assert.True(t, ok)
}
})
}
}
func MustConstructSignedData(t *testing.T, hexClientDataJSON, hexAuthenticatorData string) []byte {
clientDataJSON := MustDecodeHex(t, hexClientDataJSON)
authenticatorData := MustDecodeHex(t, hexAuthenticatorData)
sum := sha256.Sum256(clientDataJSON)
data := make([]byte, 0, len(authenticatorData)+len(sum))
data = append(data, authenticatorData...)
data = append(data, sum[:]...)
return data
}
func MustDecodeHex(t *testing.T, s string) []byte {
b, err := hex.DecodeString(s)
require.NoError(t, err)
return b
}
func DeriveEC2P256PublicKeyFromPrivateKey(t *testing.T, keyHex string) []byte {
key, err := hex.DecodeString(keyHex)
require.NoError(t, err)
require.Len(t, key, 32)
curve := ecdh.P256()
private, err := curve.NewPrivateKey(key)
require.NoError(t, err)
public, ok := private.Public().(*ecdh.PublicKey)
require.True(t, ok)
return public.Bytes()
}