1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
|
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"strings"
"time"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
var packedAttestationKey = "packed"
func init() {
RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat)
}
// The packed attestation statement looks like:
//
// packedStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
// } OR
// {
// alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
// sig: bytes,
// ecdaaKeyId: bytes
// } OR
// {
// alg: COSEAlgorithmIdentifier
// sig: bytes,
// }
//
// Specification: §8.2. Packed Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-packed-attestation)
func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
alg, present := att.AttStatement["alg"].(int64)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if x509present {
// Handle Basic Attestation steps for the x509 Certificate
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c)
}
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
// Also make sure the we did not have an x509 then
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
if ecdaaKeyPresent {
// Handle ECDAA Attestation steps for the x509 Certificate
return handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID)
}
// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
return handleSelfAttestation(alg, att.AuthData.AttData.CredentialPublicKey, att.RawAuthData, clientDataHash, sig)
}
// Handle the attestation steps laid out in
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, error) {
// Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData
// and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
for _, c := range x5c {
cb, cv := c.([]byte)
if !cv {
return "", x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
ct, err := x509.ParseCertificate(cb)
if err != nil {
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
if ct.NotBefore.After(time.Now()) || ct.NotAfter.Before(time.Now()) {
return "", x5c, ErrAttestationFormat.WithDetails("Cert in chain not time valid")
}
}
attCertBytes, valid := x5c[0].([]byte)
if !valid {
return "", x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(authData, clientDataHash...)
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
if err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, signature); err != nil {
return "", x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
// §8.2.1 can be found here https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
// Step 2.2.1 (from §8.2.1) Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
if attCert.Version != 3 {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate is incorrect version")
}
// Step 2.2.2 (from §8.2.1) Subject field MUST be set to:
// Subject-C
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)
// TODO: Find a good, useable, country code library. For now, check stringy-ness
subjectString := strings.Join(attCert.Subject.Country, "")
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid")
}
// Subject-O
// Legal name of the Authenticator vendor (UTF8String)
subjectString = strings.Join(attCert.Subject.Organization, "")
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid")
}
// Subject-OU
// Literal string “Authenticator Attestation” (UTF8String)
subjectString = strings.Join(attCert.Subject.OrganizationalUnit, " ")
if subjectString != "Authenticator Attestation" {
// TODO: Implement a return error when I'm more certain this is general practice
}
// Subject-CN
// A UTF8String of the vendor’s choosing
subjectString = attCert.Subject.CommonName
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set")
}
// TODO: And then what
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
idFido := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
var foundAAGUID []byte
for _, extension := range attCert.Extensions {
if extension.Id.Equal(idFido) {
if extension.Critical {
return "", x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
}
foundAAGUID = extension.Value
}
}
// We validate the AAGUID as mentioned above
// 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
// and make sure it matches the auth data AAGUID
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
if len(foundAAGUID) > 0 {
unMarshalledAAGUID := []byte{}
asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID)
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
return "", x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
}
}
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
if attCert.IsCA {
return "", x5c, ErrInvalidAttestation.WithDetails("Attestation certificate's Basic Constraints marked as CA")
}
// Note for 2.2.5 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL
// Distribution Point extension [RFC5280](https://www.w3.org/TR/webauthn/#biblio-rfc5280) are
// both OPTIONAL as the status of many attestation certificates is available through authenticator
// metadata services. See, for example, the FIDO Metadata Service
// [FIDOMetadataService] (https://www.w3.org/TR/webauthn/#biblio-fidometadataservice)
// Step 2.4 If successful, return attestation type Basic and attestation trust path x5c.
// We don't handle trust paths yet but we're done
return string(metadata.BasicFull), x5c, nil
}
func handleECDAAAttestation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
}
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) {
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
// clientDataHash using the credential public key with alg.
verificationData := append(authData, clientDataHash...)
key, err := webauthncose.ParsePublicKey(pubKey)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v\n", err))
}
switch k := key.(type) {
case webauthncose.OKPPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
case webauthncose.EC2PublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
case webauthncose.RSAPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
default:
return "", nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
}
if err != nil {
return "", nil, err
}
valid, err := webauthncose.VerifySignature(key, verificationData, signature)
if !valid && err == nil {
return "", nil, ErrInvalidAttestation.WithDetails("Unable to verify signature")
}
return string(metadata.BasicSurrogate), nil, err
}
func verifyKeyAlgorithm(keyAlgorithm, attestedAlgorithm int64) error {
if keyAlgorithm != attestedAlgorithm {
return ErrInvalidAttestation.WithDetails("Public key algorithm does not equal att statement algorithm")
}
return nil
}
|