b6957358fc
- Root CA error message improved - Looping through intermediate certs - Change checking unhandled extensions to using `if`
927 lines
33 KiB
Go
927 lines
33 KiB
Go
//go:build tpmsimulator
|
|
// +build tpmsimulator
|
|
|
|
package acme
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/google/go-attestation/attest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/keyutil"
|
|
"go.step.sm/crypto/minica"
|
|
"go.step.sm/crypto/tpm"
|
|
"go.step.sm/crypto/tpm/simulator"
|
|
tpmstorage "go.step.sm/crypto/tpm/storage"
|
|
"go.step.sm/crypto/x509util"
|
|
)
|
|
|
|
func newSimulatedTPM(t *testing.T) *tpm.TPM {
|
|
t.Helper()
|
|
tmpDir := t.TempDir()
|
|
tpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead
|
|
require.NoError(t, err)
|
|
return tpm
|
|
}
|
|
|
|
func withSimulator(t *testing.T) tpm.NewTPMOption {
|
|
t.Helper()
|
|
var sim simulator.Simulator
|
|
t.Cleanup(func() {
|
|
if sim == nil {
|
|
return
|
|
}
|
|
err := sim.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
sim = simulator.New()
|
|
err := sim.Open()
|
|
require.NoError(t, err)
|
|
return tpm.WithSimulator(sim)
|
|
}
|
|
|
|
func generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {
|
|
t.Helper()
|
|
b, err := x509.MarshalPKIXPublicKey(pub)
|
|
require.NoError(t, err)
|
|
hash := sha256.Sum256(b)
|
|
return hash[:]
|
|
}
|
|
|
|
func mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {
|
|
t.Helper()
|
|
aca, err := minica.New(
|
|
minica.WithName("TPM Testing"),
|
|
minica.WithGetSignerFunc(
|
|
func() (crypto.Signer, error) {
|
|
return keyutil.GenerateSigner("RSA", "", 2048)
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// prepare simulated TPM and create an AK
|
|
stpm := newSimulatedTPM(t)
|
|
eks, err := stpm.GetEKs(context.Background())
|
|
require.NoError(t, err)
|
|
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ak)
|
|
|
|
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
|
ap, err := ak.AttestationParameters(context.Background())
|
|
require.NoError(t, err)
|
|
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
|
require.NoError(t, err)
|
|
|
|
// create template and sign certificate for the AK public key
|
|
keyID := generateKeyID(t, eks[0].Public())
|
|
template := &x509.Certificate{
|
|
PublicKey: akp.Public,
|
|
}
|
|
if len(permanentIdentifiers) == 0 {
|
|
template.URIs = []*url.URL{
|
|
{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)},
|
|
}
|
|
} else {
|
|
san := x509util.SubjectAlternativeName{
|
|
Type: x509util.PermanentIdentifierType,
|
|
Value: permanentIdentifiers[0], // TODO(hs): multiple?
|
|
}
|
|
ext, err := createSubjectAltNameExtension(nil, nil, nil, nil, []x509util.SubjectAlternativeName{san}, true)
|
|
require.NoError(t, err)
|
|
template.ExtraExtensions = append(template.ExtraExtensions,
|
|
pkix.Extension{
|
|
Id: asn1.ObjectIdentifier(ext.ID),
|
|
Critical: ext.Critical,
|
|
Value: ext.Value,
|
|
},
|
|
)
|
|
}
|
|
akCert, err := aca.Sign(template)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, akCert)
|
|
|
|
// create a new key attested by the AK, while including
|
|
// the key authorization bytes as qualifying data.
|
|
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
|
config := tpm.AttestKeyConfig{
|
|
Algorithm: "RSA",
|
|
Size: 2048,
|
|
QualifyingData: keyAuthSum[:],
|
|
}
|
|
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, key)
|
|
require.Equal(t, "first-key", key.Name())
|
|
require.NotEqual(t, 0, len(key.Data()))
|
|
require.Equal(t, "first-ak", key.AttestedBy())
|
|
require.True(t, key.WasAttested())
|
|
require.True(t, key.WasAttestedBy(ak))
|
|
|
|
signer, err := key.Signer(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
// prepare the attestation object with the AK certificate chain,
|
|
// the attested key, its metadata and the signature signed by the
|
|
// AK.
|
|
params, err := key.CertificationParameters(context.Background())
|
|
require.NoError(t, err)
|
|
attObj, err := cbor.Marshal(struct {
|
|
Format string `json:"fmt"`
|
|
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
|
}{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// marshal the ACME payload
|
|
payload, err := json.Marshal(struct {
|
|
AttObj string `json:"attObj"`
|
|
}{
|
|
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return payload, signer, aca.Root
|
|
}
|
|
|
|
func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
|
|
type args struct {
|
|
ctx context.Context
|
|
ch *Challenge
|
|
db DB
|
|
jwk *jose.JSONWebKey
|
|
payload []byte
|
|
}
|
|
type test struct {
|
|
args args
|
|
wantErr *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"ok/doTPMAttestationFormat-storeError": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
payload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
|
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
|
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
|
|
|
// parse payload, set invalid "ver", remarshal
|
|
var p payloadType
|
|
err := json.Unmarshal(payload, &p)
|
|
require.NoError(t, err)
|
|
attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
|
|
require.NoError(t, err)
|
|
att := attestationObject{}
|
|
err = cbor.Unmarshal(attObj, &att)
|
|
require.NoError(t, err)
|
|
att.AttStatement["ver"] = "bogus"
|
|
attObj, err = cbor.Marshal(struct {
|
|
Format string `json:"fmt"`
|
|
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
|
}{
|
|
Format: "tpm",
|
|
AttStatement: att.AttStatement,
|
|
})
|
|
require.NoError(t, err)
|
|
payload, err = json.Marshal(struct {
|
|
AttObj string `json:"attObj"`
|
|
}{
|
|
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
|
})
|
|
require.NoError(t, err)
|
|
return test{
|
|
args: args{
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
Token: "token",
|
|
Type: "device-attest-01",
|
|
Status: StatusPending,
|
|
Value: "device.id.12345678",
|
|
},
|
|
payload: payload,
|
|
db: &MockDB{
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
|
assert.Equal(t, "azID", id)
|
|
return &Authorization{ID: "azID"}, nil
|
|
},
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusInvalid, updch.Status)
|
|
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
|
assert.Equal(t, "device.id.12345678", updch.Value)
|
|
|
|
err := NewError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)
|
|
|
|
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
|
assert.Equal(t, err.Type, updch.Error.Type)
|
|
assert.Equal(t, err.Detail, updch.Error.Detail)
|
|
assert.Equal(t, err.Status, updch.Error.Status)
|
|
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
|
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
}
|
|
},
|
|
"ok with invalid PermanentIdentifier SAN": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
payload, _, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
|
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
|
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
|
return test{
|
|
args: args{
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
Token: "token",
|
|
Type: "device-attest-01",
|
|
Status: StatusPending,
|
|
Value: "device.id.99999999",
|
|
},
|
|
payload: payload,
|
|
db: &MockDB{
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
|
assert.Equal(t, "azID", id)
|
|
return &Authorization{ID: "azID"}, nil
|
|
},
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusInvalid, updch.Status)
|
|
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
|
assert.Equal(t, "device.id.99999999", updch.Value)
|
|
|
|
err := NewError(ErrorRejectedIdentifierType, `permanent identifier does not match`).
|
|
AddSubproblems(NewSubproblemWithIdentifier(
|
|
ErrorMalformedType,
|
|
Identifier{Type: "permanent-identifier", Value: "device.id.99999999"},
|
|
`challenge identifier "device.id.99999999" doesn't match any of the attested hardware identifiers ["device.id.12345678"]`,
|
|
))
|
|
|
|
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
|
assert.Equal(t, err.Type, updch.Error.Type)
|
|
assert.Equal(t, err.Detail, updch.Error.Detail)
|
|
assert.Equal(t, err.Status, updch.Error.Status)
|
|
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
|
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
payload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
|
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
|
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
|
return test{
|
|
args: args{
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
Token: "token",
|
|
Type: "device-attest-01",
|
|
Status: StatusPending,
|
|
Value: "device.id.12345678",
|
|
},
|
|
payload: payload,
|
|
db: &MockDB{
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
|
assert.Equal(t, "azID", id)
|
|
return &Authorization{ID: "azID"}, nil
|
|
},
|
|
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
|
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "azID", az.ID)
|
|
assert.Equal(t, fingerprint, az.Fingerprint)
|
|
return nil
|
|
},
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
|
assert.Equal(t, "device.id.12345678", updch.Value)
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
}
|
|
},
|
|
"ok with PermanentIdentifier SAN": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
payload, signer, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
|
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
|
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
|
return test{
|
|
args: args{
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
Token: "token",
|
|
Type: "device-attest-01",
|
|
Status: StatusPending,
|
|
Value: "device.id.12345678",
|
|
},
|
|
payload: payload,
|
|
db: &MockDB{
|
|
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
|
assert.Equal(t, "azID", id)
|
|
return &Authorization{ID: "azID"}, nil
|
|
},
|
|
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
|
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "azID", az.ID)
|
|
assert.Equal(t, fingerprint, az.Fingerprint)
|
|
return nil
|
|
},
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
|
assert.Equal(t, "device.id.12345678", updch.Value)
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
|
|
if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {
|
|
assert.Error(t, tc.wantErr)
|
|
assert.EqualError(t, err, tc.wantErr.Error())
|
|
return
|
|
}
|
|
|
|
assert.Nil(t, tc.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func newBadAttestationStatementError(msg string) *Error {
|
|
return &Error{
|
|
Type: "urn:ietf:params:acme:error:badAttestationStatement",
|
|
Status: 400,
|
|
Err: errors.New(msg),
|
|
}
|
|
}
|
|
|
|
func newInternalServerError(msg string) *Error {
|
|
return &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Status: 500,
|
|
Err: errors.New(msg),
|
|
}
|
|
}
|
|
|
|
func Test_doTPMAttestationFormat(t *testing.T) {
|
|
ctx := context.Background()
|
|
aca, err := minica.New(
|
|
minica.WithName("TPM Testing"),
|
|
minica.WithGetSignerFunc(
|
|
func() (crypto.Signer, error) {
|
|
return keyutil.GenerateSigner("RSA", "", 2048)
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
acaRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: aca.Root.Raw})
|
|
|
|
// prepare simulated TPM and create an AK
|
|
stpm := newSimulatedTPM(t)
|
|
eks, err := stpm.GetEKs(context.Background())
|
|
require.NoError(t, err)
|
|
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ak)
|
|
|
|
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
|
ap, err := ak.AttestationParameters(context.Background())
|
|
require.NoError(t, err)
|
|
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
|
require.NoError(t, err)
|
|
|
|
// create template and sign certificate for the AK public key
|
|
keyID := generateKeyID(t, eks[0].Public())
|
|
template := &x509.Certificate{
|
|
PublicKey: akp.Public,
|
|
URIs: []*url.URL{
|
|
{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)},
|
|
},
|
|
}
|
|
akCert, err := aca.Sign(template)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, akCert)
|
|
|
|
templateMissingSAN := &x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "testakcertmissingsan",
|
|
},
|
|
PublicKey: akp.Public,
|
|
}
|
|
akCertMissingSAN, err := aca.Sign(templateMissingSAN)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, akCert)
|
|
|
|
// generate a JWK and the key authorization value
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
keyAuthorization, err := KeyAuthorization("token", jwk)
|
|
require.NoError(t, err)
|
|
|
|
// create a new key attested by the AK, while including
|
|
// the key authorization bytes as qualifying data.
|
|
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
|
config := tpm.AttestKeyConfig{
|
|
Algorithm: "RSA",
|
|
Size: 2048,
|
|
QualifyingData: keyAuthSum[:],
|
|
}
|
|
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, key)
|
|
params, err := key.CertificationParameters(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
signer, err := key.Signer(context.Background())
|
|
require.NoError(t, err)
|
|
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
|
require.NoError(t, err)
|
|
|
|
// attest another key and get its certification parameters
|
|
anotherKey, err := stpm.AttestKey(context.Background(), "first-ak", "another-key", config)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, key)
|
|
anotherKeyParams, err := anotherKey.CertificationParameters(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
prov Provisioner
|
|
ch *Challenge
|
|
jwk *jose.JSONWebKey
|
|
att *attestationObject
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *tpmAttestationData
|
|
expErr *Error
|
|
}{
|
|
{"ok", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), //
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, nil},
|
|
{"fail ver not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("ver not present")},
|
|
{"fail ver type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": []interface{}{},
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("ver not present")},
|
|
{"fail bogus ver", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "bogus",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError(`version "bogus" is not supported`)},
|
|
{"fail x5c not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c not present")},
|
|
{"fail x5c type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": [][]byte{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c not present")},
|
|
{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c is empty")},
|
|
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "step",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{"leaf", aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
|
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "step",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
|
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "step",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, "intermediate"},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
|
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "step",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
|
{"fail roots", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newInternalServerError("no root CA bundle available to verify the attestation certificate")},
|
|
{"fail verify", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "step",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("x5c is not valid: x509: certificate signed by unknown authority")},
|
|
{"fail missing SAN extension", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCertMissingSAN.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("AK certificate is missing Subject Alternative Name extension")},
|
|
{"fail pubArea not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
|
{"fail pubArea type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": []interface{}{},
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
|
{"fail pubArea empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": []byte{},
|
|
},
|
|
}}, nil, newBadAttestationStatementError("pubArea is empty")},
|
|
{"fail sig not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
|
{"fail sig type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": []interface{}{},
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
|
{"fail sig empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": []byte{},
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("sig is empty")},
|
|
{"fail certInfo not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
|
{"fail certInfo type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": []interface{}{},
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
|
{"fail certInfo empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": []byte{},
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("certInfo is empty")},
|
|
{"fail alg not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid alg in attestation statement")},
|
|
{"fail alg type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(0), // invalid alg
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid alg 0 in attestation statement")},
|
|
{"fail attestation verification", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": anotherKeyParams.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("invalid certification parameters: certification refers to a different key")},
|
|
{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), // RS256
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
|
|
{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
|
|
Format: "tpm",
|
|
AttStatement: map[string]interface{}{
|
|
"ver": "2.0",
|
|
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
|
"alg": int64(-257), //
|
|
"sig": params.CreateSignature,
|
|
"certInfo": params.CreateAttestation,
|
|
"pubArea": params.Public,
|
|
},
|
|
}}, nil, newBadAttestationStatementError("key authorization does not match")},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
|
|
if tt.expErr != nil {
|
|
var ae *Error
|
|
if assert.True(t, errors.As(err, &ae)) {
|
|
assert.EqualError(t, err, tt.expErr.Error())
|
|
assert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())
|
|
assert.Equal(t, ae.Type, tt.expErr.Type)
|
|
}
|
|
assert.Nil(t, got)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
if assert.NotNil(t, got) {
|
|
assert.Equal(t, akCert, got.Certificate)
|
|
assert.Equal(t, [][]*x509.Certificate{
|
|
{
|
|
akCert, aca.Intermediate, aca.Root,
|
|
},
|
|
}, got.VerifiedChains)
|
|
assert.Equal(t, fingerprint, got.Fingerprint)
|
|
assert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// createSubjectAltNameExtension will construct an Extension containing all
|
|
// SubjectAlternativeNames held in a Certificate. It implements more types than
|
|
// the golang x509 library, so it is used whenever OtherName or RegisteredID
|
|
// type SANs are present in the certificate.
|
|
//
|
|
// See also https://datatracker.ietf.org/doc/html/rfc5280.html#section-4.2.1.6
|
|
//
|
|
// TODO(hs): this was copied from go.step.sm/crypto/x509util. Should it be
|
|
// exposed instead?
|
|
func createSubjectAltNameExtension(dnsNames, emailAddresses x509util.MultiString, ipAddresses x509util.MultiIP, uris x509util.MultiURL, sans []x509util.SubjectAlternativeName, subjectIsEmpty bool) (x509util.Extension, error) {
|
|
var zero x509util.Extension
|
|
|
|
var rawValues []asn1.RawValue
|
|
for _, dnsName := range dnsNames {
|
|
rawValue, err := x509util.SubjectAlternativeName{
|
|
Type: x509util.DNSType, Value: dnsName,
|
|
}.RawValue()
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
rawValues = append(rawValues, rawValue)
|
|
}
|
|
|
|
for _, emailAddress := range emailAddresses {
|
|
rawValue, err := x509util.SubjectAlternativeName{
|
|
Type: x509util.EmailType, Value: emailAddress,
|
|
}.RawValue()
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
rawValues = append(rawValues, rawValue)
|
|
}
|
|
|
|
for _, ip := range ipAddresses {
|
|
rawValue, err := x509util.SubjectAlternativeName{
|
|
Type: x509util.IPType, Value: ip.String(),
|
|
}.RawValue()
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
rawValues = append(rawValues, rawValue)
|
|
}
|
|
|
|
for _, uri := range uris {
|
|
rawValue, err := x509util.SubjectAlternativeName{
|
|
Type: x509util.URIType, Value: uri.String(),
|
|
}.RawValue()
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
rawValues = append(rawValues, rawValue)
|
|
}
|
|
|
|
for _, san := range sans {
|
|
rawValue, err := san.RawValue()
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
rawValues = append(rawValues, rawValue)
|
|
}
|
|
|
|
// Now marshal the rawValues into the ASN1 sequence, and create an Extension object to hold the extension
|
|
rawBytes, err := asn1.Marshal(rawValues)
|
|
if err != nil {
|
|
return zero, fmt.Errorf("error marshaling SubjectAlternativeName extension to ASN1: %w", err)
|
|
}
|
|
|
|
return x509util.Extension{
|
|
ID: x509util.ObjectIdentifier(oidSubjectAlternativeName),
|
|
Critical: subjectIsEmpty,
|
|
Value: rawBytes,
|
|
}, nil
|
|
}
|