Skip to content

Commit 0ea14e7

Browse files
fix(protocol): possible panic conditions (#719)
This fixes a few possible panic conditions which would result in an unfavorable output. Since the http.Server handles panics correctly this is not a risk like it would be in other languages.
1 parent 81fdf43 commit 0ea14e7

9 files changed

Lines changed: 241 additions & 43 deletions

File tree

protocol/attestation_tpm.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"encoding/binary"
1111
"errors"
1212
"fmt"
13+
"math"
1314
"strings"
1415

1516
"github.com/google/go-tpm/tpm2"
@@ -116,7 +117,16 @@ func attestationFormatValidationHandlerTPM(att AttestationObject, clientDataHash
116117
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
117118
}
118119

119-
exp := uint32(k.Exponent[0]) + uint32(k.Exponent[1])<<8 + uint32(k.Exponent[2])<<16
120+
var e int
121+
122+
if e, err = webauthncose.ParseRSAPublicKeyDataExponent(&k); err != nil {
123+
return "", nil, ErrAttestationFormat.WithDetails("Unable to decode RSA exponent in attestation statement").WithError(err)
124+
} else if uint64(e) > math.MaxUint32 { //nolint:gosec // The exponent is guaranteed to be positive by the parser.
125+
return "", nil, ErrAttestationFormat.WithDetails("Invalid RSA public key size")
126+
}
127+
128+
exp := uint32(e) //nolint:gosec // The exponent is bounds checked above.
129+
120130
if tpm2Exponent(params) != exp {
121131
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
122132
}

protocol/attestation_tpm_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,93 @@ func TestTPMAttestationVerificationFailPubArea(t *testing.T) {
737737
}
738738
}
739739

740+
func TestTPMAttestationVerificationRSAExponent(t *testing.T) {
741+
_, _, _, rsaKey, _, err := getTPMAttestionKeys()
742+
require.NoError(t, err)
743+
744+
testCases := []struct {
745+
name string
746+
exponent []byte
747+
pubAreaExponent uint32
748+
err string
749+
}{
750+
{
751+
"ShouldNotPanicWithSingleByteExponentMatchingPubArea",
752+
[]byte{0x03},
753+
3,
754+
"unmarshalling field 1 of struct of type 'tpm2.TPMSAttest', EOF",
755+
},
756+
{
757+
"ShouldNotPanicWithSingleByteExponentMismatchingPubArea",
758+
[]byte{0x03},
759+
65537,
760+
"Mismatch between RSAParameters in pubArea and credentialPublicKey",
761+
},
762+
{
763+
"ShouldNotPanicWithTwoByteExponentMatchingPubArea",
764+
[]byte{0x01, 0x00},
765+
256,
766+
"unmarshalling field 1 of struct of type 'tpm2.TPMSAttest', EOF",
767+
},
768+
{
769+
"ShouldNotPanicWithTwoByteExponentMismatchingPubArea",
770+
[]byte{0x01, 0x00},
771+
1,
772+
"Mismatch between RSAParameters in pubArea and credentialPublicKey",
773+
},
774+
{
775+
"ShouldDecodeThreeByteExponentAsBigEndian",
776+
[]byte{0x01, 0x00, 0x01},
777+
65537,
778+
"unmarshalling field 1 of struct of type 'tpm2.TPMSAttest', EOF",
779+
},
780+
{
781+
"ShouldRejectExponentExceedingMaxUint32",
782+
[]byte{0x01, 0x00, 0x00, 0x00, 0x00},
783+
65537,
784+
"Invalid RSA public key size",
785+
},
786+
}
787+
788+
for _, tc := range testCases {
789+
t.Run(tc.name, func(t *testing.T) {
790+
cpk, cerr := webauthncbor.Marshal(webauthncose.RSAPublicKeyData{
791+
PublicKeyData: webauthncose.PublicKeyData{
792+
KeyType: int64(webauthncose.RSAKey),
793+
Algorithm: int64(webauthncose.AlgRS256),
794+
},
795+
Modulus: rsaKey.N.Bytes(),
796+
Exponent: tc.exponent,
797+
})
798+
require.NoError(t, cerr)
799+
800+
attStmt := make(map[string]any, len(defaultAttStatement))
801+
for id, v := range defaultAttStatement {
802+
attStmt[id] = v
803+
}
804+
805+
attStmt[stmtPubArea] = tpm2.Marshal(makeTPMTPublicRSA(&TPMRSATestParameters{Modulus: rsaKey.N.Bytes(), Exponent: tc.pubAreaExponent}))
806+
807+
att := AttestationObject{
808+
AttStatement: attStmt,
809+
AuthData: AuthenticatorData{
810+
AttData: AttestedCredentialData{
811+
CredentialPublicKey: cpk,
812+
},
813+
},
814+
}
815+
816+
require.NotPanics(t, func() {
817+
attestationType, x5cs, aerr := attestationFormatValidationHandlerTPM(att, nil, nil)
818+
819+
assert.Empty(t, attestationType)
820+
assert.Nil(t, x5cs)
821+
assert.EqualError(t, aerr, tc.err)
822+
})
823+
})
824+
}
825+
}
826+
740827
func TestTPMAttestationVerificationFailCertInfo(t *testing.T) {
741828
h := webauthncose.HasherFromCOSEAlg(webauthncose.AlgRS256)
742829
extraData := h.Sum(nil)

protocol/authenticator.go

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,12 @@ func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) (err error) {
313313

314314
if a.Flags.HasAttestedCredentialData() {
315315
if len(rawAuthData) > minAttestedAuthLength {
316-
if err = a.unmarshalAttestedData(rawAuthData); err != nil {
316+
var attDataLen int
317+
318+
if attDataLen, err = a.unmarshalAttestedData(rawAuthData); err != nil {
317319
return err
318320
}
319321

320-
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey)
321322
remaining -= attDataLen
322323
} else {
323324
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing")
@@ -344,42 +345,49 @@ func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) (err error) {
344345
return nil
345346
}
346347

347-
// If Attestation Data is present, unmarshall that into the appropriate public key structure.
348-
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) (err error) {
348+
// If Attestation Data is present, unmarshall that into the appropriate public key structure. Returns the number of
349+
// bytes of rawAuthData which the attested credential data occupied, measured from the start of the AAGUID.
350+
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) (n int, err error) {
349351
a.AttData.AAGUID = rawAuthData[37:53]
350352

351-
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
352-
if len(rawAuthData) < int(55+idLength) {
353-
return ErrBadRequest.WithDetails("Authenticator attestation data length too short")
354-
}
353+
idLength := int(binary.BigEndian.Uint16(rawAuthData[53:55]))
355354

356355
if idLength > maxCredentialIDLength {
357-
return ErrBadRequest.WithDetails("Authenticator attestation data credential id length too long")
356+
return 0, ErrBadRequest.WithDetails("Authenticator attestation data credential id length too long")
357+
}
358+
359+
if len(rawAuthData) < 55+idLength {
360+
return 0, ErrBadRequest.WithDetails("Authenticator attestation data length too short")
358361
}
359362

360363
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
361364

362-
a.AttData.CredentialPublicKey, err = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
363-
if err != nil {
364-
return ErrBadRequest.WithDetails(fmt.Sprintf("Could not unmarshal Credential Public Key: %v", err)).WithError(err)
365+
var keyLength int
366+
367+
if a.AttData.CredentialPublicKey, keyLength, err = unmarshalCredentialPublicKey(rawAuthData[55+idLength:]); err != nil {
368+
return 0, ErrBadRequest.WithDetails(fmt.Sprintf("Could not unmarshal Credential Public Key: %v", err)).WithError(err)
365369
}
366370

367-
return nil
371+
// The AAGUID is 16 bytes and the credential id length prefix is 2 bytes.
372+
return 16 + 2 + idLength + keyLength, nil
368373
}
369374

370-
// Unmarshall the credential's Public Key into CBOR encoding.
371-
func unmarshalCredentialPublicKey(keyBytes []byte) (rawBytes []byte, err error) {
375+
// Unmarshall the credential's Public Key into CBOR encoding. Returns the re-encoded key alongside the number of bytes
376+
// of keyBytes which the key occupied on the wire. These lengths are not necessarily equal as the CTAP2 canonical form
377+
// produced by Marshal may be longer or shorter than the form which was decoded, so the consumed length must be used
378+
// when locating any data which follows the key.
379+
func unmarshalCredentialPublicKey(keyBytes []byte) (rawBytes []byte, n int, err error) {
372380
var m any
373381

374-
if err = webauthncbor.Unmarshal(keyBytes, &m); err != nil {
375-
return nil, err
382+
if n, err = webauthncbor.UnmarshalFirst(keyBytes, &m); err != nil {
383+
return nil, 0, err
376384
}
377385

378386
if rawBytes, err = webauthncbor.Marshal(m); err != nil {
379-
return nil, err
387+
return nil, 0, err
380388
}
381389

382-
return rawBytes, nil
390+
return rawBytes, n, nil
383391
}
384392

385393
// ResidentKeyRequired - Require that the key be private key resident to the client device.

protocol/authenticator_oob_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package protocol
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestAuthenticatorData_Unmarshal_CredentialPublicKeyReencodingExpands(t *testing.T) {
12+
rawPublicKey, err := hex.DecodeString("f9f4cb")
13+
require.NoError(t, err)
14+
15+
rawAuthData := buildAuthData(0xC0, nil, rawPublicKey)
16+
17+
var a AuthenticatorData
18+
19+
require.NotPanics(t, func() {
20+
err = a.Unmarshal(rawAuthData)
21+
})
22+
23+
assert.Error(t, err)
24+
}
25+
26+
func TestAuthenticatorData_Unmarshal_CredentialPublicKeyReencodingShrinks(t *testing.T) {
27+
rawPublicKey, err := hex.DecodeString("a119000102")
28+
require.NoError(t, err)
29+
30+
rawAuthData := buildAuthData(0xC0, nil, rawPublicKey)
31+
32+
var a AuthenticatorData
33+
34+
require.NotPanics(t, func() {
35+
err = a.Unmarshal(rawAuthData)
36+
})
37+
38+
assert.Error(t, err)
39+
assert.Empty(t, a.ExtData)
40+
}
41+
42+
func TestAuthenticatorData_Unmarshal_TrailingBytesRejected(t *testing.T) {
43+
rawPublicKey, err := hex.DecodeString("a10102ff")
44+
require.NoError(t, err)
45+
46+
rawAuthData := buildAuthData(0x40, nil, rawPublicKey)
47+
48+
var a AuthenticatorData
49+
50+
require.NotPanics(t, func() {
51+
err = a.Unmarshal(rawAuthData)
52+
})
53+
54+
assert.EqualError(t, err, "Leftover bytes decoding AuthenticatorData")
55+
}
56+
57+
func buildAuthData(flags byte, credentialID, rawPublicKey []byte) []byte {
58+
data := make([]byte, 0, 55+len(credentialID)+len(rawPublicKey))
59+
60+
data = append(data, make([]byte, 32)...) // RPIDHash.
61+
data = append(data, flags) // Flags.
62+
data = append(data, 0x00, 0x00, 0x00, 0x01) // Counter.
63+
data = append(data, make([]byte, 16)...) // AAGUID.
64+
data = append(data, byte(len(credentialID)>>8), byte(len(credentialID))) //nolint:gosec // Test data is bounded.
65+
data = append(data, credentialID...)
66+
data = append(data, rawPublicKey...)
67+
68+
return data
69+
}

protocol/authenticator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ func TestAuthenticatorData_unmarshalAttestedData(t *testing.T) {
358358
ExtData: tc.fields.ExtData,
359359
}
360360

361-
err := actual.unmarshalAttestedData(tc.args.rawAuthData)
361+
_, err := actual.unmarshalAttestedData(tc.args.rawAuthData)
362362

363363
if tc.err != "" {
364364
assert.EqualError(t, err, tc.err)

protocol/webauthncbor/webauthncbor.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,25 @@ var ctap2CBOREncMode, _ = cbor.CTAP2EncOptions().EncMode()
1919
// following the CTAP2 canonical CBOR encoding form.
2020
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
2121
func Unmarshal(data []byte, v any) error {
22-
// TODO (james-d-elliott): investigate the specific use case for Unmarshal vs UnmarshalFirst to determine the edge cases where this may be useful.
2322
_, err := ctap2CBORDecMode.UnmarshalFirst(data, v)
2423

2524
return err
2625
}
2726

27+
// UnmarshalFirst parses the first CBOR-encoded item in data into the value pointed to by v following the CTAP2
28+
// canonical CBOR encoding form, and returns the number of bytes of data which that item consumed. Callers which decode
29+
// an item embedded in a larger byte sequence must use this rather than deriving a length from the re-encoded output of
30+
// Marshal, as the encoded form is not guaranteed to be the same size as the form which was decoded.
31+
func UnmarshalFirst(data []byte, v any) (n int, err error) {
32+
var rest []byte
33+
34+
if rest, err = ctap2CBORDecMode.UnmarshalFirst(data, v); err != nil {
35+
return 0, err
36+
}
37+
38+
return len(data) - len(rest), nil
39+
}
40+
2841
// Marshal encodes the value pointed to by v
2942
// following the CTAP2 canonical CBOR encoding form.
3043
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)

protocol/webauthncose/webauthncose.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ import (
1414
"math"
1515
"math/big"
1616

17-
"github.com/go-webauthn/x/encoding/asn1"
18-
1917
"github.com/google/go-tpm/tpm2"
2018

2119
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
20+
"github.com/go-webauthn/x/encoding/asn1"
2221
)
2322

2423
// PublicKeyData The public key portion of a Relying Party-specific credential key pair, generated
@@ -129,12 +128,12 @@ func (k *EC2PublicKeyData) ToECDSA() (key *ecdsa.PublicKey, err error) {
129128

130129
// Verify RSA Public Key Signature.
131130
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (valid bool, err error) {
132-
if err = validateRSAPublicKey(k); err != nil {
131+
var e int
132+
133+
if e, err = validateRSAPublicKey(k); err != nil {
133134
return false, err
134135
}
135136

136-
e, _ := parseRSAPublicKeyDataExponent(k)
137-
138137
pubkey := &rsa.PublicKey{
139138
N: big.NewInt(0).SetBytes(k.Modulus),
140139
E: e,
@@ -211,7 +210,7 @@ func ParsePublicKey(keyBytes []byte) (publicKey any, err error) {
211210

212211
r.PublicKeyData = pk
213212

214-
if err = validateRSAPublicKey(&r); err != nil {
213+
if _, err = validateRSAPublicKey(&r); err != nil {
215214
return nil, err
216215
}
217216

@@ -267,7 +266,7 @@ func DisplayPublicKey(cpk []byte) string {
267266
case RSAPublicKeyData:
268267
var e int
269268

270-
if e, err = parseRSAPublicKeyDataExponent(&k); err != nil {
269+
if e, err = ParseRSAPublicKeyDataExponent(&k); err != nil {
271270
return keyCannotDisplay
272271
}
273272

@@ -454,20 +453,21 @@ func validateEC2PublicKey(k *EC2PublicKeyData) error {
454453
return nil
455454
}
456455

457-
func validateRSAPublicKey(k *RSAPublicKeyData) error {
456+
func validateRSAPublicKey(k *RSAPublicKeyData) (e int, err error) {
458457
n := new(big.Int).SetBytes(k.Modulus)
459458
if n.Sign() <= 0 {
460-
return ErrUnsupportedKey.WithDetails("RSA key contains zero or empty modulus")
459+
return e, ErrUnsupportedKey.WithDetails("RSA key contains zero or empty modulus")
461460
}
462461

463-
if _, err := parseRSAPublicKeyDataExponent(k); err != nil {
464-
return ErrUnsupportedKey.WithDetails(fmt.Sprintf("RSA key contains invalid exponent: %v", err))
462+
if e, err = ParseRSAPublicKeyDataExponent(k); err != nil {
463+
return e, ErrUnsupportedKey.WithDetails(fmt.Sprintf("RSA key contains invalid exponent: %v", err))
465464
}
466465

467-
return nil
466+
return e, nil
468467
}
469468

470-
func parseRSAPublicKeyDataExponent(k *RSAPublicKeyData) (exp int, err error) {
469+
// ParseRSAPublicKeyDataExponent safely parses the exponent value of the provided RSAPublicKeyData.
470+
func ParseRSAPublicKeyDataExponent(k *RSAPublicKeyData) (exp int, err error) {
471471
if k == nil {
472472
return 0, fmt.Errorf("invalid key")
473473
}

protocol/webauthncose/webauthncose_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func TestRSAExponent(t *testing.T) {
182182

183183
for _, tc := range testCases {
184184
t.Run(tc.name, func(t *testing.T) {
185-
actual, err := parseRSAPublicKeyDataExponent(tc.have)
185+
actual, err := ParseRSAPublicKeyDataExponent(tc.have)
186186

187187
if tc.err != "" {
188188
assert.EqualError(t, err, tc.err)

0 commit comments

Comments
 (0)