start TPM support

This commit is contained in:
pascal
2026-09-16 23:00:44 +02:00
parent d89e9c86eb
commit 24f5f2a7ec
22 changed files with 1836 additions and 145 deletions
+85
View File
@@ -17,6 +17,8 @@ to one WireGuard peer key and cannot be replayed by another peer.
| Windows | `LocalMachine\MY` | the service, directly |
| Windows | signed-in user's `CurrentUser\MY` | a helper launched with that session's token |
| Linux and others | PEM directory, `NB_CERT_STORE_DIR` or `/etc/netbird/certs` | the daemon, directly |
| Linux | a `TSS2 PRIVATE KEY` file in that directory, signed by the TPM | the daemon, through `/dev/tpmrm0` |
| Linux | a PKCS#11 token named by `NB_CERT_PKCS11_URI`, such as tpm2-pkcs11 | the daemon, through the token's module, in builds with the `pkcs11` tag |
macOS and Windows both keep per-user certificates out of reach of a privileged daemon,
and both are handled the same way: the daemon reads the machine store itself and
@@ -99,6 +101,89 @@ Unlike macOS, the Windows store acquires keys with `CRYPT_ACQUIRE_SILENT_FLAG`,
that would need a prompt fails immediately instead of blocking. That also means a
smartcard PIN can never be satisfied this way.
## Linux: keys held by the TPM
Enrollment tooling on Linux keeps a TPM-resident key as a `TSS2 PRIVATE KEY` PEM file,
the format of draft-bottomley-tpm2-keys that tpm2-openssl, tpm2-tss-engine and
`tpm2_encodeobject` write. The file holds the key wrapped by its parent; the TPM is the
only thing that can use it. Drop it next to the certificate as usual:
```
openssl genpkey -provider tpm2 -algorithm EC -pkeyopt group:P-256 -out /etc/netbird/certs/device.key
openssl req -provider tpm2 -provider default -new -key /etc/netbird/certs/device.key -subj /CN=device -out device.csr
```
Sign the CSR with the organisation CA and store the result as `device.pem`. The store
parses the key file without touching the TPM, so the certificate is listed as a
candidate like any other, and every signature opens `/dev/tpmrm0`, loads the key under
its parent, signs, flushes and closes again. `NB_TPM_DEVICE` overrides the device path.
What the key file may look like:
- **Parent.** A persistent handle such as `0x81000001` is used as is. The owner
hierarchy, which both tpm2-openssl and tpm2-tss-engine default to, means the key was
created under a transient primary from the TCG default ECC P-256 template, and that
same primary is derived again before loading.
- **No authorization value.** A key created with a password needs someone to type it,
which the daemon cannot arrange, so the certificate is skipped with a log line rather
than blocking on a TPM auth failure.
- **RSA-2048 or P-256, sometimes P-384.** Those are what the PC Client profile requires
of a TPM; P-384 depends on the chip. The TPM chooses the RSA-PSS salt itself, which is
why management verifies PSS proofs with `rsa.PSSSaltLengthAuto`.
Windows needs none of this: a certificate enrolled into the TPM sits behind the Microsoft
Platform Crypto Provider and the CNG path above signs with it unchanged. macOS has no
TPM; its Secure Enclave keys are reachable only through the keychain path.
To exercise the path without hardware, run a software TPM and point the end-to-end test
at it:
```
swtpm socket --tpm2 --server type=unixio,path=/tmp/swtpm.sock --ctrl type=unixio,path=/tmp/swtpm.ctrl --flags not-need-init,startup-clear
NB_TPM_DEVICE=/tmp/swtpm.sock go test ./client/internal/certproof/ -run TestCollect_TPMKeyEndToEnd -v
```
## Linux: keys behind a PKCS#11 token
Distributions that follow Red Hat's guidance reach the TPM through tpm2-pkcs11, a PKCS#11
module whose token holds both the key and, after `tpm2_ptool addcert`, the certificate.
The store reads that token when `NB_CERT_PKCS11_URI` names it with an RFC 7512 URI:
```
NB_CERT_PKCS11_URI='pkcs11:token=netbird?module-path=/usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so&pin-source=file:/etc/netbird/pkcs11.pin'
```
`token` selects the token by label, or the first token present when absent. `module-path`
names the library to load; `module-name=tpm2_pkcs11` resolves to `libtpm2_pkcs11.so` on
the loader's search path, and with neither the p11-kit proxy is loaded, which exposes every
module the system has registered. `pin-source` points at a file holding the user PIN and
`pin-value` carries it inline; without either no login happens, and tpm2-pkcs11 then shows
no private keys at all. Every other attribute is ignored.
Certificates and private keys are paired by `CKA_ID`, which is what `tpm2_ptool addcert`
and `pkcs11-tool` set. Chains are completed from the other certificates on the token. Each
operation opens a session, logs in, works, logs out and closes, so no token handle
outlives a call, and the PEM directory keeps working when the token does not: the two are
queried together and a failing token is logged rather than hiding file certificates.
Two consequences of the PIN are worth knowing. It is a secret on disk, so the PIN file
should be root-only. And a wrong PIN counts against the TPM's dictionary-attack lockout,
which is shared with everything else on the machine that uses the TPM.
The module is loaded at runtime without cgo, through `purego`, which means the binary is
dynamically linked against libc. The standard release binary stays fully static, so the
PKCS#11 store is compiled in only with `-tags pkcs11` on linux/amd64 and linux/arm64.
Without the tag, setting `NB_CERT_PKCS11_URI` logs that the build lacks the support.
To exercise the path without hardware, initialise a SoftHSM token and run the end-to-end
test, which imports a key and certificate itself:
```
softhsm2-util --init-token --free --label netbird --pin 1234 --so-pin 1234
NB_TEST_PKCS11_URI='pkcs11:token=netbird?module-path=/usr/lib/softhsm/libsofthsm2.so&pin-value=1234' \
go test -tags pkcs11 ./client/internal/certproof/ -run PKCS11 -v
```
## Only the signed-in user can be validated
This is the central limitation of the design, and it is deliberate.
+182
View File
@@ -0,0 +1,182 @@
package certproof
import (
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"io"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/pkcs11"
)
const PKCS11URIEnv = "NB_CERT_PKCS11_URI"
// PKCS11Store yields the identities of a PKCS#11 token, which is how tpm2-pkcs11 exposes
// TPM-held keys on Linux. Certificates and keys are paired by CKA_ID, the convention
// tpm2_ptool addcert and pkcs11-tool follow, and every signature happens on the token.
type PKCS11Store struct {
uri *pkcs11.URI
}
// NewPKCS11Store reads the token, module and PIN source from an RFC 7512 PKCS#11 URI.
func NewPKCS11Store(uri string) (*PKCS11Store, error) {
parsed, err := pkcs11.ParseURI(uri)
if err != nil {
return nil, err
}
return &PKCS11Store{uri: parsed}, nil
}
func (s *PKCS11Store) Candidates(_ context.Context) ([]Candidate, error) {
session, err := s.open()
if err != nil {
return nil, err
}
defer session.Close()
certs, err := tokenCertificates(session)
if err != nil {
return nil, err
}
log.Infof("%s holds %d certificates", s, len(certs))
pool := make([]*x509.Certificate, 0, len(certs))
for _, cert := range certs {
pool = append(pool, cert.cert)
}
var candidates []Candidate
for _, cert := range certs {
if _, err := privateKey(session, cert.id); err != nil {
log.Infof("%s certificate %q has no usable private key: %v", s, cert.cert.Subject, err)
continue
}
chain := buildChain(cert.cert, pool)
log.Infof("%s candidate %q issued by %q built a chain of %d certificates", s, cert.cert.Subject, cert.cert.Issuer, len(chain))
candidates = append(candidates, Candidate{Chain: chain, Signer: &pkcs11Signer{store: s, leaf: cert.cert, id: cert.id}})
}
return candidates, nil
}
func (s *PKCS11Store) String() string {
if s.uri.Token == "" {
return "PKCS#11 token"
}
return fmt.Sprintf("PKCS#11 token %q", s.uri.Token)
}
func (s *PKCS11Store) open() (*pkcs11.Session, error) {
module, err := pkcs11.Load(s.uri.Module())
if err != nil {
return nil, err
}
pin, err := s.uri.PIN()
if err != nil {
return nil, err
}
return module.OpenSession(s.uri.Token, pin)
}
type tokenCertificate struct {
cert *x509.Certificate
id []byte
}
func tokenCertificates(session *pkcs11.Session) ([]tokenCertificate, error) {
objects, err := session.FindObjects(
pkcs11.Attribute{Type: pkcs11.AttrClass, Value: pkcs11.ULong(pkcs11.ClassCertificate)},
pkcs11.Attribute{Type: pkcs11.AttrCertificateType, Value: pkcs11.ULong(pkcs11.CertificateX509)},
)
if err != nil {
return nil, err
}
certs := make([]tokenCertificate, 0, len(objects))
for _, object := range objects {
der, err := session.Attribute(object, pkcs11.AttrValue)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
log.Warnf("skipping unparsable certificate on PKCS#11 token: %v", err)
continue
}
id, err := session.Attribute(object, pkcs11.AttrID)
if err != nil {
return nil, err
}
certs = append(certs, tokenCertificate{cert: cert, id: id})
}
return certs, nil
}
var errNoPrivateKey = errors.New("no private key shares the certificate's CKA_ID")
func privateKey(session *pkcs11.Session, id []byte) (pkcs11.Object, error) {
if len(id) == 0 {
return 0, errNoPrivateKey
}
keys, err := session.FindObjects(
pkcs11.Attribute{Type: pkcs11.AttrClass, Value: pkcs11.ULong(pkcs11.ClassPrivateKey)},
pkcs11.Attribute{Type: pkcs11.AttrID, Value: id},
)
if err != nil {
return 0, err
}
if len(keys) == 0 {
return 0, errNoPrivateKey
}
return keys[0], nil
}
// pkcs11Signer holds only the certificate and its CKA_ID; the key is looked up in a fresh
// session at signing time so no token handle outlives a call.
type pkcs11Signer struct {
store *PKCS11Store
leaf *x509.Certificate
id []byte
}
func (s *pkcs11Signer) Public() crypto.PublicKey {
return s.leaf.PublicKey
}
func (s *pkcs11Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
scheme, err := schemeFor(s.leaf.PublicKey, opts)
if err != nil {
return nil, err
}
session, err := s.store.open()
if err != nil {
return nil, err
}
defer session.Close()
key, err := privateKey(session, s.id)
if err != nil {
return nil, err
}
signature, err := session.Sign(pkcs11Mechanism(scheme), key, digest)
if err != nil {
return nil, err
}
if scheme == schemeRSAPSSSHA256 {
return signature, nil
}
return ecdsaSignatureASN1(signature)
}
// pkcs11Mechanism maps a signature scheme onto the token mechanism that consumes a digest.
func pkcs11Mechanism(scheme sigScheme) pkcs11.Mechanism {
if scheme == schemeRSAPSSSHA256 {
return pkcs11.Mechanism{
Type: pkcs11.MechRSAPKCSPSS,
PSS: &pkcs11.PSSParams{Hash: pkcs11.MechSHA256, MGF: pkcs11.MGF1SHA256, SaltLen: sha256.Size},
}
}
return pkcs11.Mechanism{Type: pkcs11.MechECDSA}
}
@@ -0,0 +1,189 @@
package certproof
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"errors"
"math/big"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/client/internal/pkcs11"
"github.com/netbirdio/netbird/shared/management/certposture"
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
"github.com/netbirdio/netbird/shared/management/proto"
)
const testPKCS11URIEnv = "NB_TEST_PKCS11_URI"
type failingStore struct{}
func (failingStore) Candidates(context.Context) ([]Candidate, error) {
return nil, errors.New("token unplugged")
}
func TestStores_KeepsFileCertificatesWhenTokenFails(t *testing.T) {
ca := certtest.NewCA(t, "corp")
key := certtest.ECDSAKey(t)
dir := t.TempDir()
writeFile(t, dir, "device.pem", certtest.CertPEM(ca.Issue(t, key, "device"))+certtest.KeyPEM(t, key))
candidates, err := Stores{failingStore{}, NewFileStore(dir)}.Candidates(context.Background())
require.NoError(t, err)
assert.Len(t, candidates, 1, "the directory's certificate must survive a failing token")
}
// TestCollect_PKCS11TokenEndToEnd needs an initialised token with a user PIN, named by
// NB_TEST_PKCS11_URI. With SoftHSM:
//
// softhsm2-util --init-token --free --label netbird --pin 1234 --so-pin 1234
// NB_TEST_PKCS11_URI='pkcs11:token=netbird?module-path=/usr/lib/softhsm/libsofthsm2.so&pin-value=1234' \
// go test -tags pkcs11 ./client/internal/certproof/ -run PKCS11 -v
//
// It imports a key and its certificate as token objects, then proves the certificate
// through the store the way the daemon would. Every run adds one more identity to the token.
func TestCollect_PKCS11TokenEndToEnd(t *testing.T) {
uri := os.Getenv(testPKCS11URIEnv)
if uri == "" {
t.Skipf("set %s to a PKCS#11 URI with a PIN to run", testPKCS11URIEnv)
}
store, err := NewPKCS11Store(uri)
require.NoError(t, err)
if _, err := pkcs11.Load(store.uri.Module()); errors.Is(err, pkcs11.ErrUnsupported) {
t.Skip(err)
}
keys := map[string]crypto.Signer{"ecdsa": certtest.ECDSAKey(t), "rsa": certtest.RSAKey(t)}
for name, key := range keys {
t.Run(name, func(t *testing.T) {
ca := certtest.NewCA(t, "corp-"+name)
leaf := ca.Issue(t, key, "device-"+name)
importIdentity(t, uri, key, leaf)
challenger := certposture.NewChallenger([]byte("secret"))
now := time.Now()
nonce := challenger.Nonce(peerKey, now)
checks := []*proto.Checks{{CertificateChallenge: &proto.CertificateChallenge{Nonce: nonce, CaCertificates: []string{ca.PEM}}}}
proofs := Collect(context.Background(), store, checks, peerKey)
require.Len(t, proofs, 1, "the token-held key must prove exactly this run's certificate")
chain, err := challenger.Verify(proofs[0], peerKey, now)
require.NoError(t, err)
assert.True(t, leaf.Equal(chain[0]), "proof must carry the imported certificate")
})
}
}
// Attribute and key types the import needs and the store does not.
const (
attrPrivate = 0x2
attrIssuer = 0x81
attrSerialNumber = 0x82
attrKeyType = 0x100
attrSensitive = 0x103
attrSign = 0x108
attrModulus = 0x120
attrPublicExponent = 0x122
attrPrivateExponent = 0x123
attrPrime1 = 0x124
attrPrime2 = 0x125
attrExponent1 = 0x126
attrExponent2 = 0x127
attrCoefficient = 0x128
attrECParams = 0x180
keyTypeRSA = 0x0
keyTypeEC = 0x3
)
var (
ckTrue = []byte{1}
ckFalse = []byte{0}
// The P-256 named curve OID in DER, which is what CKA_EC_PARAMS carries.
oidP256 = []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}
)
func importIdentity(t *testing.T, uri string, key crypto.Signer, leaf *x509.Certificate) {
t.Helper()
parsed, err := pkcs11.ParseURI(uri)
require.NoError(t, err)
module, err := pkcs11.Load(parsed.Module())
require.NoError(t, err)
pin, err := parsed.PIN()
require.NoError(t, err)
session, err := module.OpenReadWriteSession(parsed.Token, pin)
require.NoError(t, err)
defer session.Close()
id := make([]byte, 8)
_, err = rand.Read(id)
require.NoError(t, err)
label := []byte(leaf.Subject.CommonName)
serial, err := asn1.Marshal(leaf.SerialNumber)
require.NoError(t, err)
_, err = session.CreateObject(
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassCertificate)),
attr(pkcs11.AttrCertificateType, pkcs11.ULong(pkcs11.CertificateX509)),
attr(pkcs11.AttrToken, ckTrue),
attr(attrPrivate, ckFalse),
attr(pkcs11.AttrLabel, label),
attr(pkcs11.AttrID, id),
attr(pkcs11.AttrSubject, leaf.RawSubject),
attr(attrIssuer, leaf.RawIssuer),
attr(attrSerialNumber, serial),
attr(pkcs11.AttrValue, leaf.Raw),
)
require.NoError(t, err, "import certificate")
template := []pkcs11.Attribute{
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassPrivateKey)),
attr(pkcs11.AttrToken, ckTrue),
attr(attrPrivate, ckTrue),
attr(attrSensitive, ckTrue),
attr(attrSign, ckTrue),
attr(pkcs11.AttrLabel, label),
attr(pkcs11.AttrID, id),
}
_, err = session.CreateObject(append(template, keyAttributes(t, key)...)...)
require.NoError(t, err, "import private key")
}
func keyAttributes(t *testing.T, key crypto.Signer) []pkcs11.Attribute {
t.Helper()
switch k := key.(type) {
case *ecdsa.PrivateKey:
return []pkcs11.Attribute{
attr(attrKeyType, pkcs11.ULong(keyTypeEC)),
attr(attrECParams, oidP256),
attr(pkcs11.AttrValue, k.D.FillBytes(make([]byte, 32))),
}
case *rsa.PrivateKey:
k.Precompute()
return []pkcs11.Attribute{
attr(attrKeyType, pkcs11.ULong(keyTypeRSA)),
attr(attrModulus, k.N.Bytes()),
attr(attrPublicExponent, big.NewInt(int64(k.E)).Bytes()),
attr(attrPrivateExponent, k.D.Bytes()),
attr(attrPrime1, k.Primes[0].Bytes()),
attr(attrPrime2, k.Primes[1].Bytes()),
attr(attrExponent1, k.Precomputed.Dp.Bytes()),
attr(attrExponent2, k.Precomputed.Dq.Bytes()),
attr(attrCoefficient, k.Precomputed.Qinv.Bytes()),
}
}
t.Fatalf("unsupported key %T", key)
return nil
}
func attr(typ uint, value []byte) pkcs11.Attribute {
return pkcs11.Attribute{Type: typ, Value: value}
}
+25 -2
View File
@@ -12,6 +12,8 @@ import (
"strings"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/tpm"
)
const (
@@ -32,7 +34,8 @@ type Store interface {
}
// FileStore reads PEM files from a directory. A file holds the chain (leaf first) and
// either its private key or a sibling "<name>.key" file holds it.
// either its private key or a sibling "<name>.key" file holds it. The key is a plain
// PKCS#8, EC or RSA key, or a TSS2 key the TPM signs with.
type FileStore struct {
dir string
}
@@ -116,7 +119,7 @@ func parsePEM(data []byte) ([]*x509.Certificate, crypto.Signer, error) {
return nil, nil, fmt.Errorf("parse certificate: %w", err)
}
chain = append(chain, cert)
case "PRIVATE KEY", "EC PRIVATE KEY", "RSA PRIVATE KEY":
case "PRIVATE KEY", "EC PRIVATE KEY", "RSA PRIVATE KEY", tpm.KeyPEMType:
key, err := parsePrivateKey(block)
if err != nil {
return nil, nil, err
@@ -130,6 +133,8 @@ func parsePrivateKey(block *pem.Block) (crypto.Signer, error) {
var key any
var err error
switch block.Type {
case tpm.KeyPEMType:
return tpm.ParseKey(block.Bytes)
case "EC PRIVATE KEY":
key, err = x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
@@ -154,3 +159,21 @@ func isCertFile(name string) bool {
}
return false
}
// Stores queries several stores and carries on when one fails, so a broken token cannot
// hide the certificates a directory holds. A failure is logged instead of returned
// because Collect treats a store error as "no proofs at all".
type Stores []Store
func (s Stores) Candidates(ctx context.Context) ([]Candidate, error) {
var all []Candidate
for _, store := range s {
candidates, err := store.Candidates(ctx)
if err != nil {
log.Warnf("certificate store %T unavailable: %v", store, err)
continue
}
all = append(all, candidates...)
}
return all, nil
}
+19 -2
View File
@@ -2,7 +2,24 @@
package certproof
// DefaultStore is the PEM directory named by NB_CERT_STORE_DIR, or /etc/netbird/certs.
import (
"os"
log "github.com/sirupsen/logrus"
)
// DefaultStore is the PEM directory named by NB_CERT_STORE_DIR, or /etc/netbird/certs,
// joined by the PKCS#11 token named by NB_CERT_PKCS11_URI when that is set.
func DefaultStore() Store {
return NewFileStore(StoreDir())
files := NewFileStore(StoreDir())
uri := os.Getenv(PKCS11URIEnv)
if uri == "" {
return files
}
token, err := NewPKCS11Store(uri)
if err != nil {
log.Warnf("ignoring %s: %v", PKCS11URIEnv, err)
return files
}
return Stores{files, token}
}
@@ -0,0 +1,98 @@
package certproof
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/pem"
"os"
"testing"
"time"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/tpm/tss2"
"github.com/netbirdio/netbird/client/internal/tpm"
"github.com/netbirdio/netbird/client/internal/tpm/tpmtest"
"github.com/netbirdio/netbird/shared/management/certposture"
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
"github.com/netbirdio/netbird/shared/management/proto"
)
func TestFileStore_TPMKeyFile(t *testing.T) {
ca := certtest.NewCA(t, "corp")
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
leaf := ca.Issue(t, key, "device")
dir := t.TempDir()
writeFile(t, dir, "device.pem", certtest.CertPEM(leaf))
writeFile(t, dir, "device.key", tpmtest.KeyPEM(t, &key.PublicKey))
// A key that needs a password can never be used silently, so its certificate is skipped.
locked := certtest.ECDSAKey(t)
withAuth := func(k *tss2.TPMKey) { k.EmptyAuth = false }
writeFile(t, dir, "locked.pem", certtest.CertPEM(ca.Issue(t, locked, "locked")))
writeFile(t, dir, "locked.key", tpmtest.KeyPEM(t, locked.Public().(*ecdsa.PublicKey), withAuth))
candidates, err := NewFileStore(dir).Candidates(context.Background())
require.NoError(t, err)
require.Len(t, candidates, 1, "only the key without an authorization value is usable")
assert.True(t, leaf.Equal(candidates[0].Chain[0]), "candidate must carry the device certificate")
assert.True(t, key.PublicKey.Equal(candidates[0].Signer.Public()), "signer must report the certificate's key")
}
// TestCollect_TPMKeyEndToEnd runs against the TPM named by NB_TPM_DEVICE, for example a
// swtpm started with:
//
// swtpm socket --tpm2 --server type=unixio,path=/tmp/swtpm.sock \
// --ctrl type=unixio,path=/tmp/swtpm.ctrl --flags not-need-init,startup-clear
//
// It creates a key the way tpm2-openssl does, under a transient ECC primary in the owner
// hierarchy, and proves the certificate for it through the regular file store.
func TestCollect_TPMKeyEndToEnd(t *testing.T) {
if os.Getenv(tpm.DeviceEnv) == "" {
t.Skipf("set %s to a TPM device or swtpm socket to run", tpm.DeviceEnv)
}
public, private := createTPMKey(t)
keyPEM := tpmtest.EncodePEM(t, public, private)
block, _ := pem.Decode([]byte(keyPEM))
require.NotNil(t, block)
signer, err := tpm.ParseKey(block.Bytes)
require.NoError(t, err)
ca := certtest.NewCA(t, "corp")
leaf := ca.Issue(t, signer, "device")
dir := t.TempDir()
writeFile(t, dir, "device.pem", certtest.CertPEM(leaf))
writeFile(t, dir, "device.key", keyPEM)
challenger := certposture.NewChallenger([]byte("secret"))
now := time.Now()
nonce := challenger.Nonce(peerKey, now)
checks := []*proto.Checks{{CertificateChallenge: &proto.CertificateChallenge{Nonce: nonce, CaCertificates: []string{ca.PEM}}}}
proofs := Collect(context.Background(), NewFileStore(dir), checks, peerKey)
require.Len(t, proofs, 1, "the TPM-held key must prove the certificate")
chain, err := challenger.Verify(proofs[0], peerKey, now)
require.NoError(t, err)
assert.True(t, leaf.Equal(chain[0]), "proof must carry the device certificate")
}
func createTPMKey(t *testing.T) (public, private []byte) {
t.Helper()
rwc, err := tpm.Open()
require.NoError(t, err)
defer func() { _ = rwc.Close() }()
parent, _, err := tpm2.CreatePrimary(rwc, tpm2.HandleOwner, tpm2.PCRSelection{}, "", "", tss2.ECCSRKTemplate)
require.NoError(t, err)
defer func() { _ = tpm2.FlushContext(rwc, parent) }()
private, public, _, _, _, err = tpm2.CreateKey(rwc, parent, tpm2.PCRSelection{}, "", "", tpmtest.SigningTemplate())
require.NoError(t, err)
return public, private
}