Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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