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

This commit is contained in:
2026-08-21 17:33:00 -04:00
parent fb6bbd0dad
commit bfe6cfd29e
230 changed files with 44547 additions and 17 deletions
+615
View File
@@ -0,0 +1,615 @@
package protocol
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func TestValidateRPID(t *testing.T) {
testCases := []struct {
name string
value string
err string
}{
{
name: "ValidRPIDDomain",
value: "example.com",
},
{
name: "ValidRPIDLocalHost",
value: "localhost",
},
{
name: "ValidRPIDUsingIPv4",
value: "127.0.0.1",
},
{
name: "ValidRPIDUsingIPv4Alt",
value: "1.1.1.1",
},
{
name: "ValidRPIDUsingIPv6",
value: "2001:DB8:0:0:8:800:200C:417A",
},
{
name: "ValidRPIDUsingIPv6Alt",
value: "::1",
},
{
name: "InvalidRPIDNotDomain",
value: "example",
err: "the domain component must actually be a domain",
},
{
name: "InvalidRPIDScheme",
value: "https://example.com",
err: "the scheme component must be empty",
},
{
name: "InvalidRPIDPort",
value: "example.com:1234",
err: "the port component must be empty",
},
{
name: "InvalidRPIDPortWithScheme",
value: "https://example.com:1234",
err: "the port component must be empty",
},
{
name: "InvalidRPIDPath",
value: "example.com/example",
err: "the path component must be empty",
},
{
name: "InvalidRPIDQuery",
value: "example.com?abc=123",
err: "the query component must be empty",
},
{
name: "InvalidRPIDFragment",
value: "example.com#abc=123",
err: "the fragment component must be empty",
},
{
name: "InvalidRPIDPathWithScheme",
value: "https://example.com/example",
err: "the path component must be empty",
},
{
name: "InvalidRPIDQueryWithScheme",
value: "https://example.com?abc=123",
err: "the query component must be empty",
},
{
name: "InvalidRPIDFragmentWithScheme",
value: "https://example.com#abc=123",
err: "the fragment component must be empty",
},
{
name: "InvalidEmpty",
value: "",
err: "empty value provided",
},
{
name: "InvalidURI",
value: "https://example\x00.com",
err: "parse \"https://example\\x00.com\": net/url: invalid control character in URL",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateRPID(tc.value)
if tc.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
func TestMustParseX509Certificate(t *testing.T) {
t.Run("ShouldPanic", func(t *testing.T) {
assert.Panics(t, func() {
mustParseX509Certificate([]byte("not a certificate"))
})
})
}
func TestMustParseX509CertificatePEM(t *testing.T) {
t.Run("ShouldPanicInvalidPEM", func(t *testing.T) {
assert.Panics(t, func() {
mustParseX509CertificatePEM([]byte("not a pem"))
})
})
}
func TestAttStatementParseX5CS(t *testing.T) {
cert := testUtilsGenerateSelfSignedCert(t)
testCases := []struct {
name string
have map[string]any
expected struct {
count int
err string
}
}{
{
name: "ShouldFailNotArray",
have: map[string]any{"x5c": "not an array"},
expected: struct {
count int
err string
}{
err: "Error retrieving x5c value",
},
},
{
name: "ShouldFailEmptyArray",
have: map[string]any{"x5c": []any{}},
expected: struct {
count int
err string
}{
err: "Error retrieving x5c value: empty array",
},
},
{
name: "ShouldFailParseError",
have: map[string]any{"x5c": []any{[]byte("not a cert")}},
expected: struct {
count int
err string
}{
err: "Error retrieving x5c value: error occurred parsing values",
},
},
{
name: "ShouldSucceed",
have: map[string]any{"x5c": []any{cert.Raw}},
expected: struct {
count int
err string
}{
count: 1,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
x5c, x5cs, err := attStatementParseX5CS(tc.have, "x5c")
if tc.expected.err == "" {
assert.NoError(t, err)
assert.Len(t, x5c, tc.expected.count)
assert.Len(t, x5cs, tc.expected.count)
} else {
assert.Nil(t, x5c)
assert.Nil(t, x5cs)
assert.EqualError(t, err, tc.expected.err)
}
})
}
}
func TestParseX5C(t *testing.T) {
cert := testUtilsGenerateSelfSignedCert(t)
testCases := []struct {
name string
have []any
expected struct {
count int
err string
}
}{
{
name: "ShouldFailNotByteArray",
have: []any{"not bytes"},
expected: struct {
count int
err string
}{
err: "x5c[0] is not a byte array",
},
},
{
name: "ShouldFailInvalidCert",
have: []any{[]byte("invalid cert der")},
expected: struct {
count int
err string
}{
err: "x5c[0] is not a valid certificate: x509: malformed certificate",
},
},
{
name: "ShouldSucceed",
have: []any{cert.Raw},
expected: struct {
count int
err string
}{
count: 1,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
x5cs, err := parseX5C(tc.have)
if tc.expected.err == "" {
assert.NoError(t, err)
assert.Len(t, x5cs, tc.expected.count)
} else {
assert.Nil(t, x5cs)
assert.EqualError(t, err, tc.expected.err)
}
})
}
}
func TestAttStatementCertChainVerify(t *testing.T) {
ca := testUtilsGenerateSelfSignedCert(t)
leaf := testUtilsGenerateLeafCert(t, ca)
testCases := []struct {
name string
have struct {
certs []*x509.Certificate
roots *x509.CertPool
}
expected struct {
empty bool
err string
}
}{
{
name: "ShouldFailEmptyChain",
have: struct {
certs []*x509.Certificate
roots *x509.CertPool
}{},
expected: struct {
empty bool
err string
}{
empty: true,
err: "empty chain",
},
},
{
name: "ShouldVerifyChainWithNilRoots",
have: struct {
certs []*x509.Certificate
roots *x509.CertPool
}{
certs: []*x509.Certificate{leaf, ca},
},
expected: struct {
empty bool
err string
}{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
chains, err := attStatementCertChainVerify(tc.have.certs, tc.have.roots, false, time.Time{})
if tc.expected.err == "" {
assert.NoError(t, err)
assert.NotEmpty(t, chains)
} else {
assert.EqualError(t, err, tc.expected.err)
if tc.expected.empty {
assert.Nil(t, chains)
}
}
})
}
}
func TestVerifyAttestationECDSAPublicKeyMatch(t *testing.T) {
eccKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
coseKeyBytes, err := webauthncbor.Marshal(webauthncose.EC2PublicKeyData{
PublicKeyData: webauthncose.PublicKeyData{
KeyType: int64(webauthncose.EllipticKey),
Algorithm: int64(webauthncose.AlgES256),
},
Curve: int64(webauthncose.P256),
XCoord: padP256Coord(eccKey.X),
YCoord: padP256Coord(eccKey.Y),
})
require.NoError(t, err)
okpKeyBytes, err := webauthncbor.Marshal(webauthncose.OKPPublicKeyData{
PublicKeyData: webauthncose.PublicKeyData{
KeyType: int64(webauthncose.OctetKey),
Algorithm: int64(webauthncose.AlgEdDSA),
},
Curve: 1,
XCoord: make([]byte, 32),
})
require.NoError(t, err)
matchingCert := testUtilsGenerateCertWithKey(t, &eccKey.PublicKey)
differentECCKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
differentCert := testUtilsGenerateCertWithKey(t, &differentECCKey.PublicKey)
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
rsaCert := testUtilsGenerateCertWithRSAKey(t, &rsaKey.PublicKey)
testCases := []struct {
name string
have struct {
credentialPublicKey []byte
cert *x509.Certificate
}
expected struct {
algorithm int64
err string
}
}{
{
name: "ShouldSucceed",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: coseKeyBytes,
cert: matchingCert,
},
expected: struct {
algorithm int64
err string
}{
algorithm: int64(webauthncose.AlgES256),
},
},
{
name: "ShouldFailInvalidPublicKey",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: []byte("invalid"),
cert: matchingCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Error parsing public key: Unsupported Public Key Type",
},
},
{
name: "ShouldFailNotECDSAKey",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: okpKeyBytes,
cert: matchingCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Attestation public key is not ECDSA",
},
},
{
name: "ShouldFailCertNotECDSA",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: coseKeyBytes,
cert: rsaCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Credential public key is not ECDSA",
},
},
{
name: "ShouldFailKeyMismatch",
have: struct {
credentialPublicKey []byte
cert *x509.Certificate
}{
credentialPublicKey: coseKeyBytes,
cert: differentCert,
},
expected: struct {
algorithm int64
err string
}{
err: "Certificate public key does not match public key in authData",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
att := AttestationObject{
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
CredentialPublicKey: tc.have.credentialPublicKey,
},
},
}
result, err := verifyAttestationECDSAPublicKeyMatch(att, tc.have.cert)
if tc.expected.err == "" {
assert.NoError(t, err)
assert.Equal(t, tc.expected.algorithm, result.Algorithm)
} else {
assert.EqualError(t, err, tc.expected.err)
}
})
}
}
func testUtilsGenerateSelfSignedCert(t *testing.T) *x509.Certificate {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}
func testUtilsGenerateLeafCert(t *testing.T, ca *x509.Certificate) *x509.Certificate {
t.Helper()
leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// We need the CA's private key to sign. Generate a new CA key pair for signing.
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// Recreate the CA cert with the new key so we can sign the leaf.
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
require.NoError(t, err)
caCert, err := x509.ParseCertificate(caDER)
require.NoError(t, err)
*ca = *caCert
leafTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "Test Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, caCert, &leafKey.PublicKey, caKey)
require.NoError(t, err)
leafCert, err := x509.ParseCertificate(leafDER)
require.NoError(t, err)
return leafCert
}
func testUtilsGenerateCertWithKey(t *testing.T, pub *ecdsa.PublicKey) *x509.Certificate {
t.Helper()
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "Test Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, caKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}
// padP256Coord left-pads a big.Int coordinate to 32 bytes (the fixed width for P-256).
// big.Int.Bytes() drops leading zeroes, which would cause COSE EC2 key validation to
// reject coordinates shorter than 32 bytes.
func padP256Coord(v *big.Int) []byte {
const p256ByteLen = 32
b := v.Bytes()
if len(b) >= p256ByteLen {
return b
}
padded := make([]byte, p256ByteLen)
copy(padded[p256ByteLen-len(b):], b)
return padded
}
func testUtilsGenerateCertWithRSAKey(t *testing.T, pub *rsa.PublicKey) *x509.Certificate {
t.Helper()
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: "Test RSA Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, caKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}