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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/peterbourgon/diskv/v3 v3.0.1
github.com/pkg/errors v0.9.1
github.com/schollz/jsonstore v1.1.0
github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9
github.com/smallstep/go-attestation v0.4.4-0.20260814222900-a849f4e2cd68
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.54.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -764,8 +764,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9 h1:n+X1wnMKJMcCRd98YKAo/56tMRSPUg+qjAvNNS1EZeM=
github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/go-attestation v0.4.4-0.20260814222900-a849f4e2cd68 h1:KcK2guFXrE5sX/nvF1b+atHP6DjRB1gCv3ppyTXB2Zk=
github.com/smallstep/go-attestation v0.4.4-0.20260814222900-a849f4e2cd68/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
Expand Down
17 changes: 15 additions & 2 deletions kms/tpmkms/tpmkms.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"strings"
"time"

"github.com/smallstep/go-attestation/attest"

"go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/uri"
"go.step.sm/crypto/tpm"
Expand Down Expand Up @@ -1650,8 +1652,19 @@ func (k *TPMKMS) CreateAttestation(req *apiv1.CreateAttestationRequest) (*apiv1.
return nil, fmt.Errorf("failed getting signer for key %q: %w", properties.name, err)
}

params, err := key.CertificationParameters(ctx)
if err != nil {
// When the request carries qualifying data, certify the key again against
// it rather than returning the statement recorded at creation. Re-certifying
// lets one persisted key answer every subsequent certification attempt.
//
// With no qualifying data there is nothing to bind, so the stored
// statement is returned unchanged and existing callers are unaffected.
var params attest.CertificationParameters
if len(properties.qualifyingData) > 0 {
params, err = key.Recertify(ctx, tpm.RecertifyConfig{QualifyingData: properties.qualifyingData})
if err != nil {
return nil, fmt.Errorf("failed recertifying key %q: %w", key.Name(), err)
}
} else if params, err = key.CertificationParameters(ctx); err != nil {
return nil, fmt.Errorf("failed getting key certification parameters for %q: %w", key.Name(), err)
}

Expand Down
67 changes: 67 additions & 0 deletions tpm/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ func (k *Key) Signer(ctx context.Context) (crypto.Signer, error) {

// CertificationParameters returns information about the key that can be used to
// verify key certification.
//
// The parameters are the ones recorded when the key was created: the
// TPM2_Certify [TPM.AttestKey] performs, with that call's
// [AttestKeyConfig.QualifyingData] frozen in as the nonce. Use
// [Key.Recertify] to obtain parameters over a different nonce.
func (k *Key) CertificationParameters(ctx context.Context) (params attest.CertificationParameters, err error) {
if err = k.tpm.open(ctx, openOptions{machineKey: k.machineKey}); err != nil {
return params, fmt.Errorf("failed opening TPM: %w", err)
Expand All @@ -478,6 +483,68 @@ func (k *Key) CertificationParameters(ctx context.Context) (params attest.Certif
return
}

// RecertifyConfig encapsulates parameters used for re-certifying keys.
type RecertifyConfig struct {
QualifyingData []byte
}

// Recertify runs a fresh TPM2_Certify over the key using the AK that attested
// it, binding qualifyingData to the key and returns the resulting parameters.
// The key itself is untouched: only a new signed statement about it is produced.
//
// The key must have been attested by an AK, and it is only supported on TPM 2.0
// devices.
func (k *Key) Recertify(ctx context.Context, config RecertifyConfig) (params attest.CertificationParameters, err error) {
if !k.WasAttested() {
return params, fmt.Errorf("key %q was not attested", k.name)
}

if len(config.QualifyingData) == 0 {
return params, errors.New("qualifying data required")
}

if err = k.tpm.open(ctx, openOptions{machineKey: k.machineKey}); err != nil {
return params, fmt.Errorf("failed opening TPM: %w", err)
}
defer closeTPM(ctx, k.tpm, &err)

key, err := k.tpm.attestTPM.LoadKey(k.data)
if err != nil {
return params, fmt.Errorf("failed loading key %q: %w", k.name, err)
}
defer key.Close()

storedAK, err := k.tpm.store.GetAK(k.attestedBy)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return params, fmt.Errorf("failed getting AK %q: %w", k.attestedBy, ErrNotFound)
}
return params, fmt.Errorf("failed getting AK %q: %w", k.attestedBy, err)
}

ak, err := k.tpm.attestTPM.LoadAK(storedAK.Data)
if err != nil {
return params, fmt.Errorf("failed loading AK %q: %w", k.attestedBy, err)
}
defer ak.Close(k.tpm.attestTPM)

p, err := ak.Recertify(k.tpm.attestTPM, key, &attest.RecertifyConfig{
QualifyingData: config.QualifyingData,
})
if err != nil {
return params, fmt.Errorf("failed recertifying key %q: %w", k.name, err)
}

// NOTE: new certification parameters are not persisted. This means
// recertification doesn't change the representation of the current
// key, so calling [Key.CertificationParameters] will always return
// the data from creation time.

params = *p

return
}

// Blobs returns a container for the private and public key blobs.
// The resulting blobs are compatible with tpm2-tools, so can be used
// like this (after having been written to key.priv and key.pub):
Expand Down
114 changes: 113 additions & 1 deletion tpm/tpm_simulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import (
"strings"
"testing"

"github.com/smallstep/go-attestation/attest"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smallstep/go-attestation/attest"

"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/tpm/algorithm"
Expand Down Expand Up @@ -1168,3 +1170,113 @@ func TestTPMOnlyFailsWithoutStorageWhenRequired(t *testing.T) {
_, err = tpm.GetAKByPermanentIdentifier(ctx, "permanent-identifier")
require.ErrorIs(t, err, ErrNoStorageConfigured)
}

// akVerifyOpts builds the options needed to verify a certification signed by
// ak, mirroring what a relying party does with the AK's public key.
func akVerifyOpts(t *testing.T, ak *AK) attest.VerifyOpts {
t.Helper()

params, err := ak.AttestationParameters(context.Background())
require.NoError(t, err)

pub, err := tpm2.DecodePublic(params.Public)
require.NoError(t, err)

hash, err := pub.RSAParameters.Sign.Hash.Hash()
require.NoError(t, err)

return attest.VerifyOpts{
Public: &rsa.PublicKey{
E: int(pub.RSAParameters.Exponent()),
N: pub.RSAParameters.Modulus(),
},
Hash: hash,
}
}

// extraData returns the qualifying data bound into a certification.
func extraData(t *testing.T, params attest.CertificationParameters) []byte {
t.Helper()

att, err := tpm2.DecodeAttestationData(params.CreateAttestation)
require.NoError(t, err)
require.Equal(t, tpm2.TagAttestCertify, att.Type)

return att.ExtraData
}

// TestKey_Recertify covers the property the change exists for: one persisted
// key can produce a valid certification against qualifying data chosen after
// the key was created. Without it, binding new qualifying data requires a new
// key.
func TestKey_Recertify(t *testing.T) {
ctx := context.Background()
tpm := newSimulatedTPM(t)

ak, err := tpm.CreateAK(ctx, "ak")
require.NoError(t, err)

firstQualifyingData := []byte("first-qualifying-data")
key, err := tpm.AttestKey(ctx, "ak", "key", AttestKeyConfig{
Algorithm: "RSA",
Size: 2048,
QualifyingData: firstQualifyingData,
})
require.NoError(t, err)

verifyOpts := akVerifyOpts(t, ak)

// The stored certification carries the qualifying data the key
// was created with.
stored, err := key.CertificationParameters(ctx)
require.NoError(t, err)
require.NoError(t, stored.Verify(verifyOpts))
assert.Equal(t, firstQualifyingData, extraData(t, stored))

// Re-certifying binds different qualifying data to the same key.
secondQualifyingData := []byte("second-qualifying-data")
fresh, err := key.Recertify(ctx, RecertifyConfig{QualifyingData: secondQualifyingData})
require.NoError(t, err)

assert.Equal(t, secondQualifyingData, extraData(t, fresh))
assert.NotEqual(t, stored.CreateAttestation, fresh.CreateAttestation)
assert.NotEqual(t, stored.CreateSignature, fresh.CreateSignature)

// It is still the same key, and the fresh statement satisfies every check
// a relying party makes — this is what lets the credential persist.
assert.Equal(t, stored.Public, fresh.Public)
require.NoError(t, fresh.Verify(verifyOpts))

// Re-certifying does not disturb the stored certification.
reread, err := key.CertificationParameters(ctx)
require.NoError(t, err)
assert.Equal(t, firstQualifyingData, extraData(t, reread))

// The key remains usable for signing.
signer, err := key.Signer(ctx)
require.NoError(t, err)
digest := []byte("01234567890123456789012345678901")
_, err = signer.Sign(nil, digest, crypto.SHA256)
require.NoError(t, err)

// Empty qualifying data is not supported, and returns an error
empty, err := key.Recertify(ctx, RecertifyConfig{QualifyingData: nil})
assert.Error(t, err)
assert.Empty(t, empty)
}

// TestKey_Recertify_notAttested guards the precondition: a key with no AK has
// nothing to certify it, and must say so rather than fail obscurely.
func TestKey_Recertify_notAttested(t *testing.T) {
ctx := context.Background()
tpm := newSimulatedTPM(t)

key, err := tpm.CreateKey(ctx, "unattested", CreateKeyConfig{
Algorithm: "RSA",
Size: 2048,
})
require.NoError(t, err)

_, err = key.Recertify(ctx, RecertifyConfig{QualifyingData: []byte("qualifying-data")})
assert.EqualError(t, err, `key "unattested" was not attested`)
}