cek: a sidecar that will not open says which identity it derived

The wrapping key is HKDF over the master AND the principal, so three
different things make an unwrap fail — and the message named only two of
them, both about the master or the file. The third, an envelope sealed
for one identity and read under another, is the one that looks least like
itself: the master is correct, the sidecar is intact, and every word in
the error points somewhere else.

Naming the identity this open derived is the whole diagnosis. Compare it
with the one that sealed the file and a mismatch is visible; without it a
reader hunts for a lost master that was never lost.

unwrapDEK now states the cryptographic fact and nothing more, because the
caller is what holds the identity and can say which one it tried.
This commit is contained in:
zooqueen
2026-08-20 08:35:23 -07:00
parent c4e6f48000
commit 6c219194e7
2 changed files with 59 additions and 4 deletions
+15 -4
View File
@@ -94,7 +94,17 @@ func sidecarKey(master []byte, ns namespace.Namespace, path string) ([]byte, err
}
defer zero(kek)
return unwrapDEK(kek, wrapped, principalInfo(ptype, id))
dek, err := unwrapDEK(kek, wrapped, principalInfo(ptype, id))
if err != nil {
// SAY WHICH IDENTITY. The wrapping key is HKDF over the master AND the
// principal, so three different things make this fail and only one of them
// is the master. Naming the identity this open derived is what separates
// them: compare it with the identity that sealed the file and a mismatch
// is visible, where "wrong key or damaged sidecar" sends the reader to
// hunt for a lost master that was never lost.
return nil, fmt.Errorf("cek: the key beside this database does not open under %s %q — the master, the identity, or the sidecar itself differs from the one that sealed it: %w", ptype, id, err)
}
return dek, nil
}
// wrapIdentity is the identity a sidecar's wrapping key was derived under: the
@@ -142,8 +152,9 @@ func wrapKey(master []byte, ptype, id string) ([]byte, error) {
// unwrapDEK opens a wrapped DEK — version(1) || nonce(12) || ciphertext||tag,
// AES-256-GCM — with the version byte leading the additional data.
//
// A failure here is "the key is not here": a different master, or a damaged
// sidecar. GCM authenticates before it returns anything, so there is no partial
// A failure here is "this key does not open this envelope", and that is all it
// is: the caller holds the identity the key was derived under and says which one
// it tried. GCM authenticates before it returns anything, so there is no partial
// key to hand back.
func unwrapDEK(kek, blob, aad []byte) ([]byte, error) {
block, err := aes.NewCipher(kek)
@@ -163,7 +174,7 @@ func unwrapDEK(kek, blob, aad []byte) ([]byte, error) {
}
dek, err := gcm.Open(nil, blob[1:1+ns], blob[1+ns:], append([]byte{wrapVersion}, aad...))
if err != nil {
return nil, fmt.Errorf("cek: unwrap the key beside this database (wrong master key or damaged sidecar): %w", err)
return nil, fmt.Errorf("cek: the wrapped key does not authenticate: %w", err)
}
if len(dek) != dekLen {
return nil, fmt.Errorf("cek: unwrapped key is %d bytes, not %d", len(dek), dekLen)
+44
View File
@@ -4,8 +4,10 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hanzoai/namespace"
@@ -220,3 +222,45 @@ func TestTheWrongMasterDoesNotOpenAWrappedDEKDatabase(t *testing.T) {
t.Fatal("a wrapped-DEK database opened under the wrong master key")
}
}
// A sidecar sealed for one identity and opened under another is the failure that
// looks least like itself: the master is right and the file is intact, so every
// message about keys and corruption points away from the cause. The error has to
// name the identity it derived, because that name is the whole diagnosis.
//
// It reads the sidecar and nothing else — no database is opened — because the
// identity is settled before a page is ever decrypted.
func TestAnIdentityMismatchNamesTheIdentityItDerived(t *testing.T) {
master := mk(15)
dir := t.TempDir()
path := filepath.Join(dir, "kms.db")
ns := mustNS(t, "hanzo", "")
fileID := make([]byte, fileIDLen)
if _, err := rand.Read(fileID); err != nil {
t.Fatal(err)
}
// Seal a key for an identity this namespace does NOT derive.
stored := "someone-else/" + hex.EncodeToString(fileID)
kek, err := wrapKey(master, principalOrg, stored)
if err != nil {
t.Fatal(err)
}
wrapped := wrapForTest(t, kek, mk(16), principalInfo(principalOrg, stored))
if err := os.WriteFile(path+sidecarSuffix, append(append([]byte{}, fileID...), wrapped...), 0o600); err != nil {
t.Fatal(err)
}
_, err = sidecarKey(master, ns, path)
if err == nil {
t.Fatal("a sidecar sealed for another identity opened anyway")
}
_, derived := wrapIdentity(ns, fileID)
if !strings.Contains(err.Error(), derived) {
t.Fatalf("the error does not name the identity it derived (%q):\n %v", derived, err)
}
if strings.Contains(err.Error(), stored) {
t.Fatalf("the error names an identity this open never derived:\n %v", err)
}
}