Skip to content
Open
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.20260803172805-97f0ab48a939
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.20260803172805-97f0ab48a939 h1:wRNnWGUVQpyYnLr4s9Q2O+3l3sVeocCMuqYcPdZweGQ=
github.com/smallstep/go-attestation v0.4.4-0.20260803172805-97f0ab48a939/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
21 changes: 19 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,23 @@ 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. A caller
// that supplies a nonce is proving possession to something that chose that
// nonce — an ACME device-attest-01 challenge, say — and the stored
// statement carries whatever nonce the key was created with, so it only
// ever satisfies the first such challenge. Re-certifying lets one
// persisted key answer every subsequent one.
//
// 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, 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
58 changes: 58 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,59 @@ func (k *Key) CertificationParameters(ctx context.Context) (params attest.Certif
return
}

// Recertify runs a fresh TPM2_Certify over the key using the AK that attested
// it, binding qualifyingData as the nonce, and returns the resulting
// parameters. The key itself is untouched: only a new signed statement about
// it is produced.
//
// It exists for protocols that bind a per-transaction challenge into the
// certification. ACME device-attest-01 derives the expected nonce from the
// order's keyAuthorization, so proving possession for a second order against
// the parameters from [Key.CertificationParameters] fails — those carry the
// first order's nonce. Without re-certification the only way to satisfy a new
// order is a new key, which rotates the credential and invalidates anything
// registered against its public key.
//
// The key must have been attested by an AK ([Key.WasAttested]), and the TPM
// must be a 2.0 device.
func (k *Key) Recertify(ctx context.Context, qualifyingData []byte) (params attest.CertificationParameters, err error) {
if !k.WasAttested() {
return params, fmt.Errorf("key %q was not attested", k.name)
}

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)

ak, 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)
}

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

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

p, err := loadedKey.Recertify(loadedAK, qualifyingData)
if err != nil {
return params, fmt.Errorf("failed recertifying key %q: %w", k.name, err)
}

return *p, nil
}

// 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
118 changes: 118 additions & 0 deletions tpm/recertify_simulator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//go:build tpmsimulator

package tpm

import (
"context"
"crypto"
"crypto/rsa"
"testing"

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

// 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 a nonce chosen after the key
// was created. Without it, binding a new nonce 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)

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

verifyOpts := akVerifyOpts(t, ak)

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

// Re-certifying binds a different nonce to the same key.
secondNonce := []byte("second-order-key-authorization")
fresh, err := key.Recertify(ctx, secondNonce)
require.NoError(t, err)

assert.Equal(t, secondNonce, 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, firstNonce, 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)
}

// 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, []byte("nonce"))
assert.EqualError(t, err, `key "unattested" was not attested`)
}