This commit is contained in:
+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
|
||||
}
|
||||
Reference in New Issue
Block a user