fleet-enrol: content-aware NV check (-check-only), not blind "index defined"
The enrol trigger keyed on "is NV 0x01800001 defined/readable", so leftover state (an earlier tpm-provision run — bench-seen on the 820: "cert present — normal boot" with no real identity) or random bytes (tpm-selftest) silently suppressed enrolment. Now content-aware: - tpmkey.ReadNVCert reads the full NV contents (chunked). - classifyNVCert (TPM-agnostic, unit-tested): parses X.509, checks validity window, and when a real fleet CA bundle is present verifies the cert chains to it. Only a valid fleet cert = enrolled. - New `fleet-enrol -check-only` (no network/no key-gen) exits 0=valid cert, 20=enrol needed, 1=error. The full enrol path uses the same classification instead of NVPresent. - The dracut hook calls -check-only as the single gate, replacing the blind tpm2_nvread (and its now-dead TPM2TOOLS_TCTI export). tpm2_nvread stays in the initramfs for the secure path (dot1x-macsec-up.sh). Lab fallback: the shipped radius-ca.pem is a placeholder (not a cert) -> loadCAPool returns nil -> chain-less "parseable + unexpired" acceptance, so reboot-converge doesn't loop; production replaces radius-ca.pem with the real fleet CA for strict chaining. Tests: classifyNVCert table (valid/expired/garbage/foreign/no-CA) + simtest ReadNVCert roundtrip across the 512B chunk. All pass; packer validate clean.
This commit is contained in:
+15
-7
@@ -1077,13 +1077,21 @@ have their EK there), so enrolment could never create our key.
|
||||
(`avc: denied … tpm_device_t`); the faithful path is the **initramfs** run
|
||||
(reboot, unconfined), or `enforcing=0` for hand-run TPM inspection.
|
||||
|
||||
**FOLLOW-UP HARDENING (from the two findings above, not yet built):**
|
||||
- **Content-aware NV presence check** — read NV `0x01800001`, parse as X.509, verify
|
||||
it **chains to the fleet CA** (bundled `radius-ca.pem`); only then treat as enrolled,
|
||||
else enrol + overwrite (`WriteNVCert` already undefine+redefines). Cleanest as a
|
||||
`fleet-enrol -check-only` mode (no network) the hook calls — one content-aware source
|
||||
of truth, replacing the hook's blind `tpm2_nvread` and the binary's `NVPresent`
|
||||
(defined-only). Stops leftover/foreign NV from suppressing enrolment.
|
||||
**FOLLOW-UP HARDENING:**
|
||||
- **Content-aware NV presence check — BUILT 2026-07-03.** `fleet-enrol -check-only`
|
||||
(no network / no key-gen) reads NV `0x01800001` via `tpmkey.ReadNVCert`, parses it as
|
||||
X.509, checks the validity window, and — when a real fleet CA is bundled (`-cacert`,
|
||||
default `radius-ca.pem`) — verifies it **chains** to it. Only a valid fleet cert counts
|
||||
as enrolled (rc 0); empty/garbage/foreign/expired → rc 20 (enrol + overwrite;
|
||||
`WriteNVCert` undefine+redefines). The dracut hook now calls it as the single gate,
|
||||
replacing the blind `tpm2_nvread` (removed from `tpm-enrol.sh`; the binary's `NVPresent`
|
||||
stays for cheap existence checks). **Lab fallback:** the shipped `radius-ca.pem` is a
|
||||
*placeholder* (comments, not a cert) → `loadCAPool` returns nil → chain-less "parseable
|
||||
+ unexpired" acceptance, so the reboot-converge doesn't loop on a bench-CA cert;
|
||||
**production must replace `radius-ca.pem` with the real fleet CA** for strict chaining.
|
||||
Tests: `classifyNVCert` unit table (valid/expired/garbage/foreign/no-CA) + `simtest`
|
||||
ReadNVCert roundtrip across the 512B chunk. **BENCH-VERIFY** on a real boot: a garbage/
|
||||
foreign NV now enrols instead of "normal boot".
|
||||
- **De-enrol / TPM-reset procedure** for re-testing: `tpm2_nvundefine 0x01800001 -C o`
|
||||
(+ `tpm2_evictcontrol -C o -c 0x81000100` for a stale *app* key — never the EK), or
|
||||
BIOS "Clear TPM". Document in the bench README.
|
||||
|
||||
@@ -26,7 +26,6 @@ type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
|
||||
STAMP=/run/dot1x/.enrol-done
|
||||
[ -e "$STAMP" ] && exit 0
|
||||
mkdir -p /run/dot1x
|
||||
export TPM2TOOLS_TCTI="device:/dev/tpmrm0"
|
||||
|
||||
# --- skip conditions --------------------------------------------------------
|
||||
if [ "$(getarg rd.fleet.enrol=)" = "0" ]; then
|
||||
@@ -55,16 +54,21 @@ while [ ! -e /dev/tpmrm0 ] && [ "$i" -lt 10 ]; do sleep 1; i=$((i + 1)); done
|
||||
# --- already enrolled? then this is a normal boot — do NOT touch the network -
|
||||
# (Bringing up en* here would fight the secure path, which needs it UNMANAGED for
|
||||
# wpa_supplicant. So gate on NV BEFORE any network action.)
|
||||
if tpm2_nvread "$NVINDEX" -o /run/dot1x/nvprobe.der >/dev/null 2>&1 \
|
||||
&& [ -s /run/dot1x/nvprobe.der ]; then
|
||||
info "tpm-enrol: client cert present at NV $NVINDEX — normal boot"
|
||||
rm -f /run/dot1x/nvprobe.der
|
||||
exit 0
|
||||
fi
|
||||
# CONTENT-AWARE gate (fleet-enrol -check-only, no network/no key-gen): reads NV,
|
||||
# parses the cert, and — when the fleet CA is bundled — verifies it chains, so a
|
||||
# leftover/foreign/expired cert (or garbage) does NOT masquerade as enrolled.
|
||||
# rc: 0 = valid fleet cert (normal boot); 20 = enrolment needed; other = error.
|
||||
/usr/sbin/fleet-enrol -check-only -nvindex "$NVINDEX" -handle "$HANDLE"
|
||||
rc=$?
|
||||
case "$rc" in
|
||||
0) info "tpm-enrol: valid fleet cert at NV $NVINDEX — normal boot"; exit 0 ;;
|
||||
20) : ;; # fall through to enrolment
|
||||
*) warn "tpm-enrol: NV check failed (rc=$rc) — leaving to the secure path"; exit 0 ;;
|
||||
esac
|
||||
|
||||
# Committed to enrolling — one attempt per boot.
|
||||
: > "$STAMP"
|
||||
warn "tpm-enrol: NV $NVINDEX empty — starting FIRST-TIME TPM enrolment"
|
||||
warn "tpm-enrol: no valid fleet cert in NV $NVINDEX — starting FIRST-TIME TPM enrolment"
|
||||
|
||||
# NIC: explicit rd.dot1x.if=, else first non-loopback, non-macsec interface.
|
||||
NIC=$(getarg rd.dot1x.if=)
|
||||
|
||||
+31
-8
@@ -12,10 +12,14 @@
|
||||
// The hook then reboots into the normal secure path. Everything host-specific
|
||||
// (key, cert, hostnqn) is created here at runtime — nothing is baked (invariant #1).
|
||||
//
|
||||
// Exit codes: 0 = enrolled (cert written to NV; caller should reboot);
|
||||
// Exit codes (default/enrol mode): 0 = enrolled (cert written to NV; caller
|
||||
// should reboot); 10 = a VALID fleet cert is already present (normal boot —
|
||||
// caller proceeds); 1 = failure (caller leaves NV empty -> boot fails -> emergency).
|
||||
//
|
||||
// 10 = cert already present (normal boot — caller proceeds);
|
||||
// 1 = failure (caller leaves NV empty -> boot fails -> emergency).
|
||||
// Exit codes (-check-only mode, no network/no key-gen): 0 = a valid fleet cert is
|
||||
// present (caller: normal boot); 20 = enrolment needed (NV empty/garbage/foreign/
|
||||
// expired); 1 = error (e.g. TPM open failed). The dracut hook uses this as the
|
||||
// single content-aware gate instead of a blind "is the NV index defined" check.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -36,7 +40,10 @@ import (
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
)
|
||||
|
||||
const exitAlreadyEnrolled = 10
|
||||
const (
|
||||
exitAlreadyEnrolled = 10 // enrol mode: a valid fleet cert is already present
|
||||
exitNeedEnrol = 20 // -check-only: no valid fleet cert → enrolment needed
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
@@ -46,9 +53,10 @@ func main() {
|
||||
handleHex = flag.String("handle", "0x81000100", "TPM persistent handle for the signing key (non-reserved owner handle; NOT the EK slot 0x81010001)")
|
||||
cn = flag.String("cn", "fleet-host", "CSR CommonName (generic; identity is the cert serial)")
|
||||
iface = flag.String("iface", "", "network interface for the device-context MAC")
|
||||
caFile = flag.String("cacert", "/etc/dot1x/radius-ca.pem", "CA bundle for an https broker URL")
|
||||
caFile = flag.String("cacert", "/etc/dot1x/radius-ca.pem", "fleet CA bundle: verifies an https broker URL AND the NV cert's chain")
|
||||
waitFor = flag.Duration("timeout", 30*time.Minute, "max wait for admin approval")
|
||||
pollEvery = flag.Duration("poll", 5*time.Second, "status poll interval")
|
||||
checkOnly = flag.Bool("check-only", false, "classify the NV cert and exit (no network, no key-gen): 0=valid fleet cert, 20=enrol needed, 1=error")
|
||||
)
|
||||
flag.Parse()
|
||||
log.SetFlags(0)
|
||||
@@ -75,9 +83,24 @@ func main() {
|
||||
defer f.Close()
|
||||
tpm := transport.FromReadWriter(f)
|
||||
|
||||
// Already enrolled? Then this is a normal boot; let the secure path run.
|
||||
if tpmkey.NVPresent(tpm, nvIndex) {
|
||||
log.Printf("cert already present at NV %s — normal boot", *nvHex)
|
||||
// Content-aware enrolment decision: read the NV cert, parse it, and (when the
|
||||
// fleet CA is available) verify it chains — so leftover/foreign/garbage NV does
|
||||
// NOT masquerade as "enrolled" (a bare "index defined" check let an old
|
||||
// tpm-provision cert or random bytes silently suppress enrolment).
|
||||
enrolled, detail := nvEnrolled(tpm, nvIndex, *caFile)
|
||||
|
||||
if *checkOnly {
|
||||
if enrolled {
|
||||
log.Printf("check-only: %s — normal boot", detail)
|
||||
os.Exit(0)
|
||||
}
|
||||
log.Printf("check-only: enrolment needed — %s", detail)
|
||||
os.Exit(exitNeedEnrol)
|
||||
}
|
||||
|
||||
// Full enrol mode: a valid fleet cert means this is a normal boot.
|
||||
if enrolled {
|
||||
log.Printf("%s — normal boot", detail)
|
||||
os.Exit(exitAlreadyEnrolled)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// nvcheck.go — content-aware validation of the cert stored in TPM NV. The enrol
|
||||
// trigger must key on "do we hold a VALID FLEET cert", not "is the NV index
|
||||
// defined": a bare presence check let leftover state (an earlier tpm-provision
|
||||
// run, another OS) or random bytes masquerade as enrolled, silently suppressing
|
||||
// enrolment (bench-seen 2026-07-02: a host booted "cert present — normal boot"
|
||||
// with no real identity). classifyNVCert is TPM-agnostic (takes the DER + a CA
|
||||
// pool) so it is unit-testable without a TPM.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"fleet-enrol/tpmkey"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
)
|
||||
|
||||
// loadCAPool builds a cert pool from the fleet CA bundle. Returns nil if the file
|
||||
// is missing or contains no parseable certificates (e.g. the placeholder
|
||||
// radius-ca.pem in lab builds) — callers then fall back to a chain-less check.
|
||||
func loadCAPool(caFile string) *x509.CertPool {
|
||||
b, err := os.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return nil
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// classifyNVCert decides whether the DER in NV is a cert we should treat as an
|
||||
// enrolled fleet identity. Rules, in order: must parse as X.509; must be within
|
||||
// its validity window; and — when a fleet CA pool is available — must chain to
|
||||
// it. With no CA pool (lab/placeholder), a parseable, unexpired cert is accepted
|
||||
// (chain unverified) rather than forcing a re-enrol loop. Every reject returns a
|
||||
// human-readable reason for the boot log.
|
||||
func classifyNVCert(der []byte, caPool *x509.CertPool, now time.Time) (enrolled bool, detail string) {
|
||||
crt, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return false, "NV content is not an X.509 certificate"
|
||||
}
|
||||
switch {
|
||||
case now.Before(crt.NotBefore):
|
||||
return false, "NV cert is not yet valid"
|
||||
case now.After(crt.NotAfter):
|
||||
return false, fmt.Sprintf("NV cert expired (%s)", crt.NotAfter.Format(time.RFC3339))
|
||||
}
|
||||
if caPool == nil {
|
||||
return true, fmt.Sprintf("NV cert present (CN=%q serial %x) — fleet CA unavailable, chain UNVERIFIED",
|
||||
crt.Subject.CommonName, crt.SerialNumber)
|
||||
}
|
||||
// Roots and Intermediates both = the bundle: lets a leaf signed by an
|
||||
// intermediate verify whether the bundle carries the root, the intermediate,
|
||||
// or both. ExtKeyUsageAny: we assert membership, not usage (the cert's EKU is
|
||||
// clientAuth; RADIUS enforces usage at auth time).
|
||||
if _, err := crt.Verify(x509.VerifyOptions{
|
||||
Roots: caPool,
|
||||
Intermediates: caPool,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
CurrentTime: now,
|
||||
}); err != nil {
|
||||
return false, "NV cert does not chain to the fleet CA: " + err.Error()
|
||||
}
|
||||
return true, fmt.Sprintf("valid fleet cert in NV (CN=%q serial %x)", crt.Subject.CommonName, crt.SerialNumber)
|
||||
}
|
||||
|
||||
// nvEnrolled reads the NV cert and classifies it. A read error (undefined/
|
||||
// unreadable index) is "not enrolled" — enrol and (over)write.
|
||||
func nvEnrolled(tpm transport.TPM, nvIndex tpm2.TPMHandle, caFile string) (bool, string) {
|
||||
der, err := tpmkey.ReadNVCert(tpm, nvIndex)
|
||||
if err != nil {
|
||||
return false, "NV empty/undefined/unreadable"
|
||||
}
|
||||
return classifyNVCert(der, loadCAPool(caFile), time.Now())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mkCA returns a self-signed CA cert + key and a pool trusting it.
|
||||
func mkCA(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey, *x509.CertPool) {
|
||||
t.Helper()
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: bigOne(), Subject: pkixCN("fleet CA"),
|
||||
IsCA: true, BasicConstraintsValid: true,
|
||||
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatalf("create CA: %v", err)
|
||||
}
|
||||
ca, _ := x509.ParseCertificate(der)
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(ca)
|
||||
return ca, key, pool
|
||||
}
|
||||
|
||||
// mkLeaf issues a client leaf DER signed by (caCert, caKey), valid over [nb, na].
|
||||
func mkLeaf(t *testing.T, caCert *x509.Certificate, caKey *ecdsa.PrivateKey, nb, na time.Time) []byte {
|
||||
t.Helper()
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: bigN(0x1234), Subject: pkixCN("fleet-host"),
|
||||
NotBefore: nb, NotAfter: na,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
|
||||
if err != nil {
|
||||
t.Fatalf("create leaf: %v", err)
|
||||
}
|
||||
return der
|
||||
}
|
||||
|
||||
func TestClassifyNVCert(t *testing.T) {
|
||||
now := time.Now()
|
||||
ca, caKey, pool := mkCA(t)
|
||||
valid := mkLeaf(t, ca, caKey, now.Add(-time.Hour), now.Add(time.Hour))
|
||||
expired := mkLeaf(t, ca, caKey, now.Add(-2*time.Hour), now.Add(-time.Hour))
|
||||
|
||||
// a leaf from a DIFFERENT CA (foreign / not our fleet)
|
||||
otherCA, otherKey, _ := mkCA(t)
|
||||
foreign := mkLeaf(t, otherCA, otherKey, now.Add(-time.Hour), now.Add(time.Hour))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
der []byte
|
||||
pool *x509.CertPool
|
||||
enrolled bool
|
||||
}{
|
||||
{"valid fleet cert chains to CA", valid, pool, true},
|
||||
{"garbage bytes are not a cert", []byte("not a certificate at all"), pool, false},
|
||||
{"expired cert triggers re-enrol", expired, pool, false},
|
||||
{"foreign cert does not chain", foreign, pool, false},
|
||||
{"no CA pool: parseable+unexpired accepted", valid, nil, true},
|
||||
{"no CA pool: expired still rejected", expired, nil, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, detail := classifyNVCert(c.der, c.pool, now)
|
||||
if got != c.enrolled {
|
||||
t.Fatalf("enrolled=%v want %v (detail: %s)", got, c.enrolled, detail)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
package simtest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -85,7 +86,10 @@ func TestNVWriteAndPresence(t *testing.T) {
|
||||
if tpmkey.NVPresent(tpm, nv) {
|
||||
t.Fatalf("NV should be empty on a fresh TPM")
|
||||
}
|
||||
// realistic cert-sized payload that also crosses the 512B write chunk.
|
||||
if _, err := tpmkey.ReadNVCert(tpm, nv); err == nil {
|
||||
t.Fatalf("ReadNVCert should error on an undefined index")
|
||||
}
|
||||
// realistic cert-sized payload that also crosses the 512B read/write chunk.
|
||||
want := make([]byte, 700)
|
||||
rand.Read(want)
|
||||
if err := tpmkey.WriteNVCert(tpm, nv, want); err != nil {
|
||||
@@ -94,10 +98,21 @@ func TestNVWriteAndPresence(t *testing.T) {
|
||||
if !tpmkey.NVPresent(tpm, nv) {
|
||||
t.Fatalf("NV should be present after write")
|
||||
}
|
||||
// ReadNVCert must return exactly what was written (across the chunk boundary).
|
||||
got, err := tpmkey.ReadNVCert(tpm, nv)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadNVCert: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("ReadNVCert roundtrip mismatch: got %d bytes, want %d", len(got), len(want))
|
||||
}
|
||||
// re-write (re-enrolment) must succeed (undefine + redefine).
|
||||
if err := tpmkey.WriteNVCert(tpm, nv, want[:400]); err != nil {
|
||||
t.Fatalf("WriteNVCert rewrite: %v", err)
|
||||
}
|
||||
if got, _ := tpmkey.ReadNVCert(tpm, nv); !bytes.Equal(got, want[:400]) {
|
||||
t.Fatalf("ReadNVCert after rewrite mismatch: got %d bytes, want 400", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// Regression for the bench failure "key at handle is not ECC ... selector value
|
||||
|
||||
@@ -178,13 +178,47 @@ func EnsureKey(tpm transport.TPM, handle tpm2.TPMHandle) (signer *Signer, create
|
||||
return s, true, err
|
||||
}
|
||||
|
||||
// nvPresent reports whether a defined NV index exists (our enrol NV is defined
|
||||
// and written atomically, so "defined" means "has a cert").
|
||||
// nvPresent reports whether a defined NV index exists. NOTE: "defined" does NOT
|
||||
// mean "holds our fleet cert" — leftover/foreign NV (e.g. an earlier
|
||||
// tpm-provision run, or another OS) also reports present. Prefer ReadNVCert +
|
||||
// content validation for the enrol decision; this stays for cheap existence checks.
|
||||
func NVPresent(tpm transport.TPM, index tpm2.TPMHandle) bool {
|
||||
_, err := (tpm2.NVReadPublic{NVIndex: index}).Execute(tpm)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ReadNVCert reads the full contents of NV index (chunked under the NV buffer
|
||||
// limit). Returns an error if the index is undefined or unreadable — callers
|
||||
// treat that as "no cert" (enrol). The bytes are the caller's to parse/validate;
|
||||
// this does not assume they are a certificate.
|
||||
func ReadNVCert(tpm transport.TPM, index tpm2.TPMHandle) ([]byte, error) {
|
||||
rp, err := (tpm2.NVReadPublic{NVIndex: index}).Execute(tpm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nv readpublic: %w", err)
|
||||
}
|
||||
pub, err := rp.NVPublic.Contents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size := int(pub.DataSize)
|
||||
out := make([]byte, 0, size)
|
||||
const chunk = 512 // conservatively under TPM_MAX_NV_BUFFER_SIZE
|
||||
for off := 0; off < size; off += chunk {
|
||||
end := min(off+chunk, size)
|
||||
r, err := (tpm2.NVRead{
|
||||
AuthHandle: tpm2.AuthHandle{Handle: index, Name: rp.NVName, Auth: tpm2.PasswordAuth(nil)},
|
||||
NVIndex: tpm2.NamedHandle{Handle: index, Name: rp.NVName},
|
||||
Size: uint16(end - off),
|
||||
Offset: uint16(off),
|
||||
}).Execute(tpm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nv read @%d: %w", off, err)
|
||||
}
|
||||
out = append(out, r.Data.Buffer...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// nvWriteCert (re)defines the NV index sized to der and writes it under owner
|
||||
// auth. Chunks the write to stay within the TPM's NV buffer limit.
|
||||
func WriteNVCert(tpm transport.TPM, index tpm2.TPMHandle, der []byte) error {
|
||||
|
||||
Reference in New Issue
Block a user