@ -26,10 +26,11 @@ import (
"time"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-attestation/attest"
"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/legacy/tpm2"
"golang.org/x/exp/slices"
"github.com/smallstep/go-attestation/attest"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil"
@ -379,13 +380,18 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
return WrapErrorISE ( err , "error unmarshalling CBOR" )
}
format := att . Format
prov := MustProvisionerFromContext ( ctx )
if ! prov . IsAttestationFormatEnabled ( ctx , provisioner . ACMEAttestationFormat ( att . Format ) ) {
if ! prov . IsAttestationFormatEnabled ( ctx , provisioner . ACMEAttestationFormat ( format ) ) {
if format != "apple" && format != "step" && format != "tpm" {
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "unsupported attestation object format %q" , format ) )
}
return storeError ( ctx , db , ch , true ,
NewError ( ErrorBadAttestationStatementType , "attestation format %q is not enabled" , att . Format ) )
NewError ( ErrorBadAttestationStatementType , "attestation format %q is not enabled" , f ormat) )
}
switch att. F ormat {
switch f ormat {
case "apple" :
data , err := doAppleAttestationFormat ( ctx , prov , ch , & att )
if err != nil {
@ -398,11 +404,12 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
}
return WrapErrorISE ( err , "error validating attestation" )
}
// Validate nonce with SHA-256 of the token.
if len ( data . Nonce ) != 0 {
sum := sha256 . Sum256 ( [ ] byte ( ch . Token ) )
if subtle . ConstantTimeCompare ( data . Nonce , sum [ : ] ) != 1 {
return storeError ( ctx , db , ch , true , New Error( ErrorBadAttestationStatementType , "challenge token does not match" ) )
return storeError ( ctx , db , ch , true , New Detailed Error( ErrorBadAttestationStatementType , "challenge token does not match" ) )
}
}
@ -411,7 +418,12 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
//
// Note: We might want to use an external service for this.
if data . UDID != ch . Value && data . SerialNumber != ch . Value {
return storeError ( ctx , db , ch , true , NewError ( ErrorBadAttestationStatementType , "permanent identifier does not match" ) )
subproblem := NewSubproblemWithIdentifier (
ErrorRejectedIdentifierType ,
Identifier { Type : "permanent-identifier" , Value : ch . Value } ,
"challenge identifier %q doesn't match any of the attested hardware identifiers %q" , ch . Value , [ ] string { data . UDID , data . SerialNumber } ,
)
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
}
// Update attestation key fingerprint to compare against the CSR
@ -435,11 +447,11 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
// Note: We might want to use an external service for this.
if data . SerialNumber != ch . Value {
subproblem := NewSubproblemWithIdentifier (
Error Malformed Type,
Error RejectedIdentifier Type,
Identifier { Type : "permanent-identifier" , Value : ch . Value } ,
"challenge identifier %q doesn't match the attested hardware identifier %q" , ch . Value , data . SerialNumber ,
)
return storeError ( ctx , db , ch , true , New Error( ErrorBadAttestationStatementType , "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
return storeError ( ctx , db , ch , true , New Detailed Error( ErrorBadAttestationStatementType , "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
}
// Update attestation key fingerprint to compare against the CSR
@ -448,8 +460,6 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
case "tpm" :
data , err := doTPMAttestationFormat ( ctx , prov , ch , jwk , & att )
if err != nil {
// TODO(hs): we should provide more details in the error reported to the client;
// "Attestation statement cannot be verified" is VERY generic. Also holds true for the other formats.
var acmeError * Error
if errors . As ( err , & acmeError ) {
if acmeError . Status == 500 {
@ -467,17 +477,17 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
// still fail if the challenge value isn't equal to the CSR subject.
if len ( data . PermanentIdentifiers ) > 0 && ! slices . Contains ( data . PermanentIdentifiers , ch . Value ) { // TODO(hs): add support for HardwareModuleName
subproblem := NewSubproblemWithIdentifier (
Error Malformed Type,
Error RejectedIdentifier Type,
Identifier { Type : "permanent-identifier" , Value : ch . Value } ,
"challenge identifier %q doesn't match any of the attested hardware identifiers %q" , ch . Value , data . PermanentIdentifiers ,
)
return storeError ( ctx , db , ch , true , New Error( ErrorRejectedIdentifier Type, "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
return storeError ( ctx , db , ch , true , New DetailedError( ErrorBadAttestationStatement Type, "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
}
// Update attestation key fingerprint to compare against the CSR
az . Fingerprint = data . Fingerprint
default :
return storeError ( ctx , db , ch , true , New Error( ErrorBadAttestationStatementType , "un expected attestation object format" ) )
return storeError ( ctx , db , ch , true , New Detailed Error( ErrorBadAttestationStatementType , "un supported attestation object format %q", format ) )
}
// Update and store the challenge.
@ -520,41 +530,41 @@ const (
coseAlgRS256 coseAlgorithmIdentifier = - 257
)
func doTPMAttestationFormat ( ctx context . Context , prov Provisioner , ch * Challenge , jwk * jose . JSONWebKey , att * attestationObject ) ( * tpmAttestationData , error ) {
func doTPMAttestationFormat ( _ context . Context , prov Provisioner , ch * Challenge , jwk * jose . JSONWebKey , att * attestationObject ) ( * tpmAttestationData , error ) {
ver , ok := att . AttStatement [ "ver" ] . ( string )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "ver not present" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "ver not present" )
}
if ver != "2.0" {
return nil , New Error( ErrorBadAttestationStatementType , "version %q is not supported" , ver )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "version %q is not supported" , ver )
}
x5c , ok := att . AttStatement [ "x5c" ] . ( [ ] interface { } )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c not present" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c not present" )
}
if len ( x5c ) == 0 {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is empty" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is empty" )
}
akCertBytes , ok := x5c [ 0 ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is malformed" )
}
akCert , err := x509 . ParseCertificate ( akCertBytes )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
}
intermediates := x509 . NewCertPool ( )
for _ , v := range x5c [ 1 : ] {
intCertBytes , vok := v . ( [ ] byte )
if ! vok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is malformed" )
}
intCert , err := x509 . ParseCertificate ( intCertBytes )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
}
intermediates . AddCert ( intCert )
}
@ -592,19 +602,19 @@ func doTPMAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge
KeyUsages : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageAny } ,
} )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is not valid" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is not valid" )
}
// validate additional AK certificate requirements
if err := validateAKCertificate ( akCert ) ; err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "AK certificate is not valid" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "AK certificate is not valid" )
}
// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
sans , err := x509util . ParseSubjectAlternativeNames ( akCert )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "failed parsing AK certificate Subject Alternative Names" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "failed parsing AK certificate Subject Alternative Names" )
}
permanentIdentifiers := make ( [ ] string , len ( sans . PermanentIdentifiers ) )
@ -615,37 +625,37 @@ func doTPMAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge
// extract and validate pubArea, sig, certInfo and alg properties from the request body
pubArea , ok := att . AttStatement [ "pubArea" ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "invalid pubArea in attestation statement" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "invalid pubArea in attestation statement" )
}
if len ( pubArea ) == 0 {
return nil , New Error( ErrorBadAttestationStatementType , "pubArea is empty" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "pubArea is empty" )
}
sig , ok := att . AttStatement [ "sig" ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "invalid sig in attestation statement" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "invalid sig in attestation statement" )
}
if len ( sig ) == 0 {
return nil , New Error( ErrorBadAttestationStatementType , "sig is empty" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "sig is empty" )
}
certInfo , ok := att . AttStatement [ "certInfo" ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "invalid certInfo in attestation statement" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "invalid certInfo in attestation statement" )
}
if len ( certInfo ) == 0 {
return nil , New Error( ErrorBadAttestationStatementType , "certInfo is empty" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "certInfo is empty" )
}
alg , ok := att . AttStatement [ "alg" ] . ( int64 )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "invalid alg in attestation statement" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "invalid alg in attestation statement" )
}
// only RS256 and ES256 are allowed
coseAlg := coseAlgorithmIdentifier ( alg )
if coseAlg != coseAlgRS256 && coseAlg != coseAlgES256 {
return nil , New Error( ErrorBadAttestationStatementType , "invalid alg %d in attestation statement" , alg )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "invalid alg %d in attestation statement" , alg )
}
// set the hash algorithm to use to SHA256
@ -663,36 +673,36 @@ func doTPMAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge
Hash : hash ,
}
if err = certificationParameters . Verify ( verifyOpts ) ; err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "invalid certification parameters" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "invalid certification parameters" )
}
// decode the "certInfo" data. This won't fail, as it's also done as part of Verify().
tpmCertInfo , err := tpm2 . DecodeAttestationData ( certInfo )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "failed decoding attestation data" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "failed decoding attestation data" )
}
keyAuth , err := KeyAuthorization ( ch . Token , jwk )
if err != nil {
return nil , WrapError ( ErrorBadAttestationStatementType , err , "failed creating key auth digest" )
return nil , WrapError ISE ( err , "failed creating key auth digest" )
}
hashedKeyAuth := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
// verify the WebAuthn object contains the expect key authorization digest, which is carried
// within the encoded `certInfo` property of the attestation statement.
if subtle . ConstantTimeCompare ( hashedKeyAuth [ : ] , [ ] byte ( tpmCertInfo . ExtraData ) ) == 0 {
return nil , New Error( ErrorBadAttestationStatementType , "key authorization does not match ")
return nil , New Detailed Error( ErrorBadAttestationStatementType , "key authorization invalid ")
}
// decode the (attested) public key and determine its fingerprint. This won't fail, as it's also done as part of Verify().
pub , err := tpm2 . DecodePublic ( pubArea )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "failed decoding pubArea" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "failed decoding pubArea" )
}
publicKey , err := pub . Key ( )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "failed getting public key" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "failed getting public key" )
}
data := & tpmAttestationData {
@ -742,11 +752,7 @@ func validateAKCertificate(c *x509.Certificate) error {
if err := validateAKCertificateExtendedKeyUsage ( c ) ; err != nil {
return err
}
if err := validateAKCertificateSubjectAlternativeNames ( c ) ; err != nil {
return err
}
return nil
return validateAKCertificateSubjectAlternativeNames ( c )
}
// validateAKCertificateSubjectAlternativeNames checks if the AK certificate
@ -828,7 +834,7 @@ type appleAttestationData struct {
Fingerprint string
}
func doAppleAttestationFormat ( ctx context . Context , prov Provisioner , ch * Challenge , att * attestationObject ) ( * appleAttestationData , error ) {
func doAppleAttestationFormat ( _ context . Context , prov Provisioner , _ * Challenge , att * attestationObject ) ( * appleAttestationData , error ) {
// Use configured or default attestation roots if none is configured.
roots , ok := prov . GetAttestationRoots ( )
if ! ok {
@ -842,30 +848,30 @@ func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challen
x5c , ok := att . AttStatement [ "x5c" ] . ( [ ] interface { } )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c not present" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c not present" )
}
if len ( x5c ) == 0 {
return nil , New Error( ErrorRejectedIdentifier Type, "x5c is empty" )
return nil , New DetailedError( ErrorBadAttestationStatement Type, "x5c is empty" )
}
der , ok := x5c [ 0 ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is malformed" )
}
leaf , err := x509 . ParseCertificate ( der )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
}
intermediates := x509 . NewCertPool ( )
for _ , v := range x5c [ 1 : ] {
der , ok = v . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is malformed" )
}
cert , err := x509 . ParseCertificate ( der )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
}
intermediates . AddCert ( cert )
}
@ -876,7 +882,7 @@ func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challen
CurrentTime : time . Now ( ) . Truncate ( time . Second ) ,
KeyUsages : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageAny } ,
} ) ; err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is not valid" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is not valid" )
}
data := & appleAttestationData {
@ -933,7 +939,7 @@ type stepAttestationData struct {
Fingerprint string
}
func doStepAttestationFormat ( ctx context . Context , prov Provisioner , ch * Challenge , jwk * jose . JSONWebKey , att * attestationObject ) ( * stepAttestationData , error ) {
func doStepAttestationFormat ( _ context . Context , prov Provisioner , ch * Challenge , jwk * jose . JSONWebKey , att * attestationObject ) ( * stepAttestationData , error ) {
// Use configured or default attestation roots if none is configured.
roots , ok := prov . GetAttestationRoots ( )
if ! ok {
@ -948,28 +954,28 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
// Extract x5c and verify certificate
x5c , ok := att . AttStatement [ "x5c" ] . ( [ ] interface { } )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c not present" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c not present" )
}
if len ( x5c ) == 0 {
return nil , New Error( ErrorRejectedIdentifierType , "x5c is empty" )
return nil , New Detailed Error( ErrorRejectedIdentifierType , "x5c is empty" )
}
der , ok := x5c [ 0 ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is malformed" )
}
leaf , err := x509 . ParseCertificate ( der )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
}
intermediates := x509 . NewCertPool ( )
for _ , v := range x5c [ 1 : ] {
der , ok = v . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "x5c is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "x5c is malformed" )
}
cert , err := x509 . ParseCertificate ( der )
if err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is malformed" )
}
intermediates . AddCert ( cert )
}
@ -979,7 +985,7 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
CurrentTime : time . Now ( ) . Truncate ( time . Second ) ,
KeyUsages : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageAny } ,
} ) ; err != nil {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "x5c is not valid" )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "x5c is not valid" )
}
// Verify proof of possession of private key validating the key
@ -989,10 +995,10 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
var sig [ ] byte
csig , ok := att . AttStatement [ "sig" ] . ( [ ] byte )
if ! ok {
return nil , New Error( ErrorBadAttestationStatementType , "sig not present" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "sig not present" )
}
if err := cbor . Unmarshal ( csig , & sig ) ; err != nil {
return nil , New Error( ErrorBadAttestationStatementType , "sig is malformed" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "sig is malformed" )
}
keyAuth , err := KeyAuthorization ( ch . Token , jwk )
if err != nil {
@ -1002,23 +1008,23 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
switch pub := leaf . PublicKey . ( type ) {
case * ecdsa . PublicKey :
if pub . Curve != elliptic . P256 ( ) {
return nil , Wrap Error( ErrorBadAttestationStatementType , err , "unsupported elliptic curve %s" , pub . Curve )
return nil , Wrap Detailed Error( ErrorBadAttestationStatementType , err , "unsupported elliptic curve %s" , pub . Curve )
}
sum := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
if ! ecdsa . VerifyASN1 ( pub , sum [ : ] , sig ) {
return nil , New Error( ErrorBadAttestationStatementType , "failed to validate signature" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "failed to validate signature" )
}
case * rsa . PublicKey :
sum := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
if err := rsa . VerifyPKCS1v15 ( pub , crypto . SHA256 , sum [ : ] , sig ) ; err != nil {
return nil , New Error( ErrorBadAttestationStatementType , "failed to validate signature" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "failed to validate signature" )
}
case ed25519 . PublicKey :
if ! ed25519 . Verify ( pub , [ ] byte ( keyAuth ) , sig ) {
return nil , New Error( ErrorBadAttestationStatementType , "failed to validate signature" )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "failed to validate signature" )
}
default :
return nil , New Error( ErrorBadAttestationStatementType , "unsupported public key type %T" , pub )
return nil , New Detailed Error( ErrorBadAttestationStatementType , "unsupported public key type %T" , pub )
}
// Parse attestation data: