Rootz

Files for this edition

Rootz Data-Confidentiality Protocols — Specification v1.2

Version 1.2 (supersedes 1.1)
Date 2026-10-16
Publisher Rootz Corp.
Authorship Prepared with AI assistance (Claude).
Companion files SPEC-v1.2-vectors.json (known-answer vectors, test keys only); verify-v1.2.mjs (an independent consumer that opens every vector)

1. Scope

1.1 What is specified. This document specifies the data-confidentiality constructions in Rootz Corp products (the Rootz Layer, Rootz Desktop, the Rootz web applications, Rootz SDK packages, Rootz Sealed Memory, the Rootz proof binder), in the forms in which Rootz distributes or delivers them to third parties:

Components that are not yet released are included and marked as such (Section 4.9).

A data-confidentiality construction is any procedure in that code that encrypts data, wraps (encrypts) a key, or derives a key that is then used for encryption or unwrapping. Appendix A lists each construction with the products and packages in which it occurs.

The document is written so that a competent cryptographer, using only this document and the cited standards, can build an implementation that decrypts data produced by Rootz software when given the key, and can produce data that Rootz software will decrypt.

1.2 Out of scope by function. Functions whose only purpose is authentication, digital signature or data integrity are not specified: ECDSA, EIP-191, EIP-712, EIP-1271, Ed25519, ML-DSA, RSA signatures, Merkle trees and document hashes. A signature or hash appears here only where its output is an input to key derivation, or where it is a field of an encrypted envelope that a consumer must parse or check; there it is specified only as far as interoperability requires.

1.3 Also out of scope:

1.4 What this document is not. It is a specification, not source code. It contains no production keys. All private keys, seeds and content keys shown are test material (repeated-byte patterns such as 0x1111…11). G.1 uses a generated RSA test key, of which only the public half is published. The one value that is not test material is the G.2 anchor A: it is a fixed protocol public key, embedded in the application, and is published here because consumers need it.

1.5 How statements were established. Unless marked otherwise, each byte-level statement was established by running the shipping implementation with test keys and opening its output with an independent implementation (Appendix B). A statement marked [code-read] was established by reading the source code and was not executed.

2. Composition principle

Every construction in this document is a composition of named, published, standard primitives (Section 3). Rootz defines no new cipher, hash function, key-encapsulation mechanism, key-agreement primitive, key-derivation function or mode of operation. Each entry specifies the composition: which primitive outputs feed which inputs, the exact byte encodings, the fixed labels and the wire format. Each entry carries a generic name, which describes the composition in standard terms, and a Composition of line, which cites the standards it composes.

Three recurring patterns are described once here:

3. Primitives and standards

Primitive Standard Used as
AES-256 FIPS 197 (2001, upd. 2023) Block cipher under GCM
AES-GCM, 128-bit tag NIST SP 800-38D All symmetric encryption in this document
secp256k1 elliptic curve SEC 2 v2.0 §2.4.1 ECDH key agreement; private keys
EC point encoding, EC Diffie-Hellman SEC 1 v2.0 §2.3.3–2.3.4 (encoding), §3.3.1 (ECDH) Shared-point computation; 65-byte / 33-byte public keys
ECIES structure SEC 1 v2.0 §5.1 Structure of the ephemeral wraps (deviations stated per entry)
NIST P-256 FIPS 186-5 with NIST SP 800-186 (curve) Sealed box (E.1); Secure Enclave key (G.2)
ECC CDH shared secret Z; C(0e, 2s) scheme NIST SP 800-56A Rev. 3 §5.7.1.2, §6.3 x-coordinate shared secret; structure of static-static wraps
Hybrid shared secret; two-step key derivation NIST SP 800-56C Rev. 2 §2 (Z′ = Z ‖ T), §5 (extraction-then-expansion) D.5 combiner and HKDF
KEM usage NIST SP 800-227 Use of an ML-KEM shared secret as KDF input (D.1, D.4, D.5)
X25519 RFC 7748 §5 Hybrid KEM classical half (D.3)
ML-KEM-1024 FIPS 203 Post-quantum key encapsulation
SHA-256, SHA-384 FIPS 180-4 KDFs, salts, chain hashes
SHA3-256 FIPS 202 §6.1 Hybrid KEM combiner (D.3)
Keccak-256 FIPS 202 §5.2: KECCAK[512](M, 256), i.e. the sponge of §4 over Keccak-p[1600, 24] (§3) with pad10*1 (§5.1), rate 1088 bits, without the SHA-3 domain suffix 01 KDFs. The first padding byte is 0x01; SHA3-256 (suffix 01, first padding byte 0x06) gives different output. This is the function Ethereum calls keccak256
HKDF RFC 5869 KDF (SHA-256 and SHA-384 variants)
HMAC FIPS 198-1 (= RFC 2104) HKDF; record naming

Referenced only where they feed key derivation or a parsed field (Section 1.2):

Primitive Standard
Deterministic ECDSA RFC 6979 (secp256k1, low-s normalisation)
Ethereum signed-message prefix EIP-191 version 0x45 ("personal_sign")
Ethereum address and checksum Ethereum Yellow Paper (address = last 20 bytes of Keccak-256 of the 64-byte public key X‖Y); EIP-55 (mixed-case checksum)
RSASSA-PKCS1-v1_5 RFC 8017 §8.2, with SHA-256
ML-DSA-65 FIPS 204 (pure mode, empty context string)
Shamir secret sharing A. Shamir, "How to share a secret", CACM 22(11), 1979; field GF(2^8) with reduction polynomial 0x11b as in FIPS 197 §4.2
Base64 RFC 4648 §4 (standard alphabet, with padding)
JSON RFC 8259; where bytes of a JSON text are hashed, MACed or used as AAD, the exact serialisation is specified in the entry
ISO 8601 date-time Seat timestamps (D.5)
Ethereum keystore Web3 Secret Storage Definition, version 3 (cited by reference only; see C.3)

4. Conventions

4.1 Notation. ‖ is byte concatenation. utf8(s) is the UTF-8 encoding of string s. hex(b) is lower-case hexadecimal with no prefix; hex0x(b) is "0x" ‖ hex(b). "0x-hex" in a wire format means hex0x. len(b) is a length in bytes. rand(n) is n bytes from a cryptographically secure random number generator. pub(d) is the public key of private key d.

4.2 AES-GCM. GCM-Enc(K, IV, P, A) returns (C, T) per SP 800-38D with a 32-byte key, a 16-byte tag T, and len(C) = len(P). A is empty unless the entry says otherwise. The IV is 12 bytes (96 bits) unless the entry says otherwise. Two tag placements occur, and each entry states which:

4.3 secp256k1 ECDH. For a private key d and a public key Q, let P = d·Q be the shared point (SEC 1 §3.3.1, step 1). Two different byte strings are derived from P, and they are not interchangeable:

Producers emit public keys as the 65-byte uncompressed encoding 0x04 ‖ X ‖ Y unless an entry states the 33-byte compressed form (0x02/0x03 ‖ X, SEC 1 §2.3.3). Where an entry permits compressed keys (A.5), a consumer decompresses them per SEC 1 §2.3.4; the shared point, and therefore Z65, is the same. Elsewhere, shipping consumers accept only the 65-byte form (for example A.1), so a producer must not emit a compressed key where the entry does not permit one.

4.4 The secp256k1 key-derivation families. A wrap made in one family does not open in another (verified by execution for the first three):

Family Key derivation Constructions
SHA-256 over the 65-byte point K = SHA-256(Z65(P)) A.1, A.2, the classical form of D.2
SHA-256 over the 32-byte x-coordinate K = SHA-256(X32(P)) A.3 (and, through HKDF rather than SHA-256, the classical half of D.5)
Keccak-256 over the 65-byte point K = Keccak-256(Z65(P)) A.4, A.5, A.6
Keccak-256 over the 32-byte x-coordinate K = Keccak-256(X32(P)) A.7 (legacy read path; pending frozen-vector verification)

For the same pair of test keys (0x11…11, 0x22…22):

X32  = 77e0510d5042e2f5e9e59c977b81eeed590cf7d20c1c51da451a8eaa9fdc45ff
Z65  = 04 77e0510d…dc45ff 5bc2344d3d5d8376b78519f5077117b8c34f19870cd4be70f6517cd91f573738
SHA-256(Z65) = 4c68aa69740926aab5ca201a53db5d5b6d7e2f586ab1b0439db47cebddc2f924
SHA-256(X32) = fd264454c8f37c9c4b000f0672399b9c76011de71f489fd8a043e25e558226f9

4.5 Plaintext encoding of wrapped keys. A 32-byte key is wrapped in one of three encodings, and each entry states which:

4.6 Hex parsing. Producers MUST emit well-formed lower-case hexadecimal. Consumers should reject malformed hex (odd length or non-hexadecimal characters). Not every shipping consumer checks this, so interoperability rests on producers emitting well-formed hex.

4.7 JSON. Field order shown in wire formats is the order the producer emits. Consumers parse JSON and do not depend on order, except where an entry states that exact JSON bytes are hashed, MACed or used as AAD.

4.8 Randomness in vectors. IVs, nonces, content keys and ephemeral keys are random at production time. The vectors record the values that were produced; they are valid known answers for the consume direction. Values that depend only on fixed test keys (derived keys, shared secrets, signatures from deterministic signers, public keys from seeds) are reproducible from the inputs alone. Each entry in the vectors file has an inputs object that states, for every sub-vector, the keys and the expected plaintext.

4.9 Statuses.


Part A — secp256k1 ECDH key wraps

A.1 rootz.wrap.ecdh-secp256k1-sha256.v1 — device and member key wrap

Generic name. ECDH(secp256k1) key wrap; key = SHA-256 over the uncompressed shared point; AES-256-GCM.

Status. current.

Composition of. secp256k1 ECDH (SEC 1 §3.3.1, SEC 2); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). Ephemeral forms: SEC 1 §5.1 ECIES structure; static member form: SP 800-56A §6.3 C(0e, 2s) structure. Deviations: the KDF is a single SHA-256 over Z65 (the whole point, not the x-coordinate) with no counter or shared info; GCM replaces the separate encryption and MAC steps.

Purpose. Deliver a 32-byte master key (or other 32-byte key) to one recipient device or team member, so that only the holder of the recipient's secp256k1 private key can recover it.

Inputs and encodings.

Produce.

1. Choose the sender key: (e, E) ← fresh secp256k1 key pair   [ephemeral forms]
                          or  (d_S, Q_S)                       [static member wrap]
2. P  = e·Q_R  (or d_S·Q_R)
3. K  = SHA-256( Z65(P) )                      # 32 bytes; no salt, no info
4. IV = rand(12)
5. (C, T) = GCM-Enc(K, IV, MK, A = empty)      # len(C) = 32, len(T) = 16
6. Emit the wire structure below (tag split).

Consume.

1. P  = d_R · E   (ephemeral forms: E = ephemeralPub)   or   d_R · Q_S (member wrap)
2. K  = SHA-256( Z65(P) )
3. MK = GCM-Dec(K, iv, ciphertext, A = empty, tag = authTag)   # fails closed on tag mismatch

Where the sender key is found.

Wire format. All values 0x-hex, lower case. Tag split.

Encrypted key object (member wrap, and inside every entry below):

{ "ciphertext": "0x…32 bytes…", "iv": "0x…12 bytes…", "authTag": "0x…16 bytes…" }

Device key entry (ephemeral form):

{ "deviceAddress": "0x…20-byte address…", "deviceName": "…",
  "ephemeralPub": "0x04…65 bytes…",
  "encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" } }

deviceAddress may be the empty string. deviceName is optional.

Package (one entry per device, each with its own fresh ephemeral key):

{ "type": "fat-key-package", "version": "3.0",
  "secretContract": "0x…", "keyVaultBlock": <int>, "secretName": "…", "chainId": <int>,
  "timestamp": <ms since epoch>, "devices": [ <device key entry>, … ] }

Single-device package (fields in producer order):

{ "type": "single-device-package", "version": "1.0",
  "secretContract": "0x…", "deviceAddress": "0x…",
  "ephemeralPub": "0x04…65 bytes…",
  "encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" },
  "algorithm": "ECDH-AES-256-GCM", "timestamp": <ms since epoch> }

The type also declares an optional encryptedKeyVaultKey (an encrypted key object under the same ephemeral key); current producers do not emit it.

Vault form: consumers of vault data also accept the four fields in one object, encryptedMasterKey: { ciphertext, iv, authTag, ephemeralPub }.

A second, different wire format also labelled fat-key-package 3.0 exists; see A.5. The two are not interchangeable and are told apart by their fields (A.5).

Known-answer vectors (SPEC-v1.2-vectors.json → rootz.wrap.ecdh-secp256k1-sha256.v1).

Member wrap (static-static), owner d_S = 0x11…11, member d_R = 0x22…22, Q_S = pub(0x11…11) (field owner_public_key):

MK          a5a5…a5 (32 bytes)
Z65         0477e0510d5042e2f5e9e59c977b81eeed590cf7d20c1c51da451a8eaa9fdc45ff
              5bc2344d3d5d8376b78519f5077117b8c34f19870cd4be70f6517cd91f573738
K           4c68aa69740926aab5ca201a53db5d5b6d7e2f586ab1b0439db47cebddc2f924
iv          b16c7bd0a547229e2673a79e
ciphertext  cce9b3419b6bc1e906fdd2dd18b150f608af60baca5eb0e3f1475bb9e86c7b7c
authTag     001d7836e5b6431dd7d0943ab9db97ac

Device wrap (ephemeral), recipient d_R = 0x22…22, deviceAddress = address of pub(0x22…22):

deviceAddress 0x1563915e194D8CfBA1943570603F7606A3115508
ephemeralPub  049516b4fa03baa622babbe9631372fb441d3c12155019d1f1387eb3fe7ee09a7f
                d61d08abde04931004cf9fd6a59f2fae0c2447f49f70ab9c5fbcfbb1ccb6e039
iv            016089113b57c96a5d8b664f
ciphertext    091de3795e4676de711fa74817822b2581156e905288c5fb99a72e8a7420c49d
authTag       37c8bb1f93daa710b1a2e61cc627b60d      → MK = a5a5…a5

The vectors file also holds a single-device package (recipient 0x22…22), a two-device package (recipients 0x22…22 and 0x23…23) and a reverse vector (independent producer, ephemeral key 0x31…31, IV 0a0a…0a, plaintext a5a5…a5).

Security properties.

Known limitations.

A.2 rootz.wrap.ecdh-secp256k1-sha256.team-invite.v1 — team-invite wrap

Generic name. ECDH(secp256k1) key wrap; key = SHA-256 over the uncompressed shared point; AES-256-GCM with appended tag.

Status. legacy-read-only.

Composition of. secp256k1 ECDH (SEC 1, SEC 2); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). Static-static, SP 800-56A §6.3 C(0e, 2s) structure, with the A.1 KDF deviation.

Purpose. Encrypt key material between the two parties of a team invitation.

Inputs and encodings.

Produce.

1. Z  = Z65( d_A · Q_B )             # = Z65( d_B · Q_A )
2. K  = SHA-256(Z)
3. IV = rand(12)
4. (C, T) = GCM-Enc(K, IV, plaintext_bytes, A = empty)
5. Emit { encrypted: hex0x(C ‖ T), iv: hex0x(IV), algorithm: "AES-256-GCM-ECDH" }   # tag appended

Consume. Recompute Z and K; split encrypted into C (all but the last 16 bytes) and T (last 16 bytes); GCM-decrypt.

Wire format.

{ "encrypted": "0x…C‖T…", "iv": "0x…12 bytes…", "algorithm": "AES-256-GCM-ECDH" }

Owner-self read path. The web dashboard also reads { "encrypted", "iv" } objects (same KDF, tag appended, algorithm optional) in which an owner wrapped a key to itself, i.e. Z = Z65(d · pub(d)). [code-read]

Known-answer vector. d_A = 0x11…11, d_B = 0x22…22, plaintext a5a5…a5 (32 bytes).

Z65        (identical to A.1 Z65)
K          4c68aa69740926aab5ca201a53db5d5b6d7e2f586ab1b0439db47cebddc2f924
iv         13cb0e3fde3214ae71a8dd1a
encrypted  09b64c3eaa3e074a07b75fc324aefa9dfda183ca7737b6e2c260a5868d4b24bf
           f926ea66c8710079e98f82167b6a3b2a        (32 + 16 bytes)

Known limitations. As A.1: no salt, info or AAD; a fixed key per pair of parties, with IV uniqueness resting on random 96-bit IVs. The envelope differs from A.1 (tag appended, different field names), so the two wire formats are not interchangeable although the key is the same.

A.3 epistery.wrap.ecdh-secp256k1-x-sha256.peer-cipher.v1 — peer-cipher wrap and content

Generic name. Static-static ECDH(secp256k1) key wrap; key = SHA-256 over the x-coordinate; AES-256-GCM. AES-256-GCM content encryption.

Status. current (pre-release). Dependency: epistery. The construction belongs to the Epistery library, a Rootz Corp project. It is specified here because Rootz Sealed Memory uses it (D.5 classical seats, D.6 content layout). Other Epistery constructions will be specified in version 1.3.

Composition of. secp256k1 ECDH with the SEC 1 / SP 800-56A x-coordinate shared secret; SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). SP 800-56A §6.3 C(0e, 2s) structure. Deviation: the KDF is a single SHA-256 over X32 with no counter or shared info.

Purpose. (a) Wrap a 32-byte content key from one identity key to another; (b) encrypt text content under a 32-byte content key.

Inputs and encodings.

Produce — key wrap.

1. X      = X32( d_wrapper · Q_peer )        # static-static; no ephemeral key
2. K_pair = SHA-256(X)
3. IV     = rand(12)
4. (C, T) = GCM-Enc(K_pair, IV, K_content, A = empty)
5. Emit { ciphertext: hex0x(C), iv: hex0x(IV), tag: hex0x(T) }        # tag split

Produce — content.

1. IV = rand(12)
2. (C, T) = GCM-Enc(K_content, IV, utf8(text), A = empty)
3. Emit { iv: hex0x(IV), ciphertext: hex0x(C ‖ T) }                   # tag appended

Consume. Wrap: X = X32(d_peer · Q_wrapper), K_pair = SHA-256(X), GCM-decrypt with the split tag. Content: GCM-decrypt with the appended tag; decode UTF-8.

Wire format. 0x-hex, lower case.

wrap:    { "ciphertext": "0x…32 bytes…", "iv": "0x…12…", "tag": "0x…16…" }
content: { "iv": "0x…12…", "ciphertext": "0x…C‖T…" }

When this wrap is carried in a seat (D.5), the object also has "alg": "epistery/peer-cipher:secp256k1-ecdh".

Known-answer vectors. Epistery's own frozen vectors of 2026-09-30, opened independently.

wrapper public key  044f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
                      385b6b1b8ead809ca67454d9683fcf2ba03456d6fe2c4abe2b07f0fbdbb2f1c1  (= pub(0x11…11))
peer private key    0x22…22
K_pair = SHA-256(X) fd264454c8f37c9c4b000f0672399b9c76011de71f489fd8a043e25e558226f9
wrap  ciphertext    30b7c8bb9b7e56249ebbe0fa4022c1b9279443ef9afcf3a90f2f5e289fa1227c
      iv            84705ece6ea5ee22d5319248
      tag           fbe54e1be5468c1cf40360b90436d69c        → K_content = abab…ab (32 bytes)
content iv          ea00c799d12f1db94af4e0ea
content ciphertext  0b77d07111badc4de47cda1a0f9e2f604436a1ec161f77da674d4b8ffbae0672
                    5354406c9ad773a6de09bc63665ef2f075f8fdd51995e12fa87e18745b7fe3b9
                    f68b3f52f5573313da7aeb
                    → "frozen vector — the existing construction, 2026-09-30 ✓" (UTF-8)

The vectors file also holds fresh_wrap, made by the shipping producer with the same key pair (wrapper 0x11…11, peer 0x22…22) and the same content key abab…ab.

Known limitations.

A.4 rootz.wrap.ecdh-secp256k1-keccak256.teams.v1 — team key share

Generic name. ECDH(secp256k1) key wrap; key = Keccak-256 over the uncompressed shared point; AES-256-GCM with appended tag; key-text plaintext.

Status. current.

Composition of. secp256k1 ECDH (SEC 1, SEC 2); Keccak-256 (FIPS 202 §5.2 KECCAK[512], see Section 3); AES-256-GCM (FIPS 197, SP 800-38D). SEC 1 §5.1 ECIES structure when the sender key is ephemeral. Deviations: single Keccak-256 over Z65 with no counter or shared info; GCM in place of encryption and MAC.

Purpose. Deliver a team secret's master key and vault key to a team member or device.

Inputs and encodings.

Produce.

1. P  = d_sender · Q_member
2. K  = Keccak-256( Z65(P) )
3. IV = rand(12)
4. (C, T) = GCM-Enc(K, IV, utf8(keyText), A = empty)      # normally 66-byte plaintext
5. Emit the JSON text { "encrypted": hex0x(C ‖ T), "iv": hex0x(IV) }   # tag appended

Consume. P = d_member · Q_sender; K = Keccak-256(Z65(P)); GCM-decrypt (appended tag); the plaintext is the key text, returned unchanged.

Wire format. The wrapped-key object:

{"encrypted":"0x…(66+16 bytes)…","iv":"0x…12 bytes…"}

Enclosing messages. The wrapped-key layout occurs in three messages. Only the fields this specification needs are shown; the messages carry further non-cryptographic fields.

Known-answer vectors. Member d = 0x22…22, sender d = 0x33…33, plaintext utf8("0xa5…a5").

sender public key 043c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1
                    3b306b0fe085665d8fc1b28ae1676cd3ad6e08eaeda225fe38d0da4de55703e0
Z65   049110f8760a37d96052e3dcaf14862a147654f49f722cf213568ccef1eca2ec71
        34ebc7978f91470a8abcdd13332db0de3261055c06ac8ff92b75b272463c6adc
K     daf9383c748ed84aa4aaa757036539fac3c4b3c8ceca6b7fccc8e986da268205
package {"encrypted":"0xab8d33d89bfc33231278a08bbf28ba8ed3a170ef2ebcb0bb0dba7aaee1f9d86588e0
        ee775fe5af75b83e4773fa7f666e6bc854129efedcacfb6cc43487e6cc88622dcde6c97858d093fff43eb6
        2a6872d59f","iv":"0x0d1c2c29bc316f46ce82507d"}
        → plaintext "0xa5a5…a5" (66 ASCII bytes)

The vectors file also holds: a reverse package (independent producer, same sender 0x33…33); a key-delivery message and a device-sync package (sender 0x36…36, plaintexts "0xa5…a5" for the master key and "0x77…77" for the vault key); and a direct-keys 4.0 message and a direct-keys 6.0 message (recipient 0x22…22, random sender keys as recorded, plaintext "0xa5…a5"). The 6.0 message was produced end to end by the shipping SDK send path, with the transaction captured and not sent.

Known limitations.

A.5 rootz.wrap.ecdh-secp256k1-keccak256.identity.v1 — multi-device identity package

Generic name. ECDH(secp256k1) multi-recipient key wrap; key = Keccak-256 over the uncompressed shared point; AES-256-GCM.

Status. current.

Composition of. secp256k1 ECDH (SEC 1, SEC 2); Keccak-256 (FIPS 202 §5.2 KECCAK[512]); AES-256-GCM (FIPS 197, SP 800-38D). SEC 1 §5.1 ECIES structure, one ephemeral key per recipient. Deviations as A.4.

Purpose. Deliver an identity's master key and vault key to each of its authorised devices.

Inputs and encodings.

Produce (package, per device).

1. (e, E) ← fresh secp256k1 key pair
2. K  = Keccak-256( Z65(e · Q_device) )
3. (C1,T1) = GCM-Enc(K, IV1 = rand(12), MK,  A = empty)
4. (C2,T2) = GCM-Enc(K, IV2 = rand(12), KVK, A = empty)        # same K, second IV
5. Emit the device entry with ephemeralPublicKey = compressed(E)   # tag split

Consume. K = Keccak-256(Z65(d_device · E)); GCM-decrypt each object (split tag).

Wire format. 0x-hex.

{ "type": "fat-key-package", "version": "3.0",
  "secretContract": "0x…", "identityContract": "0x…", "secretName": "…",
  "devices": [ { "deviceAddress": "0x…", "deviceName": "…",
                 "ephemeralPublicKey": "0x02…|0x03…  (33 bytes)",
                 "encryptedMasterKey":   { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" },
                 "encryptedKeyVaultKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" } } ],
  "createdAt": <ms since epoch> }

Single wrap: { "ciphertext", "iv", "authTag" }.

Distinguishing A.5 from the A.1 package. Both are labelled fat-key-package / 3.0. They differ in the KDF (Keccak-256 vs SHA-256), the ephemeral-key field name (ephemeralPublicKey vs ephemeralPub), its encoding (33-byte compressed vs 65-byte uncompressed), the timestamp field (createdAt vs timestamp) and the vault-key field. A consumer tells them apart by these fields; the type/version labels do not distinguish them.

Known-answer vectors. Device d = 0x22…22, sender d = 0x33…33 (single wrap):

Z65, K      identical to A.4 (same key pair)
K           daf9383c748ed84aa4aaa757036539fac3c4b3c8ceca6b7fccc8e986da268205
iv          ab8dcb319ba2a5efb935d60c
ciphertext  498218e26a2781c7aa8201f98fc057e77cb04a9e086a427e88b8ad04e5016fc4
authTag     394e9b73059ecde62aa9283d4d8017fa      → MK = a5a5…a5 (raw)

Package entry, device d = 0x22…22:

ephemeralPublicKey   02590f2df040946f6c840ee23d630471f7ebe8382599507248a8deb9931ca9bd69
encryptedMasterKey   ct a014faca6a7f7b22819b068d25dae7f0bf2c36407e22f1f4d3d2cdbc56a0d618
                     iv f94fba9db8f0b5f9b9e333ce   tag 3b8564437df0930edbe7d4dd8ebf04fb   → a5a5…a5
encryptedKeyVaultKey ct 187d80caf1dbf4f5c25a56c3736764d63046ada7c654ccaf943f4524c62c98c7
                     iv 20f495754da352df0807d45b   tag c43841d4bba0f99f0bd1ef8efc1701a6   → 7777…77

The reverse vector (independent producer, ephemeral 0x34…34, plaintext a5a5…a5) carries a 65-byte uncompressed ephemeral key, which the shipping consumer accepts.

Known limitations.

A.6 rootz.wrap.ecdh-secp256k1-keccak256.identity-device-key.v1 — identity device key package

Generic name. ECDH(secp256k1) key wrap; key = Keccak-256 over the uncompressed shared point; AES-256-GCM with appended tag; raw plaintext.

Status. exported-unused.

Composition of. secp256k1 ECDH (SEC 1, SEC 2); Keccak-256 (FIPS 202 §5.2 KECCAK[512]); AES-256-GCM (FIPS 197, SP 800-38D). Static-static or ECIES structure depending on the sender key the caller supplies; deviations as A.4.

Purpose. Encrypt a master key for one device of an identity, and for every device of an identity that has a registered public key.

Inputs and encodings. KDF K = Keccak-256(Z65(P)) (the same key as A.4 and A.5 for the same pair of keys). Plaintext: the 32 key bytes, raw. Sender key: supplied by the caller; its 65-byte uncompressed public key is carried in the package.

Produce.

1. P  = d_sender · Q_recipient
2. K  = Keccak-256( Z65(P) )
3. IV = rand(12);  (C, T) = GCM-Enc(K, IV, MK, A = empty)
4. Emit { recipientAddress, encryptedKey: hex0x(C ‖ T), iv: hex0x(IV), senderPublicKey: hex0x(Q_sender) }

Consume. K = Keccak-256(Z65(d_recipient · senderPublicKey)); split the last 16 bytes of encryptedKey as the tag; GCM-decrypt; the result is the raw key.

Wire format.

{ "recipientAddress": "0x…", "encryptedKey": "0x…(32+16 bytes)…", "iv": "0x…12…",
  "senderPublicKey": "0x04…65 bytes…" }

The multi-device form is an array of these objects, one per device.

Known-answer vector. Recipient d = 0x22…22, sender d = 0x33…33, MK = a5a5…a5.

K                daf9383c748ed84aa4aaa757036539fac3c4b3c8ceca6b7fccc8e986da268205   (= A.4 K)
recipientAddress 0x1563915e194D8CfBA1943570603F7606A3115508
iv               ff967359fe6f13e8ec7d0625
encryptedKey     c90217fd1d85867c17c47a840e3840cba6b777f5d551f94b08c200b3bd627ac0
                 bfde97dc54be87c725144577527f65c3        → MK = a5a5…a5 (raw)
senderPublicKey  = pub(0x33…33) (see A.4)

Known limitations. No salt, info or AAD; recipientAddress is not authenticated. Where the sender uses a static key, K is fixed for the pair and safety rests on random IVs.

A.7 rootz.wrap.ecdh-secp256k1-keccak256-x.legacy.v5-2 — legacy Keccak-256(X32) family

Generic name. ECDH(secp256k1) key wrap; key = Keccak-256 over the x-coordinate; AES-256-GCM.

Status. legacy-read-only. (A frozen vector from an existing record is pending; see below.)

Composition of. secp256k1 ECDH with the x-coordinate shared secret (SEC 1, SP 800-56A); Keccak-256 (FIPS 202 §5.2 KECCAK[512]); AES-256-GCM (FIPS 197, SP 800-38D).

Description. Records written by an earlier generation of Rootz software may have derived the wrap key as K = Keccak-256(X32(P)), where an earlier version of the elliptic-curve library returned the 32-byte x-coordinate as the shared secret. The envelopes are expected to be those of A.4 and A.5. This identifier is reserved so that such records can be described exactly. The formula, envelopes and a known-answer vector will be stated in a revision of this document once a frozen vector from an existing record has been verified. Until then this entry makes no byte-level claim beyond the formula above.


Part B — Signature-derived storage keys

These constructions derive a symmetric key from a deterministic signature over a fixed message. They differ in the message, the address case, and what is hashed: the signature's hex text (B.1) or its raw bytes (B.2).

Common signature procedure: EIP-191 personal_sign over utf8(message), i.e. ECDSA on secp256k1 over Keccak-256(utf8("\x19Ethereum Signed Message:\n") ‖ utf8(decimal(len(utf8(message)))) ‖ utf8(message)), where the length is the byte length of the UTF-8 encoding of the message, written in decimal ASCII. The nonce is RFC 6979 deterministic, s is normalised to the low half of the group order, and the result is 65 bytes r(32) ‖ s(32) ‖ v(1) with v ∈ {27, 28}.

B.1 rootz.wrap.sig-storage-key.sha256-hextext.v1 — owner at-rest master key

Generic name. Key derived by SHA-256 over the hex text of a deterministic ECDSA(secp256k1) signature; AES-256-GCM with appended tag.

Status. current.

Composition of. Deterministic ECDSA on secp256k1 (RFC 6979, EIP-191 framing; used as secret KDF input); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D).

Purpose. Store a secret's master key encrypted for its owner, recoverable by any device that can produce the owner's signature.

Inputs and encodings.

Produce.

1. msg = "Storage key for master key encryption\nOwner: " ‖ EIP55(addr) ‖ "\nPurpose: encrypt-master-key"
2. sig = EIP191_Sign_RFC6979(d_owner, msg)                    # 65 bytes, v = 27/28
3. K   = SHA-256( utf8( hex0x(sig) ) )
4. (C, T) = GCM-Enc(K, IV = rand(12), utf8(keyText), A = empty)
5. Emit the package with encryptedKey = hex0x(C ‖ T)          # tag appended

Consume. Re-sign the same message with the owner key, derive K, GCM-decrypt, return the text.

Wire format.

{ "version": "1.0.0", "algorithm": "AES-256-GCM", "keyDerivation": "signature-storage-key",
  "encryptedKey": "0x…(66+16 bytes)…", "iv": "0x…12…",
  "encryptedFor": "<EIP-55 address>", "timestamp": <ms>,
  "extensions": { … application metadata … } }

Known-answer vector. Owner d = 0x11…11.

address    0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A
message    "Storage key for master key encryption\nOwner: 0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A\nPurpose: encrypt-master-key"
signature  0xdab7809ff42444ec19ed311495bb121a99c80f12eacf70ea127aa68d7b3a910e
             16fd54ecc2cd66e23daced6d362aad1d6da9b6896573b0dc8d2b98dd5505972c1b
K          69a5936f246239dd0fccf86a14b2f54ffc467ec1d76023242229836acdc5782c
iv         317588ca25d8926d97a7aea3
encryptedKey db875186f97a7233a0a92f7984f375cfc3941daa0de5e951bfaf5533655f3a7d
             0870523fdc02885427576b170241a02425647b2f335b40daf51a61a010aed3d7
             77bb6e44fb81428236da9c9508c8dbab53cf          → "0xa5a5…a5" (66 ASCII bytes)

Security properties. The key is exactly as strong as the secrecy of the owner's signing key.

Known limitations.

B.2 rootz.wrap.sig-storage-key.sha256-rawsig.hardware-keys.v1 — device key at rest under a hardware-wallet signature

Generic name. Key derived by SHA-256 over the raw bytes of a deterministic ECDSA(secp256k1) signature; AES-256-GCM.

Status. current.

Composition of. Deterministic ECDSA on secp256k1 (RFC 6979, EIP-191 framing; used as secret KDF input); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D).

Purpose. Keep a device's secp256k1 ECDH private key encrypted at rest, unlockable only by a signature from the user's hardware wallet.

Inputs and encodings.

Produce.

1. (d_dev, Q_dev) ← fresh secp256k1 key pair
2. sig = EIP191_Sign_RFC6979(d_wallet, msg)
3. K   = SHA-256(sig)                                           # 65 bytes in
4. (C, T) = GCM-Enc(K, IV = rand(12), d_dev, A = empty)
5. Emit the record (tag split)

Consume. Have the wallet sign signatureMessage; K = SHA-256(sig); GCM-decrypt; optionally check that d_dev yields publicKey.

Wire format.

Local record:

{ "version": "1.0", "publicKey": "0x04…(device ECDH public key, 65 bytes)…",
  "deviceAddress": "<EIP-55 address of the hardware WALLET>",
  "encryptedPrivateKey": { "ciphertext": "0x…32…", "iv": "0x…12…", "authTag": "0x…16…" },
  "signatureMessage": "<the local message>", "createdAt": <ms> }

On-chain record: the bytes utf8("ROOTZ_DEVICE_KEY") ‖ utf8(JSON), where JSON is

{ "version": "1.0", "type": "rootz-device-key",
  "walletAddress": "<EIP-55 wallet address>",
  "publicKey": "0x04…65 bytes…",
  "encryptedPrivateKey": { "ciphertext": "0x…32…", "iv": "0x…12…", "authTag": "0x…16…" },
  "signatureMessage": "<the on-chain message>", "createdAt": <ms>, "chainId": <int> }

Consumers also accept the JSON without the prefix.

Known-answer vectors. Wallet d = 0x11…11.

message    "Rootz Device Key Encryption\n\nThis signature unlocks your encryption keys.\nOnly sign this on trusted devices.\n\nWallet: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a\nDomain: rootz.global\nVersion: 1"
signature  0x73d739205f5db0409d356827c1d9756ff708c643a91cf8854109e1c6cf8ac680
             5aae46f9552367de901e5a1a71e2456bcac8bf996af3a572e3b4f46f7d30c5281b
K          7a1c4acc75fa798ecfbdd4e0fe0d407158fb0b59b9dd28165a94155d8a861a36
publicKey  047c4af3d5a93a67a370579aedbeaf5b473ef9f5836c6a5bc15ef66e2f4315c4e1
             26915615c521f92fb5e787f68941c3278aaa728b5dce90b5302924464be187ac
iv         a4c9765e4af0a4fb25d08b3d
ciphertext 30920e94c3c80aca8a277cab47b4db6a5b4adbd4d49d03b89f93cb0101a91cda
authTag    a60a105d651ee49cd5f25a3cb5bb6996     → d_dev with pub(d_dev) = publicKey

The vectors file also holds a reverse vector (independent producer, device key 0x66…66) and an on-chain record for chain 137 produced by the shipping code (field onchain_record).

Security properties. As B.1: depends on a deterministic signer and on the secrecy of the wallet key.

Known limitations. One K per wallet and message; no AAD. The unlocked device key is the recipient key for Part A wraps, so its compromise exposes every key wrapped to that device.


Part C — Content encryption

C.1 rootz.content.aes-256-gcm.v1 — content, bytes and content-identifier encryption

Generic name. AES-256-GCM content encryption, appended tag.

Status. current.

Composition of. AES-256-GCM (FIPS 197, SP 800-38D); SHA-256 (FIPS 180-4) for the optional string-to-key helper.

Purpose. Encrypt secret content, raw bytes, and IPFS content identifiers under a 32-byte key (normally the secret's master key).

Inputs and encodings. Key: 32 bytes (given as 0x-hex). Text: utf8(text). IPFS CID: utf8(cid) of its string form. Optional helper: a key derived from a string is SHA-256(utf8(s)).

Produce.

IV = rand(12);  (C, T) = GCM-Enc(K, IV, plaintext, A = empty);  field = hex0x(C ‖ T)   # tag appended

Consume. Split the last 16 bytes of the field as the tag; GCM-decrypt.

Wire format.

text:      { "encrypted": "0x…C‖T…", "iv": "0x…12…", "algorithm": "AES-256-GCM" }
IPFS CID:  { "encrypted": "0x…C‖T…", "iv": "0x…12…", "version": "5.0" }

(Byte form: the same C ‖ T and IV as byte arrays.)

Known-answer vectors. Key 4242…42 (32 bytes).

text  "Rootz KAT — content ✓"
      iv        74d97336f93481bcb771532e
      encrypted e270efe211cfc9e10bd454e8f521f6ffcf980c696404fcc2efbe6853a5c61387874fff9b2176601b0c
bytes 00010203feff
      iv        624236b33ad8612a4c5afbcc
      encrypted 1d9a9cb383c4b0567fa7b6de622d90069911d418edeb
CID   "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"
      iv        8924c3cc1fa04f652e333445
      encrypted ca660f12fddfe0b508a93e6aa1b199944e2c51bc95642d4812f202b5635114ae
                32bd39fc3d8593359d1a4b7c557ff13424fc5cbe55c06c640af6902677e4

Security properties. Confidentiality and integrity of each ciphertext under GCM.

Known limitations. No AAD: a ciphertext is not bound to the record, contract or position it belongs to. Many ciphertexts share one key; IV uniqueness rests on random 96-bit IVs. The string-to-key helper is a single unsalted hash, suitable only for high-entropy strings.

C.2 rootz.content.note.kvkey-aes-256-gcm.v1 — notes, files and the vault key

Generic name. AES-256-GCM note, file and key encryption under a vault key.

Status. current.

Composition of. AES-256-GCM (FIPS 197, SP 800-38D), applied as in C.1.

Purpose. Encrypt notes written to a vault, the files they reference, and the vault key itself.

Inputs and encodings.

Produce.

KVK wrap:  (C,T) = GCM-Enc(MK,  rand(12), utf8(hex0x(KVK)))   → {encrypted: hex0x(C‖T), iv, algorithm:"AES-256-GCM"}
Each file: ipfsRef = C.1-CID-encrypt(KVK, cid);  content: GCM-Enc(KVK, rand(12), utf8(base64(bytes)))
Note:      IV = rand(12); (C,T) = GCM-Enc(KVK, IV, utf8(JSON(payload)))
On-chain:  bytes = IV(12) ‖ C ‖ T(16)

Consume. KVK: GCM-decrypt under MK, parse the 66-character hex. Note: take bytes [0:12] as IV and the rest as C ‖ T; GCM-decrypt under KVK; parse JSON; decrypt each ipfsRef under KVK. File: GCM-decrypt, then base64-decode.

Wire format. KVK wrap: as C.1 text form. Note on-chain: raw bytes IV ‖ C ‖ T.

Known-answer vectors. MK = a5a5…a5, KVK = 7777…77.

KVK wrap   iv 611f9f2221b56435a3b408be
           encrypted f848371c9c97ff42024dc852f457b8ad15a13f0141a11519817c684ae78b92d1
                     5e675107c5856fe8279ef7a3e187d38458eb90a6b427f21029e2b771a3c2c01f
                     166ef67a31e90d5d101a6224c74679685f54      → "0x7777…77" (66 ASCII bytes)
file       bytes 00ff0a0dc8;  iv 54effd3d6218d802196ef8bd
           encrypted 37d995fc7e807d1700c3135682a4f33375aec5b41a5a344d   → "AP8KDcg=" (base64 text)
note       iv 586053f184ae7b99fce1ed76; full on-chain bytes in the vectors file
           → {"version":"1.0.0","content":"hello note","author":"0xabcdef0000000000000000000000000000000001",
              "timestamp":1792149128272,"files":[{"ipfsRef":{"encrypted":"0xcf0d…9072","iv":"0x072d349d628f876745558cb6",
              "version":"5.0"},"filename":"a.txt","contentType":"text/plain","size":3,"uploadedAt":1792149128272}],
              "noteVersion":"2","previousNote":7,"metadata":{"k":"v"}}

Known limitations. As C.1. No AAD anywhere: a note ciphertext is not bound to its contract, vault block or note index. One KVK encrypts every note, CID and file in a vault; IV uniqueness rests on random IVs. Base64 encoding before encryption expands file ciphertexts by about one third.

C.3 rootz.kdf.kvkey-keccak256-mktext.v1 — vault key derived from the master-key text

Generic name. Vault key derived by Keccak-256 over the master-key text.

Status. current (pre-release).

Composition of. Keccak-256 (FIPS 202 §5.2 KECCAK[512]); the derived key is used with C.1/C.2 (AES-256-GCM, FIPS 197, SP 800-38D).

Purpose. Provide a vault key for a secret whose vault data carries no wrapped vault key (encryptedKVKey absent), in the agent runtime.

Formula.

KVK = Keccak-256( utf8(MKtext) )

MKtext is the master key as text, exactly as recovered by the A.1 unwrap: "0x" followed by 64 lower-case hex digits (66 bytes). The 32-byte KVK is then used as the vault key of C.2.

Where the vault data does carry a wrapped vault key, the vault key is obtained by the C.2 KVK unwrap instead. For vault data without a wrapped vault key, other Rootz components use the master-key bytes themselves as the vault key (C.2); the two keys differ, so data written under one is not readable under the other. [code-read for where each rule applies; the formula was evaluated with the same library function the agent runtime calls]

Known-answer vector.

MKtext   "0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5"
KVK      87271b7931cbfe875457a579e08c4d846c0ec6205aa38036ed928a8e85710d7f
sample   C.1 text form under KVK: iv f86c1e0390c234a6831cf6c5,
         encrypted dacab210d448b8ed49076e5e6be11f64e3bce2ff41b4a3f2ccc30b57b6  → "agent-tee KAT"

Related standard format. The agent runtime also reads its own signing key from an Ethereum keystore file in the standard Web3 Secret Storage format (version 3), through a library. That format is cited by reference and is not a Rootz construction; it is not re-specified here.

Known limitations. The vault key is a deterministic, unsalted function of the master key; it provides no separation between master-key and vault-key holders.


Part D — Post-quantum and hybrid constructions

D.1 rootz.envelope.pq.mlkem1024-hkdfsha384-aesgcm.v2 — post-quantum envelope v2

Generic name. Multi-recipient ML-KEM-1024 envelope: HKDF-SHA-384 key-wrap key, AES-256-GCM DEK wrap, AES-256-GCM body with the header as AAD.

Status. exported-unused.

Composition of. ML-KEM-1024 (FIPS 203), used as SP 800-227 describes (the KEM shared secret is input to a KDF and the derived key protects a data key); HKDF-SHA-384 (RFC 5869, FIPS 198-1, FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). A header signature uses ML-DSA-65 (FIPS 204); it is an integrity function, described only for interoperability.

Purpose. Encrypt a payload once, under a random data-encryption key (DEK), for one or more recipients holding ML-KEM-1024 keys.

Inputs and encodings.

Produce.

1. DEK = rand(32);  n = rand(12)                                   # body nonce
2. For each recipient i:
     (ct_i, ss_i) = ML-KEM-1024.Encaps(pk_i)
     info_i = utf8("rootz-dek-wrap-v2|" ‖ recipientId_i ‖ "|" ‖ senderId) ‖ pk_i      # pk_i raw, 1568 bytes
     WK_i   = HKDF-SHA384(IKM = ss_i, salt = SHA-384(ct_i), info = info_i, L = 32)
     w_i    = rand(12);  (Cw, Tw) = GCM-Enc(WK_i, w_i, DEK, A = empty)
     wrappedDek_i = w_i ‖ Cw ‖ Tw                                  # 60 bytes
3. canonical = the exact UTF-8 bytes of the JSON text
     {"alg":A,"mv":MV,"n":N,"r":[{"alg":RA_1},…,{"alg":RA_k}],"rwb":RWB,"v":V}
   with keys in this order and no whitespace, where
     A    = the envelope "algorithm" (JSON string)
     MV   = the envelope "minVersion" (JSON number)
     N    = hex(n), lower case (JSON string)
     RA_i = recipient i's "algorithm" (JSON string); one object per recipient and no other field
     RWB  = the reWrapBy string (JSON string; "" when there is none)
     V    = the envelope "version" (JSON number)
   Strings are escaped as ECMAScript JSON.stringify does.
4. (C, T) = GCM-Enc(DEK, n, plaintext, A = canonical)
5. headerSignature = ML-DSA-65.Sign(sk_sender, canonical ‖ T)      # pure, empty context
6. Emit the JSON below

Producers always emit reWrapBy, as a JSON string, defaulting to "".

Consume.

1. Rebuild canonical from the received fields exactly as in Produce step 3, applying the rules:
     - reWrapBy absent, or null, false or 0  →  RWB = ""
     - N = hex(unhex(nonce)), re-encoded in lower case
     - V and MV are the received "version" and "minVersion", parsed as JSON numbers and written
       as ECMAScript JSON.stringify writes a Number: an integer in decimal, with no sign, fraction,
       exponent or leading zeros (a received 2.0 is written 2). Producers emit integers.
2. Reject if version < 2.
3. Verify headerSignature over canonical ‖ unhex(tag) with the sender's ML-DSA-65 public key.
   Verification is mandatory and precedes decryption: on failure, reject.
4. Find the own recipient entry by id; ss = ML-KEM-1024.Decaps(sk, unhex(encapsulation))
5. WK  = HKDF-SHA384(ss, salt = SHA-384(unhex(encapsulation)), info as above with own id, senderId, own pk)
6. DEK = GCM-Dec(WK, wrappedDek[0:12], wrappedDek[12:44], tag = wrappedDek[44:60])
7. plaintext = GCM-Dec(DEK, unhex(nonce), unhex(ciphertext), A = canonical, tag = unhex(tag))

Wire format. JSON; all binary values lower-case hex without 0x.

{ "version": 2, "minVersion": 2, "algorithm": "ML-KEM-1024+AES-256-GCM",
  "nonce": "<hex 12>", "reWrapBy": "<string>",
  "recipients": [ { "id": "<recipientId>", "epoch": <int>, "algorithm": "ML-KEM-1024",
                    "encapsulation": "<hex 1568>", "wrappedDek": "<hex 60>" } ],
  "headerSignature": "<hex 3309>", "ciphertext": "<hex>", "tag": "<hex 16>" }

Known-answer vector. KEM seed d ‖ z = 4444…44 (64 bytes); sender signing master seed 5555…55 (the ML-DSA-65 key pair is ML-DSA.KeyGen_internal(ξ) of FIPS 204 (Algorithm 6, ML-DSA-65 parameters), with ξ = HKDF-SHA384(IKM = seed, salt = empty, info = utf8("rootz-signing-v2"), L = 32)); recipientId = "device-A", senderId = "sender-1", epoch = 3, reWrapBy = "2030-01-01", plaintext "PQ envelope KAT plaintext". The vectors file carries sender_id, recipient_id, the full recipient ML-KEM public key, the full sender ML-DSA public key and the exact canonical header.

SHA-256(recipient ML-KEM pk)  882f9299da0551ca9d8929fc95c222b1fa137c2447d9e1779d64ea953e950439
SHA-256(sender ML-DSA pk)     d7d75b6db43bbc00f218a2ebdd39a19fc468a4d723f5024a835af7807f41efec
canonical  {"alg":"ML-KEM-1024+AES-256-GCM","mv":2,"n":"60fc4155966c780116743907","r":[{"alg":"ML-KEM-1024"}],"rwb":"2030-01-01","v":2}
ss         520287900fe65fc5e8be5fc770f5b70e57908d2d7e9e303ab1c1ae75474f71be
WK         303cc78db3d713b3713d6ed9060c0e046b5656d6ee836bb795b8a129ce6fe51d
wrappedDek e8543c757347bfda37e4f37b8ef8bf6c813db77668a8288fb2590e8135fd01dc
           3892ca3546d94613d8f0d61ff5b965a737bb0b95c48285c0bc380ec8
DEK        7d4da5270755c7de84da3b4ee38f78ce1163bdb83974b026cad0e16fab689fbe
nonce      60fc4155966c780116743907
ciphertext 6ce96982994f75ed44d36a0e835b7d218b4cfd64bafa8c5ddc
tag        1b7899c7831f906767ff5619d0ee25cb

Interoperability note. info is 1,603 bytes for one recipient. Some HKDF interfaces cap info at 1,024 bytes; an implementation on such an interface computes RFC 5869 directly from HMAC-SHA-384.

Security properties. Payload confidentiality under ML-KEM-1024 and AES-256-GCM. The wrap key binds the ML-KEM ciphertext (salt), the recipient's public key and both identifiers (info). The header signature and the AAD cover the algorithm, versions, nonce, reWrapBy and the number of recipient entries with their algorithm labels.

Known limitations.

D.2 rootz.envelope.agile.v1 — algorithm-tagged envelope

Generic name. Algorithm-tagged envelope. Classical form: ephemeral ECDH(secp256k1)/SHA-256 wrap of a content key, with AES-256-GCM content encryption. Post-quantum form: D.1.

Status. exported-unused.

Composition of. Classical form: secp256k1 ECDH, SHA-256 and AES-256-GCM exactly as the A.1 ephemeral form (SEC 1 §5.1 structure, A.1 deviations), with AES-256-GCM (FIPS 197, SP 800-38D) for the content. Post-quantum form: D.1.

Purpose. One sealing interface whose output states its own algorithm, so that a consumer can dispatch.

Algorithm identifiers (complete list). "ECDH-AES-256-GCM" (version 1, classical, default) and "ML-KEM-1024" (version 2, produces a D.1 envelope). Any other identifier is rejected at seal time. ("ML-KEM-1024+X25519" is declared but has no seal or open route.)

Produce (classical).

1. CEK = rand(32);  IV = rand(12)
2. (C, T) = GCM-Enc(CEK, IV, plaintext, A = empty)
3. wrap = A.1 ephemeral device entry wrapping CEK (raw) to the recipient's secp256k1 key
4. Emit { v: 1, alg: "ECDH-AES-256-GCM", ct: base64(C ‖ T), iv: base64(IV), wrap }

Consume (dispatch).

1. If the JSON has a numeric top-level "version" ≥ 2 → open as D.1.
2. Else if alg == "ECDH-AES-256-GCM" → unwrap CEK per A.1 from wrap.ephemeralPub /
   wrap.encryptedMasterKey; GCM-decrypt base64-decoded ct (tag appended) with base64-decoded iv.
3. Else → error.

A string-valued "version" (for example "2") does not select step 1.

Wire format (classical). ct and iv are standard base64 (RFC 4648 §4); wrap fields are 0x-hex as A.1 (deviceAddress may be empty or absent).

{ "v": 1, "alg": "ECDH-AES-256-GCM", "ct": "<base64 C‖T>", "iv": "<base64 12 bytes>",
  "wrap": { "deviceAddress": "", "ephemeralPub": "0x04…",
            "encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" } } }

Known-answer vector. Recipient d = 0x22…22; plaintext "agile classical payload".

{"v":1,"alg":"ECDH-AES-256-GCM","ct":"6c0Cm40ABJfgX0roBG4tSN6mNPB1ZkJaO9ZHAwASSceXUpShRWIY",
 "iv":"wKk7tpONXpNrKcXD","wrap":{"deviceAddress":"",
 "ephemeralPub":"0x04f397df702d54a470f19176057d33f58be90e6b448170f84a9472f5959528fbdb11581637b9cef4560e060190f8c17b2b06167f3307ee89645e56229b0bb35117",
 "encryptedMasterKey":{"ciphertext":"0x6c14cc7b0736943dee4a6508343fb7f6206ae8c399128f4dbc3ea7c1ff7c4a3a",
 "iv":"0x8acce034b834dcd32b2ffb41","authTag":"0x8a8293d3deb74e0dbea9874ad4ebabd9"}}}

The vectors file also holds a reverse envelope (independent producer, ephemeral key 0x35…35, plaintext "indep agile").

Known limitations.

D.3 rootz.kem.hybrid.mlkem1024-x25519-sha3.v1 — hybrid KEM combiner

Generic name. Hybrid KEM combiner: ML-KEM-1024 and X25519 shared secrets combined by SHA3-256 together with both ciphertexts and both recipient public keys.

Status. exported-unused.

Composition of. ML-KEM-1024 (FIPS 203); X25519 (RFC 7748); SHA3-256 (FIPS 202 §6.1). The combined input is a concatenation of shared secrets in the sense of SP 800-56C Rev. 2 §2 (Z′ = Z ‖ T), extended with the ciphertexts and public keys.

Purpose. Combine a post-quantum and a classical shared secret into one 32-byte secret.

Produce (encapsulate).

1. (ct_M, ss_M) = ML-KEM-1024.Encaps(pk_M)
2. (e_X, E_X)   ← fresh X25519 key pair;  ss_X = X25519(e_X, pk_X)
3. ss = SHA3-256( utf8("rootz-pq-wallet-v2") ‖ ss_M(32) ‖ ss_X(32) ‖ ct_M(1568)
                  ‖ E_X(32) ‖ pk_X(32) ‖ pk_M(1568) )
4. Output (ct_M, E_X, ss)

Consume (decapsulate). ss_M = Decaps(sk_M, ct_M), ss_X = X25519(sk_X, E_X), then step 3. Field order is significant; all fields are fixed length and there are no length prefixes.

Wire format. None defined (function outputs only).

Known-answer vector. Recipient X25519 secret 8888…88; recipient ML-KEM seed 4444…44 (d ‖ z). The vectors file carries the full ML-KEM ciphertext ct_M (1568 bytes).

recipient X25519 pk   30d3c865a48fceb3d6118577cf2e5f228d6ff69866264757785b253cb7a4806a
ephemeral X25519 pk   6da41d7c39ab682bfaed66dd5685a90de875098ae5b1d22b6efb37aa30f53d2b
ct_M                  73a41b60f3f391512716d2fe5ff0b9b8…  (1568 bytes, in the vectors file)
ss                    2f20a702582398ddefe21522e558484f51bb448150f404fbe04d01966192978d

Security properties. The combined secret depends on both shared secrets, both ciphertexts and both recipient public keys. No deployed data path uses this construction.

D.4 rootz.keychain.mlkem1024-epochs-hkdfsha384.v2 — epoch key chain

Generic name. ML-KEM-1024 epoch key sequence derived with HKDF-SHA-384; backward AES-256-GCM wrap of the previous decapsulation key; SHA-384 chain hash.

Status. exported-unused.

Composition of. HKDF-SHA-384 (RFC 5869); ML-KEM-1024 key generation (FIPS 203); AES-256-GCM (FIPS 197, SP 800-38D); SHA-384 (FIPS 180-4).

Purpose. Derive a sequence of ML-KEM-1024 key pairs ("epochs") from one master seed, and let the holder of epoch n recover the decapsulation key of epoch n−1.

Produce.

seed_n = HKDF-SHA384(IKM = master, salt = empty, info = utf8("rootz-enc-epoch-" ‖ decimal(n)), L = 64)
(pk_n, sk_n) = ML-KEM-1024.KeyGen_internal(d = seed_n[0:32], z = seed_n[32:64])
# sk_n is the FIPS 203 decapsulation key: dk_PKE(1536) ‖ ek(1568) ‖ H(ek)(32) ‖ z(32) = 3168 bytes

For n ≥ 1:
  WK_n = HKDF-SHA384(IKM = sk_n[0:32], salt = empty, info = utf8("rootz-key-chain-wrap-v2"), L = 32)
  prevKeyWrapped_n = nonce(12) ‖ C ‖ T   where (C,T) = GCM-Enc(WK_n, nonce = rand(12), sk_{n-1}, A = empty)
                                          # 12 + 3168 + 16 = 3196 bytes
Chain hash (integrity): leaf_n = SHA-384(0x00 ‖ pk_n ‖ prevKeyWrapped_n)   (prevKeyWrapped empty for n = 0)
                        h_0 = leaf_0;  h_n = SHA-384(0x01 ‖ h_{n-1} ‖ leaf_n)

Consume. From sk_n, compute WK_n, split prevKeyWrapped_n, GCM-decrypt to obtain sk_{n-1}.

Wire format. No serialised link format is defined. A chain link exists in memory only, as the structure { epoch (integer), pubkey (1568 bytes), prevKeyWrapped (3196 bytes, or none for epoch 0), keyChainHash (48 bytes) }. It is not an interchange format.

Known-answer vector. Master seed 9999…99 (32 bytes). The vectors file shows each link's fields as hex, for testing only.

epoch 0  SHA-256(pk) f79759e2e1fcb16fddd1aaa32df605b8a9835e5086a4cb85fe5addac2f6ed6ae
         keyChainHash f2f029d837e475ba3ac59b1543fa9f44bdcc2c2d5f76d0e80954d16cd5da4431
                      0287622a85d1f8cb9e338f18eb74ff66
epoch 1  SHA-256(pk) 362382667c918b123e695ac12246f6c648a5a042a763c269ed6087e9f03cbcf0
         prevKeyWrapped 9360701462bc484deeac246e…  (3196 bytes) → sk_0
         keyChainHash 9456663c4d8b409ed31024cc2d6ea861a6d3fe9bc7f3e6336eacf333aafe1129
                      0ca0301b9632732a21c3ce7fdc232e21
epoch 2  SHA-256(pk) 49004cdb77416b605e22cbb2e39af6d52c5be7c20ee9b368213c4f338f7c711d
         prevKeyWrapped b04653227d0eb417fcb860ff…  (3196 bytes) → sk_1
         keyChainHash 09107ad1a60d9eaeeda2206e824501c8594e5d08252e9afc0e88aba0362d7274
                      d78252fde4d4745a083559f0f51851c3

Known limitations.

D.5 rootz.wrap.hybrid.secp256k1-mlkem1024-hkdfsha256.sealed-memory.v1 — hybrid seat wrap

Generic name. Hybrid key wrap: static-static ECDH(secp256k1) x-coordinate concatenated with an ML-KEM-1024 shared secret, HKDF-SHA-256, AES-256-GCM; carried in a seat object.

Status. current (pre-release).

Composition of. secp256k1 ECDH with the x-coordinate shared secret (SEC 1, SP 800-56A); ML-KEM-1024 (FIPS 203, used as SP 800-227 describes); the hybrid shared secret Z′ = Z ‖ T of SP 800-56C Rev. 2 §2 with the two-step (extraction-then-expansion) derivation of SP 800-56C Rev. 2 §5, instantiated as HKDF-SHA-256 (RFC 5869); AES-256-GCM (FIPS 197, SP 800-38D).

Purpose. Wrap a 32-byte record key K from a granting identity to a member, so that recovery needs both the member's secp256k1 private key and its ML-KEM-1024 decapsulation key.

Inputs and encodings.

Produce.

1. X = X32( d_granter · Q_member )
2. (ct_M, ss_M) = ML-KEM-1024.Encaps(pk_M_member)
3. KEK = HKDF-SHA256(IKM = X ‖ ss_M, salt = empty, info = utf8("sealed-memory/v1/hybrid:secp256k1-ecdh+ml-kem-1024"), L = 32)
   # empty salt = 32 zero bytes per RFC 5869 §2.2
4. (C, T) = GCM-Enc(KEK, IV = rand(12), K, A = empty)
5. Emit { alg, kemCipherText: hex0x(ct_M), iv: hex0x(IV), ciphertext: hex0x(C), tag: hex0x(T) }   # tag split

Consume. X = X32(d_member · Q_granter) with Q_granter from the seat's granterPublicKey; ss_M = Decaps(sk_M, kemCipherText); derive KEK; GCM-decrypt.

Wire format — the wrap. 0x-hex.

{ "alg": "hybrid:secp256k1-ecdh+ml-kem-1024", "kemCipherText": "0x…1568 bytes…",
  "iv": "0x…12…", "ciphertext": "0x…32…", "tag": "0x…16…" }

Wire format — the seat. A wrap is carried in a seat object (fields in producer order):

{ "seatFormat": 1,
  "memberPublicKey":  "0x04…65 bytes…",
  "granterPublicKey": "0x04…65 bytes…",
  "label": <string|null>,
  "grantedAt": "<ISO 8601 date-time>",
  "wrap": <wrap object> }

Known-answer vectors. Granter 0x11…11, member 0x22…22, member ML-KEM seed 4545…45 (64 bytes), K = abab…ab.

X     77e0510d5042e2f5e9e59c977b81eeed590cf7d20c1c51da451a8eaa9fdc45ff
ss_M  14e30fa8c28eb6b48da5d6bd6b8c52835a0b3d9e8ae4db7543a6517b188c5fad
KEK   accd233485308ef700f7a62b01e2517bbe51f3d0be06d7018ff75887b8cf8c72
iv    f62b553e93fe575d5c03ed75
ciphertext abd6cb27aa2431133d2f5ccaa00f82593eb883171f17139b5d54bd76db288a6b
tag        7c107d3bd961febcd8a2aa722bfd57ff
kemCipherText: in the vectors file

The vectors file also holds a reverse wrap (independent producer), and a hybrid seat and a classical seat produced by the shipping code with the same keys. Each seat opens from its own fields.

Security properties. Recovering K requires both halves (classical and post-quantum).

Known limitations.

D.6 rootz.seal.sealed-memory.content-embedding-hmac-naming.v2 — record seal and record naming

Generic name. AES-256-GCM record encryption with an HKDF-SHA-256 naming subkey and an HMAC-SHA-256 record name.

Status. current (pre-release).

Composition of. AES-256-GCM (FIPS 197, SP 800-38D); HKDF-SHA-256 (RFC 5869); HMAC-SHA-256 (FIPS 198-1). The content layout is the A.3 content form.

Purpose. Seal one memory record (its text and its embedding vector together) under a 32-byte record key K, and give it a name that does not reveal its identifiers.

Produce.

1. payload = UTF-8 JSON of { title, content, embedding, tags, metadata, key, project }
             # project = the record's coordinates.project (may be null)
2. IV = rand(12);  (C, T) = GCM-Enc(K, IV, payload, A = empty)
3. N  = HKDF-SHA256(IKM = K, salt = empty, info = utf8("sealed-memory/v2/naming"), L = 32)
4. recordName = hex( HMAC-SHA256(N, utf8( JSON.stringify([namespace, project, key]) )) )
   # exact bytes: a JSON array of three values, no whitespace, e.g. ["ns","p1","k1"];
   # strings are escaped as ECMAScript JSON.stringify does (non-ASCII emitted as UTF-8)
5. Emit the record below (content tag appended)

Consume. GCM-decrypt ciphertext (appended tag) with K and iv; parse JSON.

Wire format.

{ "sealFormat": 2, "contentWire": "epistery/peer-cipher:aes-256-gcm",
  "recordName": "<hex 32 bytes>", "wrap": { "alg": "local" },
  "iv": "0x…12…", "ciphertext": "0x…C‖T…",
  "clear": { "id": "…", "namespace": "…", "kind": "…", "createdAt": <n>,
             "contentHash": <…|null>, "actorId": <…|null>, "epoch": <int> } }

wrap.alg must be "local", the only accepted value in a sealFormat 2 record: K is held by the reader and is not carried in the record. A record with any other wrap.alg is rejected. [code-read] (Keys are delivered to other readers by seats, D.5, not by this field.) clear is plaintext metadata.

Known-answer vector. K = cdcd…cd; namespace "ns", project "p1", key "k1".

naming key N  b190f9b2eeeccbc2b26afcac01f2c0ea2d672e2bf61825c3b86e4ab11c2d58c9
recordName    62b2ef6a56a6bbcfbd1936a498c1072eeb2a92c4b77dce521f6ce460170a031d
iv            6f76de4ffa1a54a46d13fa6d
ciphertext    747d4e57b03022000da3bf389716d6238c2802197d749194a1cd393cb23b9caa
              b46bb2ded94934acfaefa2750e2177c8bc3cf412345e50dbafa3583572d8f5e9
              bf49eb6da6f51725bb91d343e510b7fb9fc9935b31af523c8e07a8a2f2f27fae
              95c320c6322c1ff46e2d7b9c053f52f9048b7edb1ef1ea3c3f7dc719fd048cd2
              a4f60bae69c413984dae
 → {"title":"T","content":"secret body","embedding":[0.125,-0.5,1],"tags":["a"],"metadata":{"m":1},"key":"k1","project":"p1"}

Security properties. Content and embedding are confidential together; the embedding is not exposed separately.

Known limitations.


Part E — Other key-agreement and key-splitting constructions

E.1 rootz.seal.ecdh-p256-hkdf-sha256.proof-binder.v1 — sealed box (P-256)

Generic name. ECIES-style sealed box: ephemeral ECDH(P-256), HKDF-SHA-256, AES-256-GCM.

Status. current (pre-release).

Composition of. ECDH on NIST P-256 (FIPS 186-5, SP 800-186; SP 800-56A ECC CDH); HKDF-SHA-256 (RFC 5869); AES-256-GCM (FIPS 197, SP 800-38D). SEC 1 §5.1 ECIES structure. Deviations: HKDF-SHA-256 with fixed salt and info in place of the ANSI X9.63 KDF; GCM in place of encryption and MAC.

Purpose. Encrypt a message to the holder of a P-256 key (anonymous sender).

Produce.

1. (e, E) ← fresh P-256 key pair
2. Z   = X32( e · Q_R )                                        # 32-byte x-coordinate
3. key = HKDF-SHA256(IKM = Z, salt = utf8("rootz-proof-salt"), info = utf8("rootz-proof-v1"), L = 32)
4. IV  = rand(12);  (C, T) = GCM-Enc(key, IV, plaintext, A = empty)
5. Emit { epk: hex(E uncompressed, 65 bytes), iv: hex(IV), ct: hex(C ‖ T) }     # no 0x; tag appended

Consume. Z = X32(d_R · epk); derive key; GCM-decrypt.

Wire format. { "epk": "<hex 65>", "iv": "<hex 12>", "ct": "<hex C‖T>" } (lower-case hex, no 0x). Public keys are uncompressed X9.62 / SEC 1 hex.

Known-answer vector. Recipient d = 3333…33 (P-256).

recipient pub  0451a7580833898ea1b183cbd7350a4099078c6ef1c1e18e970cd7683035f25e7d
                 0110522712b0b5a7cff081685486984a94e6831edac46e7360fa9d834a7a81a1
epk            043e4fbf76c0fe40351b17e69f51c25f6a67d1218c8850679344071c58db77c85b
                 ccd03834723b968269a05fb13f63840563359fda386e6773c9e701a08abcaac6
key            d83ea42388fc38a93177ab5614381cbea1a84cc00728ed7ca1df151aaf4d7a7b
iv             aa596a58bdb24026b9f584c7
ct             8fba859af5d57b5537fd41e5a5e8728a2a2e9cc9df159713c41e9e92db847004483f335ecb
 → plaintext   "sealed prompt — KAT" (UTF-8)

Security properties. A fresh key per box.

Known limitations. The sender is not authenticated inside the box (any authentication is a separate signature, out of scope). No AAD.

E.2 rootz.recovery.shamir-gf256-0x11b.v1 — Shamir secret sharing over GF(2^8)

Generic name. Shamir secret sharing over GF(2^8).

Status. exported-unused.

Composition of. Shamir secret sharing (1979) over GF(2^8) with reduction polynomial x^8 + x^4 + x^3 + x + 1 (0x11b, the FIPS 197 field).

Purpose. Split a secret of any length into n shares, any k of which recover it.

Produce. For each secret byte s, choose a_1 … a_{k−1} uniformly at random and evaluate a(x) = s + a_1·x + … + a_{k−1}·x^{k−1} in GF(2^8) at x = 1 … n. Share i is { index: i, data: [a_b(i) for each byte b] } (same length as the secret).

Consume. Lagrange interpolation at x = 0, byte by byte, over the supplied shares.

Wire format. { index: <1..n>, data: <bytes> } per share. The threshold k is not encoded.

Known-answer vector. Secret 00 ab…ab ff (32 bytes), 3-of-5:

1  f490edae42b8960317c72b249606b2ddc3b14481b2c65dfa29d0cc772c8bf73c
2  5387f0686c159a2d52771b3536323c8d84ae6cca36d8400fa9bc698a9333db14
3  a7bcb66d8506a785ee1b9bba0b9f25fbecb483e02fb5b65e2bc70e56141387d7
4  10806c3f069afea51c184ad63bcf3ce2246536bd00df16e0db2c04d76fcd46f1
5  e4bb2a3aef89c30da074ca59066225944c7fd99719b2e0b15957630be8ed1a32

Any three shares recover the secret.

Security properties. Information-theoretic secrecy below the threshold.

Known limitations. No below-threshold detection and no share integrity: combining fewer than k shares, or a corrupted share, yields a different value without an error.


Part F — Local at-rest stores

F.1 rootz.atrest.agent-storage.keccak-privtext.v1 — agent local secret store

Generic name. At-rest store: key = Keccak-256 over the private-key text and a fixed label; AES-256-GCM with a 128-bit IV.

Status. current.

Composition of. Keccak-256 (FIPS 202 §5.2 KECCAK[512]); AES-256-GCM (FIPS 197, SP 800-38D) with a 128-bit IV (SP 800-38D §7.1, non-96-bit IV case).

Purpose. Encrypt an automated agent's local store of secrets (including master keys) under a key derived from the agent's signing key.

Inputs and encodings.

Produce. IV = rand(16); (C, T) = GCM-Enc(K, IV, utf8(JSON(records)), A = empty).

Consume. Derive K from the same privText; GCM-decrypt; parse JSON.

Wire format. { "iv": "<hex 16>", "encrypted": "<hex C>", "authTag": "<hex 16>" } — hex without 0x, tag split.

Known-answer vector. privText = "0x1212…12".

K       baacf49261d89309eb68000beca584109261e89783a11f6811eaf9cc53387fe1
iv      f4537b42447282a9e4ee09f31cdf5326
authTag 42a96739cf554a91019975d74cf94708
encrypted: in the vectors file  → a JSON array with one record whose masterKey is "0xa5a5…a5"

Known limitations.

F.2 rootz.atrest.mcp-accumulator.aes-256-gcm.v1 — session transcript buffer

Generic name. Line-by-line AES-256-GCM buffer encryption with a sequence-prefixed IV.

Status. current.

Composition of. AES-256-GCM (FIPS 197, SP 800-38D).

Purpose. Encrypt, line by line, a local buffer of an AI-assistant session before it is archived.

Inputs and encodings. A random 32-byte key per session. Each line's plaintext is utf8(JSON text) of one buffered event. IV = uint32_be(seq) ‖ rand(8) (12 bytes), where seq is the line's sequence number.

Produce. (C, T) = GCM-Enc(K, IV, utf8(line), A = empty); emit { seq, enc: base64(IV ‖ C), tag: base64(T) }.

Consume. b = base64-decode(enc); IV = b[0:12], C = b[12:]; GCM-decrypt with T = base64-decode(tag).

Wire format. { "seq": <int>, "enc": "<base64 IV‖C>", "tag": "<base64 16 bytes>" }, one per line.

Known-answer vector. K = 2121…21.

{"seq":7,"enc":"AAAAB4hn2WoWgEj0YiFe3H11RVMxlTjji+M/0DRNK2oxMcvv6TQSOTsOWAk73TI5ZgXxh7x8",
 "tag":"WvWdFMHnqxCNWBKLlzyCsw=="}
 → {"ts":1,"type":"observation","text":"kat"}

Security properties. Per-line confidentiality and integrity.

Known limitations. Each line is protected on its own. There is no AAD, and the buffer as a whole has no integrity: the order and completeness of lines are not protected by this construction. Storage of the session key is key management and is out of scope (Section 1.3).


Part G — Hardware-bound key derivation

These constructions derive a secp256k1 private key that the Part A constructions then use as a recipient key to unwrap master keys. They are included because the derived key decrypts. The hardware operations were not executed for this document: each vector uses a software stand-in and is not hardware-verified.

G.1 rootz.hwkey.tpm-keccak-entropy-rsasig.v1 — TPM-bound wallet key

Generic name. Key derived by Keccak-256 over a software entropy value and a deterministic RSA PKCS#1 v1.5 signature over a stored label.

Status. current.

Composition of. RSASSA-PKCS1-v1_5 with SHA-256 (RFC 8017 §8.2; computed by a TPM-resident RSA-2048 key, used as secret KDF input); Keccak-256 (FIPS 202 §5.2 KECCAK[512]).

Purpose. Derive a wallet key that can be recreated only with both a software secret and one specific TPM.

Formula.

entropy = rand(32)                                       # software factor
label   = the wallet's stored label (a parameter; see below)
sig     = RSASSA-PKCS1-v1_5-Sign_SHA256(TPM RSA-2048 key, utf8(label))   # 256 bytes
priv    = Keccak-256( entropy(32) ‖ sig(256) )           # 32-byte secp256k1 private key (big-endian)

The label is a stored parameter. Each wallet record stores its label (field salt) next to its public metadata, and the label is re-read whenever the key is re-derived. Two values are registered:

Label Use
rootz-tpm-wallet-v1 The desktop TPM wallet.
rootz-tpm-signer-v1 The default of the TPM signer library interface when no label is supplied.

Different labels give different keys. [code-read for where each label is used and stored; the derivation itself was executed with both labels]

Encoding details. The label is passed to the TPM as the hex of its UTF-8 bytes; the TPM signs the UTF-8 bytes. The TPM returns the signature as hexadecimal text (upper case, no prefix). The concatenation entropy ‖ sig is over the decoded bytes.

Known-answer vectors — SOFTWARE STAND-IN, NOT HARDWARE-VERIFIED. A generated RSA-2048 test key replaces the TPM-resident key. Only its public key (SPKI DER) is published in the vectors file, with the recorded signature for each label; the private key is not needed to check the derivation, because priv depends only on the entropy and the signature. PKCS#1 v1.5 is deterministic, so each label gives one signature.

entropy                       0e0e…0e (32 bytes)
label rootz-tpm-wallet-v1  →  signature 9A54B15563753628B710DA75AD817B98… (256 bytes, in the vectors file)
                              address   0x2E5EfD1765D80F9c4fc01503482ec59bf7Cf2BFE
label rootz-tpm-signer-v1  →  signature A832AD6EF5E66DB3CE687E4AA415966E… (256 bytes, in the vectors file)
                              address   0xfB947F5f13C6508F21eEe66e834fa4b7d490133b

Known limitations.

G.2 rootz.hwkey.se-keccak-domain-entropy-ecdh-anchor.v1 — Secure-Enclave-bound wallet key

Generic name. Key derived by Keccak-256 over a fixed label, a software entropy value and an ECDH(P-256) x-coordinate with a fixed public key.

Status. current.

Composition of. ECDH on NIST P-256 (SP 800-56A ECC CDH; performed with a Secure-Enclave-resident key); Keccak-256 (FIPS 202 §5.2 KECCAK[512]).

Purpose. As G.1, with an Apple Secure Enclave P-256 key in place of the TPM.

Formula.

entropy = rand(32)
S       = X32( SE P-256 private key · A )                 # 32-byte x-coordinate
priv    = Keccak-256( utf8("rootz-se-wallet-v1") ‖ entropy(32) ‖ S(32) )

A is a fixed P-256 public key embedded in the application (a valid point, not the generator):

A = 04059c4056243314e6e6c668bcace85dd16305810f63ea1918df244baf9354a9c4
      ae00304a6a7959d0e36b24347e110896ba344815d33c1da24fbb0ae2cd361303

The enclave helper returns S as the 32-byte x-coordinate (the CryptoKit shared secret), encoded as 0x plus lower-case hex. [code-read; not executed on hardware]

Known-answer vector — SOFTWARE STAND-IN, NOT HARDWARE-VERIFIED. The enclave key is replaced by the software P-256 key 5e5e…5e.

S (X32)  4ca8d09c39c0600516d562653fde2b726a67143585a849c770aa9ca7b0c918b4
entropy  0e0e…0e
priv     7046782a5060e8f2aeb5c2f3830458f58716e7e652f481e7179dbbb4d0f5468f
address  0x536875A7d5A3b49872F6aC77B87dA5BA42E91df1

Known limitations.

Appendix A — Identifier registry

This version specifies 24 constructions: 23 with known-answer vectors, and one (A.7) whose vector is pending.

Products: Desktop = Rootz Desktop (installer), which bundles the web dashboard, its MCP server component and the SDK packages it uses; Web = the web dashboard code of the Rootz Layer, served to browsers; SDK = the Rootz SDK packages (@rootz/*); Sealed Memory = Rootz Sealed Memory; Proof binder = the Rootz proof binder; Agent TEE = the Rootz agent runtime container (a pre-release component).

§ Identifier Status Products Packages and components
A.1 rootz.wrap.ecdh-secp256k1-sha256.v1 current SDK, Desktop, Web, Agent TEE @rootz/crypto; byte-compatible copies in @rootz/hardware-keys (library and web page) and @rootz/team-invite (mock provider); agile classical wrap in @rootz/pq-crypto; read path in the agent runtime container
A.2 rootz.wrap.ecdh-secp256k1-sha256.team-invite.v1 legacy-read-only SDK, Web @rootz/team-invite; owner-self read path in the web dashboard
A.3 epistery.wrap.ecdh-secp256k1-x-sha256.peer-cipher.v1 current (pre-release) Sealed Memory (dependency: epistery) Epistery peer-cipher module, used by Rootz Sealed Memory (classical seats, content layout)
A.4 rootz.wrap.ecdh-secp256k1-keccak256.teams.v1 current SDK, Desktop, Web @rootz/teams; callers in @rootz/api-server, @rootz/agent and the web dashboard
A.5 rootz.wrap.ecdh-secp256k1-keccak256.identity.v1 current SDK, Desktop, Web @rootz/identity-provider
A.6 rootz.wrap.ecdh-secp256k1-keccak256.identity-device-key.v1 exported-unused SDK @rootz/identity-provider
A.7 rootz.wrap.ecdh-secp256k1-keccak256-x.legacy.v5-2 legacy-read-only (data written by an earlier generation) —
B.1 rootz.wrap.sig-storage-key.sha256-hextext.v1 current SDK, Desktop, Web @rootz/crypto
B.2 rootz.wrap.sig-storage-key.sha256-rawsig.hardware-keys.v1 current SDK @rootz/hardware-keys (library and web page)
C.1 rootz.content.aes-256-gcm.v1 current SDK, Desktop, Web, Agent TEE @rootz/crypto; copy in @rootz/hardware-keys
C.2 rootz.content.note.kvkey-aes-256-gcm.v1 current SDK, Desktop, Web @rootz/notes
C.3 rootz.kdf.kvkey-keccak256-mktext.v1 current (pre-release) Agent TEE agent runtime container
D.1 rootz.envelope.pq.mlkem1024-hkdfsha384-aesgcm.v2 exported-unused SDK @rootz/pq-crypto
D.2 rootz.envelope.agile.v1 exported-unused SDK @rootz/pq-crypto
D.3 rootz.kem.hybrid.mlkem1024-x25519-sha3.v1 exported-unused SDK @rootz/pq-crypto
D.4 rootz.keychain.mlkem1024-epochs-hkdfsha384.v2 exported-unused SDK @rootz/pq-crypto
D.5 rootz.wrap.hybrid.secp256k1-mlkem1024-hkdfsha256.sealed-memory.v1 current (pre-release) Sealed Memory Rootz Sealed Memory
D.6 rootz.seal.sealed-memory.content-embedding-hmac-naming.v2 current (pre-release) Sealed Memory Rootz Sealed Memory
E.1 rootz.seal.ecdh-p256-hkdf-sha256.proof-binder.v1 current (pre-release) Proof binder Rootz proof binder
E.2 rootz.recovery.shamir-gf256-0x11b.v1 exported-unused SDK @rootz/pq-crypto
F.1 rootz.atrest.agent-storage.keccak-privtext.v1 current SDK @rootz/agent
F.2 rootz.atrest.mcp-accumulator.aes-256-gcm.v1 current Desktop Rootz Desktop (MCP server component)
G.1 rootz.hwkey.tpm-keccak-entropy-rsasig.v1 current Desktop, SDK @rootz/tpm-provider; desktop wallet in the web dashboard
G.2 rootz.hwkey.se-keccak-domain-entropy-ecdh-anchor.v1 current Desktop, SDK @rootz/secure-enclave-provider (and its native helper)

Appendix B — How the vectors were produced and verified

B.1 The vectors file. SPEC-v1.2-vectors.json contains, under vectors.<identifier>, the status, the generic_name, a vector object, and an inputs object that states the keys and expected plaintext of every sub-vector. All private keys, seeds and content keys in it are test values (repeated-byte patterns). For G.1 only the public half of the generated RSA test key is included. Hex strings are reproduced exactly as they appear on the wire, with or without a 0x prefix.

B.2 How independence was established. Three separate pieces of work contribute, and they are described here as they were done.

  1. Producer. For every vector, the shipping Rootz implementation (its built distribution, or the shipped module) produced the values from test keys. Vectors marked reverse were produced by an independent implementation and opened by the shipping implementation.
  2. Known-answer harness (written alongside version 1.0 of this specification). Its consumers opened every producer output. They used node:crypto on OpenSSL 3.6 for AES-GCM, SHA-2, SHA-3, HKDF, HMAC, X25519, ML-KEM-1024 and ML-DSA-65; @noble/curves 1.9.7 for secp256k1 and P-256 point arithmetic and RFC 6979 signing (a library by the same author as the curve code inside the ethers library that the producers use, in a different version), with every x-coordinate cross-checked against OpenSSL; and Keccak-256, EIP-55 and GF(2^8) arithmetic written in the harness. Version 1.0 of this text was written from the harness output. The harness reports 23 PASS: the 21 constructions of version 1.0 that have vectors, plus two meta-checks (a self-test of its own primitives, and a check that each built distribution is newer than its source). Five deliberate errors injected into its consumers (x-only KDF in place of the full point, SHA3-256 in place of Keccak-256, a wrong HKDF info string, a wrong salt label, a wrong naming label) each turned the matching construction red.
  3. Text-first independent consumer. An independent review wrote a consumer from the text of version 1.0 alone, with its own secp256k1 and P-256 arithmetic, its own Keccak-256 (checked against the empty-string known answer), its own RFC 6979 and EIP-191 signing, and its own HKDF; only AES-GCM, SHA-2/SHA-3, HMAC, ML-KEM and ML-DSA came from node:crypto. It opened every version 1.0 vector except those that lacked data (D.3, the D.4 links) and one truncated field, and it established the facts added in version 1.1 by running the shipping code. verify-v1.2.mjs is derived from that consumer. It adds its own FIPS 203 expansion of the ML-KEM decapsulation key (for D.4), and RSA signature verification and X25519 from node:crypto.
  4. Final text-first check. A further independent review wrote a new consumer from the text of version 1.1 alone and opened every vector on its first run. Version 1.2 corrects the sentences that review found insufficient (D.1 key generation and number serialisation, the D.5 seat-format rule, the A.4 enclosing messages, and §4.3 on compressed keys).

B.3 Version 1.2 verification result. node verify-v1.2.mjs opens every vector in the file: 23 of 23 entries with vectors, 128 checks passed, 0 failed. Per entry: A.1 11, A.2 2, A.3 5, A.4 15, A.5 6, A.6 5, B.1 4, B.2 8, C.1 3, C.2 5, C.3 2, D.1 12, D.2 2, D.3 3, D.4 10, D.5 7, D.6 5, E.1 3, E.2 5, F.1 2, F.2 2, G.1 8, G.2 3. The A.4 count includes the direct-keys checks, among them a negative check that the A.2 key derivation does not open a direct-keys 6.0 wrap. To check that the consumer can fail, corrupted copies of the vectors file were run; each corruption was detected.

B.4 Statements established by reading source code. The statements marked [code-read] in the text were not executed: where the A.1 member wrap's sender key comes from; the A.2 owner-self read path; the assembly of the A.4 key-delivery, device-sync and direct-keys 4.0 messages (their wrapped-key objects were executed; the direct-keys 6.0 message was executed end to end); the fields that the shipping web consumer reads from a direct-keys message; the D.5 rules for a seat without alg, for an unknown alg and for an unknown seatFormat; the D.6 rule that only local is accepted; which vault-key rule (C.2, C.3) each component applies; where each G.1 label is used and stored; and the G.2 helper output encoding. C.3 was produced by evaluating its formula with the library function that the (non-exported) agent-runtime code calls.

B.5 To verify an entry.

  1. Take the test private keys or seeds from the entry's vector and inputs.
  2. Recompute the reproducible values (public keys, Z65/X32, derived keys K/KEK/WK, signatures from deterministic signers, ML-KEM public keys from seeds) and compare.
  3. Decrypt each recorded ciphertext with the entry's Consume procedure and compare with the plaintext given in inputs.
  4. Optionally produce a fresh value with your implementation and open it with your own Consume procedure, then compare field formats with the Wire format section.

B.6 Vector limitations.

Appendix C — Revision history

Version Date Change
1.0 2026-10-16 First version, written from executed known-answer tests.
1.1 2026-10-16 Revised after an independent assurance review. Scope defined (Section 1.1); entries A.6, A.7 and C.3 added; G.1 label defined as a stored parameter with both registered values; B.2 on-chain record form added; message fields for A.1, A.4, D.1 and D.5 specified (sender keys, seats, senderId, reWrapBy rule, mandatory signature check); D.1, D.2 and D.4 statuses corrected to exported-unused; full D.3 and D.4 vectors; every sub-vector's keys and plaintext stated; generic names, standards citations and known-limitation lists added; implementation detail not needed for interoperability removed.
1.2 2026-10-16 Revised after a final independent review. Scope stated by product (Section 1.1), with Epistery constructions other than A.3 and server-only code out of scope (Section 1.3); direct-keys 4.0 and 6.0 messages added to A.4, with vectors and the rule that the construction is selected by message type and version, not by the algorithm label; status definitions revised (Section 4.9), with D.5, D.6, A.3, C.3 and E.1 marked current (pre-release); the reserved-prefix entry removed; Appendix A lists packages and products instead of source paths; D.1 key generation (ML-DSA.KeyGen_internal) and number serialisation pinned; D.5 seat-format rule added; §1.4, §2, §4.3 and §4.6 corrected; F.2, D.2 and D.6 limitations restated as properties; G.1 vectors carry the public RSA test key only.