You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

attestation_tpm.go 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. package protocol
  2. import (
  3. "bytes"
  4. "crypto/x509"
  5. "crypto/x509/pkix"
  6. "encoding/asn1"
  7. "errors"
  8. "fmt"
  9. "math/big"
  10. "strings"
  11. "github.com/duo-labs/webauthn/protocol/webauthncose"
  12. "github.com/duo-labs/webauthn/protocol/googletpm"
  13. )
  14. var tpmAttestationKey = "tpm"
  15. func init() {
  16. RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat)
  17. googletpm.UseTPM20LengthPrefixSize()
  18. }
  19. func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
  20. // Given the verification procedure inputs attStmt, authenticatorData
  21. // and clientDataHash, the verification procedure is as follows
  22. // Verify that attStmt is valid CBOR conforming to the syntax defined
  23. // above and perform CBOR decoding on it to extract the contained fields
  24. ver, present := att.AttStatement["ver"].(string)
  25. if !present {
  26. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving ver value")
  27. }
  28. if ver != "2.0" {
  29. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
  30. }
  31. alg, present := att.AttStatement["alg"].(int64)
  32. if !present {
  33. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
  34. }
  35. coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
  36. x5c, x509present := att.AttStatement["x5c"].([]interface{})
  37. if !x509present {
  38. // Handle Basic Attestation steps for the x509 Certificate
  39. return tpmAttestationKey, nil, ErrNotImplemented
  40. }
  41. _, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
  42. if ecdaaKeyPresent {
  43. return tpmAttestationKey, nil, ErrNotImplemented
  44. }
  45. sigBytes, present := att.AttStatement["sig"].([]byte)
  46. if !present {
  47. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
  48. }
  49. certInfoBytes, present := att.AttStatement["certInfo"].([]byte)
  50. if !present {
  51. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving certInfo value")
  52. }
  53. pubAreaBytes, present := att.AttStatement["pubArea"].([]byte)
  54. if !present {
  55. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving pubArea value")
  56. }
  57. // Verify that the public key specified by the parameters and unique fields of pubArea
  58. // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
  59. pubArea, err := googletpm.DecodePublic(pubAreaBytes)
  60. if err != nil {
  61. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement")
  62. }
  63. key, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
  64. switch key.(type) {
  65. case webauthncose.EC2PublicKeyData:
  66. e := key.(webauthncose.EC2PublicKeyData)
  67. if pubArea.ECCParameters.CurveID != googletpm.EllipticCurve(e.Curve) ||
  68. 0 != pubArea.ECCParameters.Point.X.Cmp(new(big.Int).SetBytes(e.XCoord)) ||
  69. 0 != pubArea.ECCParameters.Point.Y.Cmp(new(big.Int).SetBytes(e.YCoord)) {
  70. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
  71. }
  72. case webauthncose.RSAPublicKeyData:
  73. r := key.(webauthncose.RSAPublicKeyData)
  74. mod := new(big.Int).SetBytes(r.Modulus)
  75. exp := uint32(r.Exponent[0]) + uint32(r.Exponent[1])<<8 + uint32(r.Exponent[2])<<16
  76. if 0 != pubArea.RSAParameters.Modulus.Cmp(mod) ||
  77. pubArea.RSAParameters.Exponent != exp {
  78. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
  79. }
  80. default:
  81. return "", nil, ErrUnsupportedKey
  82. }
  83. // Concatenate authenticatorData and clientDataHash to form attToBeSigned
  84. attToBeSigned := append(att.RawAuthData, clientDataHash...)
  85. // Validate that certInfo is valid:
  86. certInfo, err := googletpm.DecodeAttestationData(certInfoBytes)
  87. // 1/4 Verify that magic is set to TPM_GENERATED_VALUE.
  88. if certInfo.Magic != 0xff544347 {
  89. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Magic is not set to TPM_GENERATED_VALUE")
  90. }
  91. // 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
  92. if certInfo.Type != googletpm.TagAttestCertify {
  93. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Type is not set to TPM_ST_ATTEST_CERTIFY")
  94. }
  95. // 3/4 Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
  96. f := webauthncose.HasherFromCOSEAlg(coseAlg)
  97. h := f()
  98. h.Write(attToBeSigned)
  99. if 0 != bytes.Compare(certInfo.ExtraData, h.Sum(nil)) {
  100. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
  101. }
  102. // 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
  103. // [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
  104. // as computed using the algorithm in the nameAlg field of pubArea
  105. // using the procedure specified in [TPMv2-Part1] section 16.
  106. f, err = certInfo.AttestedCertifyInfo.Name.Digest.Alg.HashConstructor()
  107. h = f()
  108. h.Write(pubAreaBytes)
  109. if 0 != bytes.Compare(h.Sum(nil), certInfo.AttestedCertifyInfo.Name.Digest.Value) {
  110. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea")
  111. }
  112. // Note that the remaining fields in the "Standard Attestation Structure"
  113. // [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion
  114. // are ignored. These fields MAY be used as an input to risk engines.
  115. // If x5c is present, this indicates that the attestation type is not ECDAA.
  116. if x509present {
  117. // In this case:
  118. // Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
  119. aikCertBytes, valid := x5c[0].([]byte)
  120. if !valid {
  121. return tpmAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
  122. }
  123. aikCert, err := x509.ParseCertificate(aikCertBytes)
  124. if err != nil {
  125. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
  126. }
  127. sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
  128. err = aikCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), certInfoBytes, sigBytes)
  129. if err != nil {
  130. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
  131. }
  132. // Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements
  133. // 1/6 Version MUST be set to 3.
  134. if aikCert.Version != 3 {
  135. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
  136. }
  137. // 2/6 Subject field MUST be set to empty.
  138. if aikCert.Subject.String() != "" {
  139. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty")
  140. }
  141. // 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9{}
  142. var manufacturer, model, version string
  143. for _, ext := range aikCert.Extensions {
  144. if ext.Id.Equal([]int{2, 5, 29, 17}) {
  145. manufacturer, model, version, err = parseSANExtension(ext.Value)
  146. }
  147. }
  148. if manufacturer == "" || model == "" || version == "" {
  149. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
  150. }
  151. if false == isValidTPMManufacturer(manufacturer) {
  152. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid TPM manufacturer")
  153. }
  154. // 4/6 The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
  155. var ekuValid = false
  156. var eku []asn1.ObjectIdentifier
  157. for _, ext := range aikCert.Extensions {
  158. if ext.Id.Equal([]int{2, 5, 29, 37}) {
  159. rest, err := asn1.Unmarshal(ext.Value, &eku)
  160. if len(rest) != 0 || err != nil || !eku[0].Equal(tcgKpAIKCertificate) {
  161. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3")
  162. }
  163. ekuValid = true
  164. }
  165. }
  166. if false == ekuValid {
  167. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
  168. }
  169. // 5/6 The Basic Constraints extension MUST have the CA component set to false.
  170. type basicConstraints struct {
  171. IsCA bool `asn1:"optional"`
  172. MaxPathLen int `asn1:"optional,default:-1"`
  173. }
  174. var constraints basicConstraints
  175. for _, ext := range aikCert.Extensions {
  176. if ext.Id.Equal([]int{2, 5, 29, 19}) {
  177. if rest, err := asn1.Unmarshal(ext.Value, &constraints); err != nil {
  178. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
  179. } else if len(rest) != 0 {
  180. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
  181. }
  182. }
  183. }
  184. if constraints.IsCA != false {
  185. return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
  186. }
  187. // 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
  188. // extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
  189. // through metadata services. See, for example, the FIDO Metadata Service.
  190. }
  191. return tpmAttestationKey, x5c, err
  192. }
  193. func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
  194. // RFC 5280, 4.2.1.6
  195. // SubjectAltName ::= GeneralNames
  196. //
  197. // GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
  198. //
  199. // GeneralName ::= CHOICE {
  200. // otherName [0] OtherName,
  201. // rfc822Name [1] IA5String,
  202. // dNSName [2] IA5String,
  203. // x400Address [3] ORAddress,
  204. // directoryName [4] Name,
  205. // ediPartyName [5] EDIPartyName,
  206. // uniformResourceIdentifier [6] IA5String,
  207. // iPAddress [7] OCTET STRING,
  208. // registeredID [8] OBJECT IDENTIFIER }
  209. var seq asn1.RawValue
  210. rest, err := asn1.Unmarshal(extension, &seq)
  211. if err != nil {
  212. return err
  213. } else if len(rest) != 0 {
  214. return errors.New("x509: trailing data after X.509 extension")
  215. }
  216. if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
  217. return asn1.StructuralError{Msg: "bad SAN sequence"}
  218. }
  219. rest = seq.Bytes
  220. for len(rest) > 0 {
  221. var v asn1.RawValue
  222. rest, err = asn1.Unmarshal(rest, &v)
  223. if err != nil {
  224. return err
  225. }
  226. if err := callback(v.Tag, v.Bytes); err != nil {
  227. return err
  228. }
  229. }
  230. return nil
  231. }
  232. const (
  233. nameTypeDN = 4
  234. )
  235. var (
  236. tcgKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
  237. tcgAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
  238. tcgAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
  239. tcgAtTpmVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
  240. )
  241. func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) {
  242. err = forEachSAN(value, func(tag int, data []byte) error {
  243. switch tag {
  244. case nameTypeDN:
  245. tpmDeviceAttributes := pkix.RDNSequence{}
  246. _, err := asn1.Unmarshal(data, &tpmDeviceAttributes)
  247. if err != nil {
  248. return err
  249. }
  250. for _, rdn := range tpmDeviceAttributes {
  251. if len(rdn) == 0 {
  252. continue
  253. }
  254. for _, atv := range rdn {
  255. value, ok := atv.Value.(string)
  256. if !ok {
  257. continue
  258. }
  259. if atv.Type.Equal(tcgAtTpmManufacturer) {
  260. manufacturer = strings.TrimPrefix(value, "id:")
  261. }
  262. if atv.Type.Equal(tcgAtTpmModel) {
  263. model = value
  264. }
  265. if atv.Type.Equal(tcgAtTpmVersion) {
  266. version = strings.TrimPrefix(value, "id:")
  267. }
  268. }
  269. }
  270. }
  271. return nil
  272. })
  273. return
  274. }
  275. var tpmManufacturers = []struct {
  276. id string
  277. name string
  278. code string
  279. }{
  280. {"414D4400", "AMD", "AMD"},
  281. {"41544D4C", "Atmel", "ATML"},
  282. {"4252434D", "Broadcom", "BRCM"},
  283. {"49424d00", "IBM", "IBM"},
  284. {"49465800", "Infineon", "IFX"},
  285. {"494E5443", "Intel", "INTC"},
  286. {"4C454E00", "Lenovo", "LEN"},
  287. {"4E534D20", "National Semiconductor", "NSM"},
  288. {"4E545A00", "Nationz", "NTZ"},
  289. {"4E544300", "Nuvoton Technology", "NTC"},
  290. {"51434F4D", "Qualcomm", "QCOM"},
  291. {"534D5343", "SMSC", "SMSC"},
  292. {"53544D20", "ST Microelectronics", "STM"},
  293. {"534D534E", "Samsung", "SMSN"},
  294. {"534E5300", "Sinosun", "SNS"},
  295. {"54584E00", "Texas Instruments", "TXN"},
  296. {"57454300", "Winbond", "WEC"},
  297. {"524F4343", "Fuzhouk Rockchip", "ROCC"},
  298. {"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
  299. }
  300. func isValidTPMManufacturer(id string) bool {
  301. for _, m := range tpmManufacturers {
  302. if m.id == id {
  303. return true
  304. }
  305. }
  306. return false
  307. }