Files
cek/identity_test.go
zeekayandHanzo Dev c4e6f48000 cek: state the identity a key is derived under, and the key's bytes
A file's key comes from two joins: an owner and a file id joined into one
identity, and a namespace and a subsystem joined into one info string. Both
joins were described but never stated — the vectors took the pieces already
joined, so they held for any way of joining them.

These state them. Every expected value is a literal from an implementation of
the scheme written separately from this one, in another language, from the
original source: hanzoai/sqlite v0.4.0 and hanzoai/cloud v1.801.360. That
implementation reproduces the vectors already here exactly, which is what makes
it a reference rather than a second opinion.

A sidecar sealed by it is opened here from nothing but a namespace and a file
on disk, so the reader has to arrive at the owner and the file rather than be
handed them. The encoding is read back by a reader written from its definition,
over lengths that run past 128, where the length prefix grows a second byte,
and past 16384, where it grows a third.

The derivation is unchanged.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-19 22:27:13 -07:00

431 lines
14 KiB
Go

package cek
import (
"encoding/binary"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hanzoai/namespace"
sqlitedrv "github.com/hanzoai/sqlite"
)
// The identity a sidecar's key was derived under, pinned as literal bytes.
//
// sidecar_golden_test.go pins wrapKey and unwrapDEK for identities written out by
// hand. That leaves the function that BUILDS an identity from a namespace and a
// file id unpinned, and it is the one carrying the owner: change how the owner
// and the file are joined and every sidecar on disk derives a different key,
// while a suite that hands the pieces in already joined stays green.
//
// So these state the composition itself. Every expected value below is a literal
// produced by an implementation of the scheme written separately from this one,
// in another language, from the original source — hanzoai/sqlite v0.4.0's
// lengthPrefixedInfo/DeriveKey/WrapDEK and hanzoai/cloud v1.801.360's
// derivationID and nsPrincipal. That implementation reproduces the vectors in
// sidecar_golden_test.go exactly, which is what makes it a reference rather than
// a second opinion.
// fileID is the 16 random bytes a sidecar opens with. Fixed here so the
// identities below are literals rather than something a test computes.
func fileID() []byte {
f := make([]byte, fileIDLen)
for i := range f {
f[i] = byte(i)
}
return f
}
const fileIDHex = "000102030405060708090a0b0c0d0e0f"
// The owner and the file, in that order. A system namespace owns its files as
// the platform and carries the bare file id; every other namespace carries its
// own id ahead of it.
func TestTheIdentityAFileIsWrappedUnder(t *testing.T) {
for _, tc := range []struct {
name string
ns namespace.Namespace
ptype string
id string
}{
{
name: "an org",
ns: namespace.MustOrg("hanzo"),
ptype: "org",
id: "hanzo/" + fileIDHex,
},
{
// The group names which of an org's files this is; the file id already
// tells one file from another, so the group is not part of the identity.
name: "a project of that org",
ns: namespace.MustOrgProject("acme", "web"),
ptype: "org",
id: "acme/" + fileIDHex,
},
{
name: "the platform",
ns: namespace.System(),
ptype: "global",
id: fileIDHex,
},
{
name: "a group of the platform's own files",
ns: namespace.System().WithGroup(namespace.MustGroup("notices")),
ptype: "global",
id: fileIDHex,
},
{
name: "an org named like the platform",
ns: namespace.MustOrg("global"),
ptype: "org",
id: "global/" + fileIDHex,
},
{
// A name chosen to read as a bare file id. It stays an org.
name: "an org named like a file id",
ns: namespace.MustOrg("0011223344556677889aabbccddeeff0"),
ptype: "org",
id: "0011223344556677889aabbccddeeff0/" + fileIDHex,
},
{
name: "an org id carrying the shape a folded name gets",
ns: namespace.MustOrg("acme-0123456789abcdef"),
ptype: "org",
id: "acme-0123456789abcdef/" + fileIDHex,
},
{
name: "the longest org id there is",
ns: namespace.MustOrg("a" + strings.Repeat("b", 63)),
ptype: "org",
id: "a" + strings.Repeat("b", 63) + "/" + fileIDHex,
},
{
// Not a shape any store has, but it is the branch every namespace that
// is not the platform takes, and it is what the original mapped too.
name: "a namespace that names some other entity",
ns: namespace.MustRepo("r1"),
ptype: "org",
id: "r1/" + fileIDHex,
},
} {
t.Run(tc.name, func(t *testing.T) {
ptype, id := wrapIdentity(tc.ns, fileID())
if ptype != tc.ptype || id != tc.id {
t.Fatalf("the identity drifted from the one on disk:\n got (%q, %q)\nwant (%q, %q)", ptype, id, tc.ptype, tc.id)
}
})
}
}
// Two file ids under one owner, to pin that the file is rendered as hex of
// exactly the 16 bytes it is and nothing else is folded in.
func TestTheFileIsRenderedAsItself(t *testing.T) {
for _, tc := range []struct{ name, raw, id string }{
{"a zero file id", strings.Repeat("00", fileIDLen), "hanzo/" + strings.Repeat("00", fileIDLen)},
{"an all-ones file id", strings.Repeat("ff", fileIDLen), "hanzo/" + strings.Repeat("ff", fileIDLen)},
} {
t.Run(tc.name, func(t *testing.T) {
raw, err := hex.DecodeString(tc.raw)
if err != nil {
t.Fatal(err)
}
ptype, id := wrapIdentity(namespace.MustOrg("hanzo"), raw)
if ptype != principalOrg || id != tc.id {
t.Fatalf("got (%q, %q), want (%q, %q)", ptype, id, principalOrg, tc.id)
}
})
}
}
// The bytes an identity renders to, pinned literally.
func TestThePrincipalEncoding(t *testing.T) {
for _, tc := range []struct{ name, ptype, id, want string }{
{
name: "an org's file",
ptype: "org",
id: "hanzo/" + fileIDHex,
want: "036f72672668616e7a6f2f3030303130323033303430353036303730383039306130623063306430653066",
},
{
name: "the platform's own file",
ptype: "global",
id: fileIDHex,
want: "06676c6f62616c203030303130323033303430353036303730383039306130623063306430653066",
},
{
name: "the longest identity a namespace can produce",
ptype: "org",
id: strings.Repeat("a", 64) + "/" + fileIDHex,
want: "036f726761616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161612f3030303130323033303430353036303730383039306130623063306430653066",
},
{
// The pair that shows the length prefix earning its place: run these
// together without one and both read "abc".
name: "a type that ends where the id begins",
ptype: "ab",
id: "c",
want: "0261620163",
},
{
name: "the same letters split the other way",
ptype: "a",
id: "bc",
want: "0161026263",
},
} {
t.Run(tc.name, func(t *testing.T) {
if got := hex.EncodeToString(principalInfo(tc.ptype, tc.id)); got != tc.want {
t.Fatalf("the encoding drifted:\n got %s\nwant %s", got, tc.want)
}
})
}
}
// read is a reader for the encoding, written from its definition rather than
// from the encoder. principalInfo has no decoder in the package, so this is a
// second implementation and not the first one run backwards: it is what lets
// "these bytes mean exactly one identity" be tested at all.
func read(t *testing.T, b []byte) (ptype, id string) {
t.Helper()
field := func(b []byte) ([]byte, []byte) {
n, w := binary.Uvarint(b)
if w <= 0 {
t.Fatalf("no length at the front of %x", b)
}
b = b[w:]
if uint64(len(b)) < n {
t.Fatalf("length says %d, only %d bytes left", n, len(b))
}
return b[:n], b[n:]
}
tb, rest := field(b)
ib, rest := field(rest)
if len(rest) != 0 {
t.Fatalf("%d bytes left over", len(rest))
}
return string(tb), string(ib)
}
// One identity, one encoding, and the encoding says which. Lengths run across
// 128, where the prefix grows a second byte, and across 16384, where it grows a
// third: those are the two places a length prefix stops being one byte and a
// reader that assumed otherwise would start reading an identity as another.
func TestThePrincipalEncodingSaysWhichIdentityItIs(t *testing.T) {
lengths := []int{1, 2, 3, 126, 127, 128, 129, 130, 16382, 16383, 16384, 16385}
seen := map[string][2]int{}
for _, lt := range []int{1, 2, 3, 127, 128, 129} {
for _, li := range lengths {
ptype, id := strings.Repeat("t", lt), strings.Repeat("i", li)
enc := principalInfo(ptype, id)
gotType, gotID := read(t, enc)
if gotType != ptype || gotID != id {
t.Fatalf("a type of %d and an id of %d read back as %d and %d", lt, li, len(gotType), len(gotID))
}
if was, ok := seen[string(enc)]; ok {
t.Fatalf("(%d, %d) encodes the same as (%d, %d)", lt, li, was[0], was[1])
}
seen[string(enc)] = [2]int{lt, li}
}
}
}
// An org id and a file id must not run together into a string that could be read
// as some other pair. Two things keep them apart: a namespace id is
// [a-z0-9][a-z0-9_-]* and so carries no separator, and the file is always the
// last 32 characters because it is always 16 bytes. Either alone settles it, so
// this checks both hold for names a person can choose.
func TestTheOwnerAndTheFileStayApart(t *testing.T) {
names := []string{
"acme",
"global",
"acme/0011223344556677889aabbccddeeff",
"acme/" + fileIDHex,
"/" + fileIDHex,
"a/b/c",
"ACME",
"acme.",
"acme-0123456789abcdef",
"0011223344556677889aabbccddeeff0",
strings.Repeat("z", 64),
"org",
"système",
"acme:" + fileIDHex,
}
for _, name := range names {
t.Run(name, func(t *testing.T) {
ns, err := namespace.OrgProject(name, "")
if err != nil {
t.Skipf("%q is not a name a namespace can be built from: %v", name, err)
}
ptype, id := wrapIdentity(ns, fileID())
if ptype != principalOrg {
t.Fatalf("an org rendered as %q", ptype)
}
if strings.Contains(ns.ID(), "/") {
t.Fatalf("the org id %q carries a separator, so the owner no longer ends where the file begins", ns.ID())
}
// Read the pair back the two independent ways, and require both to
// return the owner and the file that went in.
owner, file, ok := strings.Cut(id, "/")
if !ok || owner != ns.ID() || file != fileIDHex {
t.Fatalf("splitting %q at its first separator gave (%q, %q), want (%q, %q)", id, owner, file, ns.ID(), fileIDHex)
}
if len(id) < len(fileIDHex)+1 || id[len(id)-len(fileIDHex)-1] != '/' {
t.Fatalf("%q does not end in a separator and 32 hex characters", id)
}
if got := id[len(id)-len(fileIDHex):]; got != fileIDHex {
t.Fatalf("the last %d characters of %q are %q, want the file id", len(fileIDHex), id, got)
}
if got := id[:len(id)-len(fileIDHex)-1]; got != ns.ID() {
t.Fatalf("what is left of %q is %q, want the owner %q", id, got, ns.ID())
}
})
}
}
// The platform is not a tenant, and no org can be named into becoming it. The
// type is the first field of the encoding and it is length-prefixed, so the two
// forms differ at byte zero however the rest is chosen.
func TestNoOrgCanBeNamedIntoThePlatform(t *testing.T) {
platform := principalInfo(wrapIdentity(namespace.System(), fileID()))
for _, name := range []string{
"global",
fileIDHex,
"global-" + fileIDHex,
"0011223344556677889aabbccddeeff0",
} {
t.Run(name, func(t *testing.T) {
ns, err := namespace.OrgProject(name, "")
if err != nil {
t.Skipf("not a usable name: %v", err)
}
org := principalInfo(wrapIdentity(ns, fileID()))
if string(org) == string(platform) {
t.Fatalf("the org %q encodes exactly as the platform does", name)
}
if org[0] == platform[0] {
t.Fatalf("the org %q and the platform start with the same byte %#x", name, org[0])
}
})
}
}
// The wrapping key for an identity this package COMPOSED, pinned to the key the
// original derived. This is the composition and the derivation in one statement:
// a change to either end fails here.
func TestTheWrappingKeyForAComposedIdentity(t *testing.T) {
for _, tc := range []struct {
name string
ns namespace.Namespace
kek string
}{
{"an org", namespace.MustOrg("hanzo"), "ab95e9935e6a8e430c579b0189a1ce7b438ba3a68f0d3c6ec49b99f5194eb265"},
{"the platform", namespace.System(), "7d66331c88e1d7a2344ca752590c80deb88ad6d73c19ef14c2065c33fc431487"},
{"another org", namespace.MustOrg("acme-2"), "ccf8ba20b8f73b9a33cfed386805cbfbcfae2e506facba312006e6bc49c12e9a"},
} {
t.Run(tc.name, func(t *testing.T) {
ptype, id := wrapIdentity(tc.ns, fileID())
kek, err := wrapKey(goldenMaster(), ptype, id)
if err != nil {
t.Fatal(err)
}
if got := hex.EncodeToString(kek); got != tc.kek {
t.Fatalf("the wrapping key drifted:\n got %s\nwant %s", got, tc.kek)
}
})
}
}
// A sidecar sealed by the other implementation, opened by this one.
//
// Nothing here tells the reader which identity to use: it is handed a namespace
// and a file on disk, and has to arrive at the same owner-and-file the sealer
// used. That is the property the composition exists for, and the reason these
// blobs are literals — sealing them here with the same function that reads them
// would agree with itself whatever it did.
func TestASidecarSealedElsewhereOpensHere(t *testing.T) {
const dek = "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0"
for _, tc := range []struct {
name string
ns namespace.Namespace
subsystem string
blob string
}{
{
name: "an org's file",
ns: namespace.MustOrg("hanzo"),
subsystem: "kms",
blob: "01a1b2c3d4e5f60718293a4b5cde06ded7ee709daf4af9689176bb794a13c2640f5a04c14088bcf6fef6b46a2d309d1e8dfa898b147bba7fb7e19002a3",
},
{
name: "the platform's own file",
ns: namespace.System(),
subsystem: "treasury",
blob: "01a1b2c3d4e5f60718293a4b5c715726d14491a936bf930fe407fe32c1253e96ead3d4df7963e07111f127d3ba8978f5011d041c8dd85be9ad4cc72a09",
},
} {
t.Run(tc.name, func(t *testing.T) {
master := goldenMaster()
if err := SetMaster(master); err != nil {
t.Fatal(err)
}
dir := t.TempDir()
path, err := namespace.Path(dir, tc.ns, tc.subsystem)
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
key, err := hex.DecodeString(dek)
if err != nil {
t.Fatal(err)
}
db, err := sqlitedrv.OpenDB(path, key)
if err != nil {
t.Fatalf("write a database under the sealed key: %v", err)
}
if _, err := db.Exec(`CREATE TABLE secret (v TEXT)`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO secret VALUES ('sealed elsewhere')`); err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
blob, err := hex.DecodeString(tc.blob)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path+sidecarSuffix, append(fileID(), blob...), 0o600); err != nil {
t.Fatal(err)
}
opened, err := Open(tc.ns, tc.subsystem, dir)
if err != nil {
t.Fatalf("open a database whose key was sealed by the other implementation: %v", err)
}
defer opened.Close()
var v string
if err := opened.QueryRow(`SELECT v FROM secret`).Scan(&v); err != nil {
t.Fatalf("read the row: %v", err)
}
if v != "sealed elsewhere" {
t.Fatalf("read %q", v)
}
})
}
}