Skip to content

Commit f9a63f9

Browse files
fix(protocol): missing tpm steps (#725)
This fixes a number of missing steps in the TPM attestation process. Specifically, the id-fido-gen-ce-aaguid check in certInfo step, the missing check of basic constraints, and missing manufacturer checks.
1 parent 88219ba commit f9a63f9

6 files changed

Lines changed: 532 additions & 185 deletions

File tree

protocol/attestation_packed.go

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package protocol
33
import (
44
"bytes"
55
"crypto/x509"
6-
"encoding/asn1"
76
"fmt"
87
"strings"
98
"time"
@@ -157,33 +156,22 @@ func handleBasicAttestation(sig, clientDataHash, authData, aaguid []byte, alg in
157156
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
158157
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
159158
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
160-
var foundAAGUID []byte
161-
162-
for _, extension := range attestnCert.Extensions {
163-
if extension.Id.Equal(oidFIDOGenCeAAGUID) {
164-
if extension.Critical {
165-
return "", x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
166-
}
159+
//
160+
// We validate the AAGUID as mentioned above. This is not well defined in §8.2.1 but mentioned in step 2.3: we
161+
// validate the AAGUID if it is present within the certificate and make sure it matches the auth data AAGUID.
162+
var (
163+
certAAGUID []byte
164+
critical bool
165+
)
167166

168-
foundAAGUID = extension.Value
169-
}
167+
if certAAGUID, critical, _, err = attestationCertAAGUID(attestnCert); critical {
168+
return "", x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
169+
} else if err != nil {
170+
return "", x5c, ErrInvalidAttestation.WithDetails("Error unmarshalling AAGUID from certificate")
170171
}
171172

172-
// We validate the AAGUID as mentioned above
173-
// This is not well defined in§8.2.1 but mentioned in step 2.3: we validate the AAGUID if it is present within the certificate
174-
// and make sure it matches the auth data AAGUID
175-
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
176-
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
177-
if len(foundAAGUID) > 0 {
178-
var unMarshalledAAGUID []byte
179-
180-
if _, err = asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID); err != nil {
181-
return "", x5c, ErrInvalidAttestation.WithDetails("Error unmarshalling AAGUID from certificate")
182-
}
183-
184-
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
185-
return "", x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
186-
}
173+
if len(certAAGUID) > 0 && !bytes.Equal(aaguid, certAAGUID) {
174+
return "", x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
187175
}
188176

189177
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.

protocol/attestation_tpm.go

Lines changed: 48 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ package protocol
22

33
import (
44
"bytes"
5-
"crypto"
65
"crypto/subtle"
76
"crypto/x509"
87
"crypto/x509/pkix"
98
"encoding/asn1"
10-
"encoding/binary"
119
"errors"
1210
"fmt"
1311
"math"
@@ -208,6 +206,7 @@ func attestationFormatValidationHandlerTPM(att AttestationObject, clientDataHash
208206
var (
209207
manufacturer, model, version string
210208
ekuValid = false
209+
constraintsValid = false
211210
eku []asn1.ObjectIdentifier
212211
constraints tpmBasicConstraints
213212
rest []byte
@@ -246,6 +245,8 @@ func attestationFormatValidationHandlerTPM(att AttestationObject, clientDataHash
246245
} else if len(rest) != 0 {
247246
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
248247
}
248+
249+
constraintsValid = true
249250
}
250251
}
251252

@@ -263,13 +264,29 @@ func attestationFormatValidationHandlerTPM(att AttestationObject, clientDataHash
263264
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
264265
}
265266

267+
// 5/6 The Basic Constraints extension MUST have the CA component set to false. An absent extension can't have the
268+
// CA component set to false so it's rejected in the same way as one which asserts CA is true.
269+
//
266270
// 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
267271
// extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
268272
// through metadata services. See, for example, the FIDO Metadata Service.
269-
if constraints.IsCA {
273+
if !constraintsValid || constraints.IsCA {
270274
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
271275
}
272276

277+
// If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value
278+
// of this extension matches the aaguid in authenticatorData.
279+
var (
280+
aaguid []byte
281+
found bool
282+
)
283+
284+
if aaguid, _, found, err = attestationCertAAGUID(aikCert); err != nil {
285+
return "", nil, ErrInvalidAttestation.WithDetails("Error unmarshalling AAGUID from certificate").WithError(err)
286+
} else if found && !bytes.Equal(aaguid, att.AuthData.AttData.AAGUID) {
287+
return "", nil, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
288+
}
289+
273290
// 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
274291
// [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
275292
// as computed using the algorithm in the nameAlg field of pubArea
@@ -322,34 +339,6 @@ func tpm2NameMatch(certInfo *tpm2.TPMSAttest, pubArea *tpm2.TPMTPublic) (match b
322339
return subtle.ConstantTimeCompare(certifyInfo.Name.Buffer, name.Buffer) == 1, nil
323340
}
324341

325-
func tpm2NameDigest(name tpm2.TPM2BName) (alg tpm2.TPMIAlgHash, digest []byte, err error) {
326-
buf := name.Buffer
327-
328-
if len(buf) < 3 {
329-
return 0, nil, fmt.Errorf("name too short")
330-
}
331-
332-
alg = tpm2.TPMIAlgHash(binary.BigEndian.Uint16(buf[:2]))
333-
334-
var hash crypto.Hash
335-
336-
if hash, err = alg.Hash(); err != nil {
337-
return 0, nil, fmt.Errorf("invalid hash algorithm: %w", err)
338-
}
339-
340-
digest = buf[2:]
341-
342-
if len(digest) == 0 {
343-
return 0, nil, fmt.Errorf("name digest is empty")
344-
}
345-
346-
if len(digest) != hash.Size() {
347-
return 0, nil, fmt.Errorf("invalid name digest length: %d", len(digest))
348-
}
349-
350-
return alg, digest, nil
351-
}
352-
353342
type tpm2AttStatement struct {
354343
Version string
355344
Algorithm int64
@@ -507,72 +496,29 @@ type tpmManufacturer struct {
507496
code string
508497
}
509498

510-
// See https://trustedcomputinggroup.org/resource/vendor-id-registry/ for registry contents.
511-
var (
512-
tpmManufacturers = []tpmManufacturer{
513-
{"414D4400", "AMD", "AMD"},
514-
{"414E5400", "Ant Group", "ANT"},
515-
{"41544D4C", "Atmel", "ATML"},
516-
{"4252434D", "Broadcom", "BRCM"},
517-
{"4353434F", "Cisco", "CSCO"},
518-
{"464C5953", "Flyslice Technologies", "FLYS"},
519-
{"524F4343", "Fuzhou Rockchip", "ROCC"},
520-
{"474F4F47", "Google", "GOOG"},
521-
{"48504900", "HPI", "HPI"},
522-
{"48504500", "HPE", "HPE"},
523-
{"48495349", "Huawei", "HISI"},
524-
{"49424d00", "IBM", "IBM"},
525-
{"49424D00", "IBM", "IBM"},
526-
{"49465800", "Infineon", "IFX"},
527-
{"494E5443", "Intel", "INTC"},
528-
{"4C454E00", "Lenovo", "LEN"},
529-
{"4D534654", "Microsoft", "MSFT"},
530-
{"4E534D20", "National Semiconductor", "NSM"},
531-
{"4E545A00", "Nationz", "NTZ"},
532-
{"4E534700", "NSING", "NSG"},
533-
{"4E544300", "Nuvoton Technology", "NTC"},
534-
{"51434F4D", "Qualcomm", "QCOM"},
535-
{"534D534E", "Samsung", "SECE"},
536-
{"53454345", "SecEdge", "SecEdge"},
537-
{"534E5300", "Sinosun", "SNS"},
538-
{"534D5343", "SMSC", "SMSC"},
539-
{"53544D20", "ST Microelectronics", "STM"},
540-
{"54584E00", "Texas Instruments", "TXN"},
541-
{"57454300", "Winbond", "WEC"},
542-
{"5345414C", "Wisekey", "SEAL"},
543-
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
544-
}
545-
)
546-
547-
func isValidTPMManufacturer(id string) bool {
548-
for _, m := range tpmManufacturers {
549-
if m.id == id {
550-
return true
551-
}
499+
func tpmParseAIKAttCA(x5c *x509.Certificate, x5cis []*x509.Certificate) (leaf *x509.Certificate, parents []*x509.Certificate, protoErr *Error) {
500+
if leaf, protoErr = tpmParseSANExtension(x5c); protoErr != nil {
501+
return nil, nil, protoErr
552502
}
553503

554-
return false
555-
}
504+
var hasAIK bool
556505

557-
func tpmParseAIKAttCA(x5c *x509.Certificate, x5cis []*x509.Certificate) (err *Error) {
558-
if err = tpmParseSANExtension(x5c); err != nil {
559-
return err
506+
// The AIK Extended Key Usage is only required on the attestation certificate itself per §8.3.1. TPM vendor
507+
// intermediates generally don't carry it so it must not be required of them.
508+
if leaf, hasAIK = tpmRemoveEKU(leaf); !hasAIK {
509+
return nil, nil, ErrAttestationFormat.WithDetails("Attestation Identity Key certificate missing required Extended Key Usage.")
560510
}
561511

562-
if err = tpmRemoveEKU(x5c); err != nil {
563-
return err
564-
}
512+
for _, x5ci := range x5cis {
513+
parent, _ := tpmRemoveEKU(x5ci)
565514

566-
for _, parent := range x5cis {
567-
if err = tpmRemoveEKU(parent); err != nil {
568-
return err
569-
}
515+
parents = append(parents, parent)
570516
}
571517

572-
return nil
518+
return leaf, parents, nil
573519
}
574520

575-
func tpmParseSANExtension(attestation *x509.Certificate) (protoErr *Error) {
521+
func tpmParseSANExtension(attestation *x509.Certificate) (out *x509.Certificate, protoErr *Error) {
576522
var (
577523
manufacturer, model, version string
578524
err error
@@ -581,13 +527,13 @@ func tpmParseSANExtension(attestation *x509.Certificate) (protoErr *Error) {
581527
for _, ext := range attestation.Extensions {
582528
if ext.Id.Equal(oidExtensionSubjectAltName) {
583529
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
584-
return ErrInvalidAttestation.WithDetails("Authenticator with invalid Authenticator Identity Key SAN data encountered during attestation validation.").WithInfo(fmt.Sprintf("Error occurred parsing SAN extension: %s", err.Error())).WithError(err)
530+
return nil, ErrInvalidAttestation.WithDetails("Authenticator with invalid Authenticator Identity Key SAN data encountered during attestation validation.").WithInfo(fmt.Sprintf("Error occurred parsing SAN extension: %s", err.Error())).WithError(err)
585531
}
586532
}
587533
}
588534

589535
if manufacturer == "" || model == "" || version == "" {
590-
return ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate.")
536+
return nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate.")
591537
}
592538

593539
var unhandled []asn1.ObjectIdentifier
@@ -600,22 +546,23 @@ func tpmParseSANExtension(attestation *x509.Certificate) (protoErr *Error) {
600546
unhandled = append(unhandled, uce)
601547
}
602548

603-
attestation.UnhandledCriticalExtensions = unhandled
549+
out = new(x509.Certificate)
550+
*out = *attestation
551+
out.UnhandledCriticalExtensions = unhandled
604552

605-
return nil
553+
return out, nil
606554
}
607555

608556
type tpmBasicConstraints struct {
609557
IsCA bool `asn1:"optional"`
610558
MaxPathLen int `asn1:"optional,default:-1"`
611559
}
612560

613-
// Remove extension key usage to avoid ExtKeyUsage check failure.
614-
func tpmRemoveEKU(x5c *x509.Certificate) *Error {
615-
var (
616-
unknown []asn1.ObjectIdentifier
617-
hasAiK bool
618-
)
561+
// tpmRemoveEKU returns a copy of the certificate with the TCG and Microsoft extension key usages removed to avoid the
562+
// ExtKeyUsage check failure, and reports whether the certificate carried the AIK extension key usage. The certificate
563+
// given is never modified as it's owned by the caller.
564+
func tpmRemoveEKU(x5c *x509.Certificate) (out *x509.Certificate, hasAiK bool) {
565+
var unknown []asn1.ObjectIdentifier
619566

620567
for _, eku := range x5c.UnknownExtKeyUsage {
621568
if eku.Equal(oidTCGKpAIKCertificate) {
@@ -631,13 +578,11 @@ func tpmRemoveEKU(x5c *x509.Certificate) *Error {
631578
unknown = append(unknown, eku)
632579
}
633580

634-
if !hasAiK {
635-
return ErrAttestationFormat.WithDetails("Attestation Identity Key certificate missing required Extended Key Usage.")
636-
}
637-
638-
x5c.UnknownExtKeyUsage = unknown
581+
out = new(x509.Certificate)
582+
*out = *x5c
583+
out.UnknownExtKeyUsage = unknown
639584

640-
return nil
585+
return out, hasAiK
641586
}
642587

643588
func init() {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package protocol
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// The values below are Table 2 (TPM Capabilities Vendor ID) of the TCG TPM Vendor ID Registry Family 1.2 and 2.0,
8+
// Version 1.08, July 20, 2026. This is the table which applies as the TPMManufacturer SAN value carries the
9+
// TPM_PT_MANUFACTURER property, not the 16-bit hardware interface VenID of Table 1.
10+
//
11+
// Values in Table 1 with no Table 2 equivalent (i.e. Solidigm), and the simulator and testing values of Table 3, are
12+
// intentionally excluded as they must not appear in a production attestation.
13+
//
14+
// See https://trustedcomputinggroup.org/resource/vendor-id-registry/ for registry contents.
15+
var (
16+
tpmManufacturers = []tpmManufacturer{
17+
{"414D4400", "AMD", "AMD"},
18+
{"414E5400", "Ant Group", "ANT"},
19+
{"41524D00", "Arm", "ARM"},
20+
{"41544D4C", "Atmel", "ATML"},
21+
{"4252434D", "Broadcom", "BRCM"},
22+
{"4353434F", "Cisco", "CSCO"},
23+
{"464C5953", "Flyslice Technologies", "FLYS"},
24+
{"524F4343", "Fuzhou Rockchip", "ROCC"},
25+
{"474F4F47", "Google", "GOOG"},
26+
{"48504900", "HPI", "HPI"},
27+
{"48504500", "HPE", "HPE"},
28+
{"48495349", "Huawei", "HISI"},
29+
{"49424D00", "IBM", "IBM"},
30+
{"49465800", "Infineon", "IFX"},
31+
{"494E5443", "Intel", "INTC"},
32+
{"4C454E00", "Lenovo", "LEN"},
33+
{"4D534654", "Microsoft", "MSFT"},
34+
{"4E534D20", "National Semiconductor", "NSM"},
35+
{"4E545A00", "Nationz", "NTZ"},
36+
{"4E534700", "NSING", "NSG"},
37+
{"4E544300", "Nuvoton Technology", "NTC"},
38+
{"51434F4D", "Qualcomm", "QCOM"},
39+
{"534D534E", "Samsung", "SMSN"},
40+
{"53454345", "SecEdge", "SECE"},
41+
{"534E5300", "Sinosun", "SNS"},
42+
{"534D5343", "SMSC", "SMSC"},
43+
{"53544D20", "STMicroelectronics", "STM"},
44+
{"54584E00", "Texas Instruments", "TXN"},
45+
{"57454300", "Winbond", "WEC"},
46+
{"5345414C", "Wisekey", "SEAL"},
47+
48+
// The following value is not assigned by the TCG, it's used by the FIDO Alliance conformance tooling.
49+
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
50+
}
51+
)
52+
53+
// isValidTPMManufacturer determines if the given TPM manufacturer id is registered. The comparison is deliberately
54+
// case-insensitive as the id is the hexadecimal representation of a four byte value rendered as a string by the
55+
// authenticator, and the case of the hexadecimal digits is not fixed by any specification. The TCG registry itself
56+
// renders IBM as '0x49 0x42 0x4d 0x00' with a lowercase digit.
57+
func isValidTPMManufacturer(id string) bool {
58+
for _, m := range tpmManufacturers {
59+
if strings.EqualFold(m.id, id) {
60+
return true
61+
}
62+
}
63+
64+
return false
65+
}

0 commit comments

Comments
 (0)