nats-io/nats-server · critical

unable to unseal data: %v

Error message

unable to unseal data: %v

What it means

The key object was loaded and the policy session created, but tpm2.UnsealWithSession failed to unseal the data. Because the policy binds both PCR state and password, this most often means the PCR value changed since sealing or the object password is wrong.

Source

Thrown at server/tpm/js_ek_tpm_windows.go:197

	// Load the public/private blobs into the TPM for decryption.
	objectHandle, _, err := tpm2.Load(rwc, srkHandle, srkPassword, publicBlob, privateBlob)
	if err != nil {
		return "", fmt.Errorf("unable to load data: %v", err)
	}
	defer tpm2.FlushContext(rwc, objectHandle)

	// Create the authorization session with TPM.
	sessHandle, _, err := policyPCRPasswordSession(rwc, pcr)
	if err != nil {
		return "", fmt.Errorf("unable to get auth session: %v", err)
	}
	defer func() {
		tpm2.FlushContext(rwc, sessHandle)
	}()
	// Unseal the data we've loaded into the TPM with the object (js key) password.
	unsealedData, err := tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword)
	if err != nil {
		return "", fmt.Errorf("unable to unseal data: %v", err)
	}
	return string(unsealedData), nil
}

// Returns session handle and policy digest.
func policyPCRPasswordSession(rwc io.ReadWriteCloser, pcr int) (sessHandle tpmutil.Handle, policy []byte, retErr error) {
	sessHandle, _, err := tpm2.StartAuthSession(
		rwc,
		tpm2.HandleNull,  /*tpmKey*/
		tpm2.HandleNull,  /*bindKey*/
		make([]byte, 16), /*nonceCaller*/
		nil,              /*secret*/
		tpm2.SessionPolicy,
		tpm2.AlgNull,
		tpm2.AlgSHA256)
	if err != nil {
		return tpm2.HandleNull, nil, fmt.Errorf("unable to start session: %v", err)
	}

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Compare current PCR digest with the one at seal time (tpm2_pcrread); if changed, re-seal the key.
  2. Verify jsKeyPassword/objectPassword matches the one used at seal time.
  3. As a last resort, delete the key file and regenerate the encryption key (data encrypted with the old key becomes unrecoverable).
  4. Back up the key file path mapping to the PCR index used at seal time.

Example fix

// before: wrong password silently propagated
unsealedData, err := tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword)
// after: fail with actionable context
unsealedData, err := tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword)
if err != nil {
	return "", fmt.Errorf("unable to unseal data (check password and PCR %d state): %v", pcr, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

currentPCR, err := tpm2.ReadPCR(rwc, pcr, tpm2.AlgSHA256)
if err != nil {
	return fmt.Errorf("cannot read PCR %d: %w", pcr, err)
}
// compare with PCR digest recorded at seal time before attempting unseal

Type guard

func isUnsealError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "unable to unseal data")
}

Try / catch

key, err := LoadJetStreamEncryptionKeyFromTPM(rwc)
if err != nil && isUnsealError(err) {
	log.Printf("unseal failed (PCR %d state or password changed): %v", pcr, err)
	// DO NOT delete key file automatically — encrypted data may be lost
	return err
}

Prevention

When it happens

Trigger: tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword) errors — jsKeyPassword mismatch, PCR state differs from seal time, or policy not satisfied.

Common situations: A firmware/OS update changed PCR values (e.g. PCR 0/7) after the key was sealed; wrong jsKeyPassword passed by caller; TPM HMAC/authorization failure.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/66c214f35fd33b1d. Report an issue: GitHub.