Skip to content

Commit db1e068

Browse files
feat: ml-dsa preference list and gated availability (#768)
This adds webauthn.CredentialParametersPQCRecommendedL3 which includes the recommended order of algorithms for RP's that support ML-DSA. In addition it gates all ML-DSA availability on go 1.27 so that the API is clearer.
1 parent 5fb926e commit db1e068

18 files changed

Lines changed: 446 additions & 163 deletions

protocol/attestation_mldsa_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,50 @@ func TestVerifyAttestationPublicKeyMatchMLDSA(t *testing.T) {
6060
})
6161
}
6262

63+
// TestAKPHelpersRejectOtherKeyTypes asserts that neither helper claims a key which is not of the AKP key type. Each
64+
// is reached only from the default arm of a switch which has already handled every other key type, so a helper
65+
// which claimed one of them would divert a key away from the branch that knows how to handle it.
66+
func TestAKPHelpersRejectOtherKeyTypes(t *testing.T) {
67+
keys := []any{
68+
struct{}{},
69+
webauthncose.EC2PublicKeyData{},
70+
webauthncose.RSAPublicKeyData{},
71+
webauthncose.OKPPublicKeyData{},
72+
}
73+
74+
for _, key := range keys {
75+
algorithm, ok := akpKeyAlgorithm(key)
76+
77+
assert.False(t, ok)
78+
assert.Zero(t, algorithm)
79+
80+
public, ok, err := akpPublicKey(key)
81+
82+
assert.False(t, ok)
83+
assert.Nil(t, public)
84+
require.NoError(t, err)
85+
}
86+
87+
t.Run("ShouldClaimAKPKey", func(t *testing.T) {
88+
_, pub := mldsaTestCredentialPublicKey(t, webauthncose.AlgMLDSA44, mldsa.MLDSA44())
89+
90+
parsed, err := webauthncose.ParsePublicKey(pub)
91+
92+
require.NoError(t, err)
93+
94+
algorithm, ok := akpKeyAlgorithm(parsed)
95+
96+
assert.True(t, ok)
97+
assert.Equal(t, int64(webauthncose.AlgMLDSA44), algorithm)
98+
99+
public, ok, err := akpPublicKey(parsed)
100+
101+
assert.True(t, ok)
102+
require.NoError(t, err)
103+
assert.NotNil(t, public)
104+
})
105+
}
106+
63107
// TestPackedFormat_SelfAttestationMLDSA asserts that a packed self attestation made with an ML-DSA credential key
64108
// verifies, and that a statement whose alg contradicts the credential public key does not.
65109
//

protocol/attestation_packed.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,13 @@ func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, sig []by
214214
err = verifyKeyAlgorithm(k.Algorithm, alg)
215215
case webauthncose.RSAPublicKeyData:
216216
err = verifyKeyAlgorithm(k.Algorithm, alg)
217-
case webauthncose.AKPPublicKeyData:
218-
err = verifyKeyAlgorithm(k.Algorithm, alg)
219217
default:
220-
return "", nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
218+
keyAlgorithm, ok := akpKeyAlgorithm(key)
219+
if !ok {
220+
return "", nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
221+
}
222+
223+
err = verifyKeyAlgorithm(keyAlgorithm, alg)
221224
}
222225

223226
if err != nil {

protocol/mldsa.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//go:build go1.27
2+
3+
package protocol
4+
5+
import (
6+
"crypto"
7+
8+
"github.com/go-webauthn/webauthn/protocol/webauthncose"
9+
)
10+
11+
// akpKeyAlgorithm returns the COSE algorithm a credential public key of the AKP key type names, and whether the key
12+
// is one at all. It exists so that the attestation formats can hold such a key to the algorithm an attestation
13+
// statement declares without naming a type which only exists on a build that can verify with it.
14+
func akpKeyAlgorithm(key any) (algorithm int64, ok bool) {
15+
k, is := key.(webauthncose.AKPPublicKeyData)
16+
if !is {
17+
return 0, false
18+
}
19+
20+
return k.Algorithm, true
21+
}
22+
23+
// akpPublicKey converts a credential public key of the AKP key type into the equivalent standard library key. The
24+
// second value reports whether the key was one at all, which distinguishes a key this does not handle from an AKP
25+
// key which could not be converted.
26+
func akpPublicKey(key any) (public crypto.PublicKey, ok bool, err error) {
27+
k, is := key.(webauthncose.AKPPublicKeyData)
28+
if !is {
29+
return nil, false, nil
30+
}
31+
32+
public, err = k.ToPublicKey()
33+
34+
return public, true, err
35+
}

protocol/mldsa_unsupported.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//go:build !go1.27
2+
3+
package protocol
4+
5+
import (
6+
"crypto"
7+
)
8+
9+
// akpKeyAlgorithm reports that the key is not one of the AKP key type. No parser on a build which cannot verify an
10+
// ML-DSA signature produces such a key, so nothing carrying one reaches the attestation formats.
11+
func akpKeyAlgorithm(_ any) (algorithm int64, ok bool) {
12+
return 0, false
13+
}
14+
15+
// akpPublicKey reports that the key is not one of the AKP key type, for the same reason as [akpKeyAlgorithm].
16+
func akpPublicKey(_ any) (public crypto.PublicKey, ok bool, err error) {
17+
return nil, false, nil
18+
}

protocol/mldsa_unsupported_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//go:build !go1.27
2+
3+
package protocol
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/go-webauthn/webauthn/protocol/webauthncose"
12+
)
13+
14+
// TestAKPHelpersUnsupported asserts that neither helper claims a key on a build which cannot verify an ML-DSA
15+
// signature. Neither is reached today, as no parser on such a build produces a key of the AKP key type, so they are
16+
// asserted directly: they are what keeps the attestation formats from treating an unverifiable key as one they
17+
// handle if that ever changes.
18+
func TestAKPHelpersUnsupported(t *testing.T) {
19+
keys := []any{
20+
struct{}{},
21+
webauthncose.EC2PublicKeyData{},
22+
webauthncose.RSAPublicKeyData{},
23+
webauthncose.OKPPublicKeyData{},
24+
}
25+
26+
for _, key := range keys {
27+
algorithm, ok := akpKeyAlgorithm(key)
28+
29+
assert.False(t, ok)
30+
assert.Zero(t, algorithm)
31+
32+
public, ok, err := akpPublicKey(key)
33+
34+
assert.False(t, ok)
35+
assert.Nil(t, public)
36+
require.NoError(t, err)
37+
}
38+
}

protocol/utils.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,11 @@ func attestationCredentialPublicKey(credentialPublicKey any) (public crypto.Publ
246246
// The coordinate is of the length ed25519 requires as webauthncose.ParsePublicKey rejects any other, so no
247247
// length is asserted here.
248248
return ed25519.PublicKey(k.XCoord), nil
249-
case webauthncose.AKPPublicKeyData:
250-
return k.ToPublicKey()
251249
default:
250+
if public, ok, err := akpPublicKey(credentialPublicKey); ok {
251+
return public, err
252+
}
253+
252254
return nil, fmt.Errorf("unsupported public key type %T", credentialPublicKey)
253255
}
254256
}

protocol/webauthncose/akp.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
//go:build go1.27
2+
13
package webauthncose
24

35
import (
46
"crypto"
57
"fmt"
8+
9+
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
610
)
711

812
// AKPPublicKeyData is a credential public key of the AKP (Algorithm Key Pair) key type, which carries its key
@@ -29,9 +33,6 @@ func (k *AKPPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
2933

3034
// ToPublicKey converts the AKPPublicKeyData to the standard library key of the algorithm it names, which for the
3135
// ML-DSA parameter sets is a *[crypto/mldsa.PublicKey].
32-
//
33-
// The concrete type is not named in the signature because the package which declares it is only present from Go
34-
// 1.27, which is also the point from which this returns a key at all.
3536
func (k *AKPPublicKeyData) ToPublicKey() (key crypto.PublicKey, err error) {
3637
if err = validateAKPPublicKey(k); err != nil {
3738
return nil, err
@@ -40,6 +41,48 @@ func (k *AKPPublicKeyData) ToPublicKey() (key crypto.PublicKey, err error) {
4041
return mldsaPublicKey(COSEAlgorithmIdentifier(k.Algorithm), k.PublicKey)
4142
}
4243

44+
// parseAKPPublicKey decodes a credential public key of the AKP key type. It is the arm [ParsePublicKey] delegates
45+
// that key type to, so that the parser itself does not name a type which only exists on a build that can verify
46+
// with it.
47+
func parseAKPPublicKey(pk PublicKeyData, keyBytes []byte) (key any, err error) {
48+
var a AKPPublicKeyData
49+
50+
if err = webauthncbor.Unmarshal(keyBytes, &a); err != nil {
51+
return nil, err
52+
}
53+
54+
a.PublicKeyData = pk
55+
56+
if err = validateAKPPublicKey(&a); err != nil {
57+
return nil, err
58+
}
59+
60+
return a, nil
61+
}
62+
63+
// verifyAKPSignature is the arm [VerifySignature] delegates a key of the AKP key type to. A key of any other type
64+
// has already been handled by its caller, so anything reaching here that is not one is a key this library does not
65+
// verify with.
66+
func verifyAKPSignature(key any, data []byte, sig []byte) (bool, error) {
67+
k, ok := key.(AKPPublicKeyData)
68+
if !ok {
69+
return false, ErrUnsupportedKey
70+
}
71+
72+
return k.Verify(data, sig)
73+
}
74+
75+
// displayAKPPublicKey is the arm [DisplayPublicKey] delegates a key of the AKP key type to. A nil encoding and a
76+
// nil error report that the key is not an AKP key at all, which the caller renders as a key type it cannot display.
77+
func displayAKPPublicKey(key any) (der []byte, err error) {
78+
k, ok := key.(AKPPublicKeyData)
79+
if !ok {
80+
return nil, nil
81+
}
82+
83+
return mldsaMarshalPublicKey(COSEAlgorithmIdentifier(k.Algorithm), k.PublicKey)
84+
}
85+
4386
// validateAKPPublicKey checks that a credential public key of type AKP names an algorithm this library verifies
4487
// with, and carries key material that algorithm accepts.
4588
//

protocol/webauthncose/akp_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,22 @@ func TestMLDSAFailsClosed(t *testing.T) {
447447
require.EqualError(t, err, "AKP key is not a valid ML-DSA public key")
448448
})
449449

450+
t.Run("DispatchOfOtherKeyTypes", func(t *testing.T) {
451+
// Each dispatch arm is reached only from the default of a switch which has handled every other key
452+
// type, so claiming one would divert it from the branch which knows how to handle it.
453+
for _, other := range []any{struct{}{}, EC2PublicKeyData{}, RSAPublicKeyData{}, OKPPublicKeyData{}} {
454+
der, err := displayAKPPublicKey(other)
455+
456+
assert.Nil(t, der)
457+
require.NoError(t, err)
458+
459+
valid, err := verifyAKPSignature(other, data, sig)
460+
461+
assert.False(t, valid)
462+
require.EqualError(t, err, "Unsupported Public Key Type")
463+
}
464+
})
465+
450466
t.Run("MarshalPublicKey", func(t *testing.T) {
451467
der, err := mldsaMarshalPublicKey(AlgES256, key.PublicKey)
452468

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build !go1.27
2+
3+
package webauthncose
4+
5+
// This file supplies the arms which [ParsePublicKey], [VerifySignature] and [DisplayPublicKey] delegate the AKP key
6+
// type to on a build which cannot verify a signature made with one.
7+
//
8+
// The only algorithms registered for that key type are the ML-DSA parameter sets, which this library verifies with
9+
// through crypto/mldsa, and the standard library did not have that package before Go 1.27. The credential is
10+
// refused when it is parsed rather than when it is verified, so a Relying Party never registers a credential it
11+
// would be unable to authenticate with.
12+
13+
// parseAKPPublicKey refuses a credential public key of the AKP key type.
14+
func parseAKPPublicKey(_ PublicKeyData, _ []byte) (key any, err error) {
15+
return nil, ErrUnsupportedKey.WithDetails("AKP key type requires this library to be built with Go 1.27 or newer")
16+
}
17+
18+
// verifyAKPSignature refuses a key of the AKP key type, which no parser on this build produces.
19+
func verifyAKPSignature(_ any, _ []byte, _ []byte) (bool, error) {
20+
return false, ErrUnsupportedKey
21+
}
22+
23+
// displayAKPPublicKey reports that the key is not one it renders, as no key of the AKP key type reaches it.
24+
func displayAKPPublicKey(_ any) (der []byte, err error) {
25+
return nil, nil
26+
}

0 commit comments

Comments
 (0)