This commit is contained in:
+205
@@ -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
@@ -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
@@ -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 fmt’s 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
@@ -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"
|
||||
}`,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}`,
|
||||
}
|
||||
@@ -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 vendor’s 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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 enumeration’s 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 enumeration’s 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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 credential’s 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 object’s 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, QM–QZ, XA–XZ, 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, QM–QZ, XA–XZ, 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": {},
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 user’s 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"
|
||||
)
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user