Skip to content

Commit

Permalink
Add packed attestation verification (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
e3b0c442 authored Jan 25, 2020
1 parent 40cb145 commit 315d03b
Show file tree
Hide file tree
Showing 8 changed files with 1,063 additions and 28 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2020-01-21]
## [0.4.0] - 2020-01-25
### Added
- Attestation verification for the _packed_ format has been added, continuing the previous guidance for trust chain validation. ECDAA attestation type is not supported due to lack of ECDAA support in Go standard library.

## [0.3.0] - 2020-01-21
### Added
- Attestation verification for the _fido-u2f_ format has been added. The attestation type and trust path are __not__ validated, only the signature against the provided certificate. It is up to the implementor to verify the trust chain using the `*AttestationObject` returned from `FinishRegistration`.
### Changed
Expand All @@ -18,7 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `UnmarshalBinary` and `MarshalBinary` methods on `AttestationObject` and `AuthenticatorData`, implementing the `BinaryMarshaler` and `BinaryUnmarshaler` interfaces
- `Encode` method on `AuthenticatorData` to facilitate encoding `AttestationObject` for storage

### Changed
- **[BREAKING]** `FinishRegistration` now returns `(*AttestationObject, error)` instead of `(string, []byte, error)`, to allow the implementor to choose how much or little of the authenticator data to save.
- **[BREAKING]** `FinishAuthentication` now returns `(*AuthenticatorData, error)` instead of `(uint, error)`, to allow the implementor full access to the authenticator data for other uses
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ _warp_ was built with the following goals in mind:
* Supported: ES256, ES384, ES512, EdDSA, RS1, RS256, RS384, RS512, PS256, PS384, PS512
* To be implemented: None plannned
* Attestation formats
* Supported: _fido-u2f_, _none_
* To be implemented: _packed_, _tpm_, _android-key_, _android-safetynet_
* Supported: _packed, _fido-u2f_, _none_
* To be implemented: _tpm_, _android-key_, _android-safetynet_
* Defined extensions
* Supported: _appid_
* To be implemented: _txAuthSimple_, _txAuthGeneric_, _authnSel_, _exts_, _uvi_, _loc_, _uvm_, _biometricPerfBounds_
Expand Down
149 changes: 148 additions & 1 deletion attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"encoding/asn1"

"github.com/fxamacker/cbor"
)

var idFidoGenCeAaguid asn1.ObjectIdentifier = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4})

//AttestationObject contains both authenticator data and an attestation
//statement.
type AttestationObject struct {
Expand Down Expand Up @@ -92,6 +95,150 @@ func VerifyNoneAttestationStatement(attStmt []byte, _ []byte, _ [32]byte) error
return nil
}

//PackedAttestationStatement represents a decoded attestation statement of type
//"packed"
type PackedAttestationStatement struct {
Alg COSEAlgorithmIdentifier `cbor:"alg"`
Sig []byte `cbor:"sig"`
X5C [][]byte `cbor:"x5c"`
ECDAAKeyID []byte `cbor:"ecdaaKeyId"`
}

var coseToSigAlg = map[COSEAlgorithmIdentifier]x509.SignatureAlgorithm{
AlgorithmES256: x509.ECDSAWithSHA256,
AlgorithmES384: x509.ECDSAWithSHA384,
AlgorithmES512: x509.ECDSAWithSHA512,
AlgorithmEdDSA: x509.PureEd25519,
AlgorithmPS256: x509.SHA256WithRSAPSS,
AlgorithmPS384: x509.SHA384WithRSAPSS,
AlgorithmPS512: x509.SHA512WithRSAPSS,
AlgorithmRS1: x509.SHA1WithRSA,
AlgorithmRS256: x509.SHA256WithRSA,
AlgorithmRS384: x509.SHA384WithRSA,
AlgorithmRS512: x509.SHA512WithRSA,
}

//VerifyPackedAttestationStatement verifies that an attestation statement of
//type "packed" is valid
func VerifyPackedAttestationStatement(attStmt []byte, rawAuthData []byte, clientDataHash [32]byte) error {
//1. Verify that attStmt is valid CBOR conforming to the syntax defined
//above and perform CBOR decoding on it to extract the contained fields.
var att PackedAttestationStatement
if err := cbor.Unmarshal(attStmt, &att); err != nil {
return ErrVerifyAttestation.Wrap(NewError("packed attestation statement not valid CBOR").Wrap(err))
}

authData := AuthenticatorData{}
if err := authData.UnmarshalBinary(rawAuthData); err != nil {
return ErrVerifyAttestation.Wrap(NewError("error unmarshaling authData"))
}

verificationData := append(rawAuthData, clientDataHash[:]...)

if len(att.X5C) > 0 {
//2. If x5c is present, this indicates that the attestation type is not
//ECDAA. In this case:

//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.
attestnCert, err := x509.ParseCertificate(att.X5C[0])
if err != nil {
return ErrVerifyAttestation.Wrap(NewError("error parsing attestation certificate").Wrap(err))
}

sigAlg, ok := coseToSigAlg[att.Alg]
if !ok {
return ErrVerifyAttestation.Wrap(NewError("unsupported signature algorithm").Wrap(err))
}
if err = attestnCert.CheckSignature(sigAlg, verificationData, att.Sig); err != nil {
return ErrVerifyAttestation.Wrap(NewError("error verifying signature over auth and client data").Wrap(err))
}

//Verify that attestnCert meets the requirements in §8.2.1 Packed
//Attestation Statement Certificate Requirements.

//Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with
//value 2).
if attestnCert.Version != 3 {
return ErrVerifyAttestation.Wrap(NewError("invalid attestation certificate version"))
}

//Subject field MUST be set to:
//
// * Subject-C
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)
// * Subject-O
// Legal name of the Authenticator vendor (UTF8String)
// * Subject-OU
// Literal string “Authenticator Attestation” (UTF8String)
// * Subject-CN
// A UTF8String of the vendor’s choosing
if len(attestnCert.Subject.Country) < 1 || len(attestnCert.Subject.Country[0]) < 2 || len(attestnCert.Subject.Country[0]) > 3 {
return ErrVerifyAttestation.Wrap(NewError("invalid subject country, must be ISO3166 code"))
}
if len(attestnCert.Subject.Organization) < 1 {
return ErrVerifyAttestation.Wrap(NewError("subject organization not present"))
}
if len(attestnCert.Subject.OrganizationalUnit) < 1 || attestnCert.Subject.OrganizationalUnit[0] != "Authenticator Attestation" {
return ErrVerifyAttestation.Wrap(NewError("invalid subject organizational unit, must be \"Authenticator Attestation\""))
}
if attestnCert.Subject.CommonName == "" {
return ErrVerifyAttestation.Wrap(NewError("subject common name not present"))
}

//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.
for _, ext := range attestnCert.Extensions {
if ext.Id.Equal(idFidoGenCeAaguid) {
if ext.Critical {
return ErrVerifyAttestation.Wrap(NewError("AAGUID extension marked critical"))
}
var certAAGUID []byte
_, err := asn1.Unmarshal(ext.Value, &certAAGUID)
if err != nil {
return ErrVerifyAttestation.Wrap(NewError("error unmarshaling certificate AAGUID").Wrap(err))
}

if !bytes.Equal(certAAGUID, authData.AttestedCredentialData.AAGUID[:]) {
return ErrVerifyAttestation.Wrap(NewError("AAGUID mismatch"))
}
}
}

//The Basic Constraints extension MUST have the CA component set to false.
if attestnCert.IsCA {
return ErrVerifyAttestation.Wrap(NewError("attestation certificate has CA constraint"))
}
} else if att.ECDAAKeyID != nil {
//3. If ecdaaKeyId is present, then the attestation type is ECDAA.
return ErrVerifyAttestation.Wrap(ErrECDAANotSupported)
} else {
//4. If neither x5c nor ecdaaKeyId is present, self attestation is in
//use.

//Validate that alg matches the algorithm of the credentialPublicKey in
//authenticatorData.
var credPubKey COSEKey
if err := cbor.Unmarshal(authData.AttestedCredentialData.CredentialPublicKey, &credPubKey); err != nil {
return ErrVerifyAttestation.Wrap(NewError("error unmarshaling credential public key").Wrap(err))
}
if credPubKey.Alg != int(att.Alg) {
return ErrVerifyAttestation.Wrap(NewError("credential public key algorithm does not match attestation algorithm"))
}

//Verify that sig is a valid signature over the concatenation of
//authenticatorData and clientDataHash using the credential public key
//with alg.
if err := VerifySignature(authData.AttestedCredentialData.CredentialPublicKey, verificationData, att.Sig); err != nil {
return ErrVerifyAttestation.Wrap(err)
}
}

return nil
}

//FIDOU2FAttestationStatement represents a decoded attestation statement of type
//"fido-u2f"
type FIDOU2FAttestationStatement struct {
Expand All @@ -106,7 +253,7 @@ func VerifyFIDOU2FAttestationStatement(attStmt []byte, rawAuthData []byte, clien
//above and perform CBOR decoding on it to extract the contained fields.
var att FIDOU2FAttestationStatement
if err := cbor.Unmarshal(attStmt, &att); err != nil {
return ErrVerifyAttestation.Wrap(NewError("fido-u2f attestation statement not valid cbor").Wrap(err))
return ErrVerifyAttestation.Wrap(NewError("fido-u2f attestation statement not valid CBOR").Wrap(err))
}

//2. Check that x5c has exactly one element and let attCert be that element.
Expand Down
Loading

0 comments on commit 315d03b

Please sign in to comment.