343 lines
11 KiB
Go
Vendored
343 lines
11 KiB
Go
Vendored
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"
|
|
}`,
|
|
}
|