nats-io/nats-server · critical

unable to seal data: %v

Error message

unable to seal data: %v

What it means

createAndSealJsEncryptionKey failed to seal the newly generated JetStream encryption key into the TPM. tpm2.Seal wraps jsStoreKey under the SRK with a policy-PCR+password session; any TPM-level failure (handle, auth, policy mismatch) surfaces here wrapped with the underlying cause in %v.

Source

Thrown at server/tpm/js_ek_tpm_windows.go:167

	if err != nil {
		return "", fmt.Errorf("unable to get policy: %v", err)
	}
	if err := tpm2.FlushContext(rwc, sessHandle); err != nil {
		return "", fmt.Errorf("unable to flush session: %v", err)
	}
	// Seal the data to the parent key and the policy
	user, err := nkeys.CreateUser()
	if err != nil {
		return "", fmt.Errorf("unable to create seed: %v", err)
	}
	// We'll use the seed to represent the encryption key.
	jsStoreKey, err := user.Seed()
	if err != nil {
		return "", fmt.Errorf("unable to get seed: %v", err)
	}
	privateArea, publicArea, err := tpm2.Seal(rwc, srkHandle, srkPassword, jsKeyPassword, policy, jsStoreKey)
	if err != nil {
		return "", fmt.Errorf("unable to seal data: %v", err)
	}
	err = writeTPMKeysToFile(jsKeyFile, privateArea, publicArea)
	if err != nil {
		return "", fmt.Errorf("unable to write key file: %v", err)
	}
	return string(jsStoreKey), nil
}

// Unseals the JetStream encryption key from the TPM with the provided keys.
// The key is returned as a string.
func unsealJsEncrpytionKey(rwc io.ReadWriteCloser, pcr int, srkHandle tpmutil.Handle, srkPassword, objectPassword string, publicBlob, privateBlob []byte) (string, error) {
	// 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)

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Inspect the wrapped %v cause for the actual TPM error (auth failure vs I/O).
  2. Verify the SRK handle (e.g. tpm2.HandleOwner) exists and srkPassword is correct (often empty string).
  3. Confirm the TPM device is open and responsive before calling (see LoadJetStreamEncryptionKeyFromTPM).
  4. Ensure the policy session (policyPCRPasswordSession) succeeds and PCRs haven't changed since sealing.

Example fix

// before: guessing srk password
privateArea, publicArea, err := tpm2.Seal(rwc, srkHandle, srkPassword, jsKeyPassword, policy, jsStoreKey)
// after: ensure SRK exists with empty auth before sealing
srkHandle := tpm2.HandleOwner
srkPassword := "" // default SRK auth unless changed
privateArea, publicArea, err := tpm2.Seal(rwc, srkHandle, srkPassword, jsKeyPassword, policy, jsStoreKey)
Defensive patterns

Strategy: try-catch

Validate before calling

f, err := os.OpenFile(tpmDevicePath, os.O_RDWR, 0)
if err != nil {
	return fmt.Errorf("TPM device unavailable: %w", err)
}
f.Close()

Type guard

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

Try / catch

key, err := LoadJetStreamEncryptionKeyFromTPM(rwc)
if err != nil {
	if strings.Contains(err.Error(), "unable to seal data") {
		log.Printf("TPM seal failed: %v — verify SRK handle and PCR state", err)
	}
	return err
}

Prevention

When it happens

Trigger: tpm2.Seal(rwc, srkHandle, srkPassword, jsKeyPassword, policy, jsStoreKey) returns non-nil err — e.g. bad SRK handle/password, SRK not created, policy digest mismatch, or TPM device I/O error.

Common situations: Wrong srkHandle or srkPassword passed in; TPM not accessible or not initialized; PCR state changed so the policy session is invalid; first run where the SRK persistent handle doesn't exist.

Related errors


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