Skip to content

feat(session): per-profile symmetric-key derivation (derive_symmetric_key) - #3

Merged
MichaelTaylor3d merged 3 commits into
mainfrom
feat/derive-symmetric-key
Jul 20, 2026
Merged

feat(session): per-profile symmetric-key derivation (derive_symmetric_key)#3
MichaelTaylor3d merged 3 commits into
mainfrom
feat/derive-symmetric-key

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Adds an ADDITIVE UnlockedIdentity::derive_symmetric_key(label) deriving a per-profile DEK, byte-identical to dig-app's current HKDF-from-identity derivation (keystore/secrets.rs). Release-first for dig-app #1024 Phase 2. v0.1.1 -> v0.2.0.

Custody-crypto: byte-exactness is the whole point. Blast radius + golden vector to follow in a comment.

Refs DIG-Network/dig_ecosystem#1024

🤖 Draft — WIP

MichaelTaylor3d and others added 2 commits July 20, 2026 13:21
…024)

Placeholder to open a draft PR early (ecosystem §1.8 push-early).

Co-Authored-By: Claude <noreply@anthropic.com>
…_key)

Add additive `UnlockedIdentity::derive_symmetric_key(label)`: derive a
per-profile symmetric key (DEK) from the unlocked identity scalar via
HKDF-SHA256, returning the derived key by label while the raw scalar stays
inside the facade (#908 preserved).

The construction is byte-identical to dig-app's current profile DEK
(dig-app-core keystore/secrets.rs `dek_password` + `to_sealed_bytes`):
HKDF-SHA256(salt="dig-app:dek-salt:v1", ikm=0x02||identity_scalar, info=label)
expanded to 32 bytes — so already-sealed profile blobs stay readable (§5.1).
The IKM is the versioned 33-byte layout (0x02 prefix), NOT the bare scalar.

Tests: a byte-identity test against an independent reconstruction of dig-app's
construction, a frozen golden vector (fixed scalar + label -> exact DEK bytes),
and label determinism/uniqueness. DEK + intermediates are Zeroizing.

Bump 0.1.1 -> 0.2.0. Closes #1024.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review July 20, 2026 20:56
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

v0.2.0 — derive_symmetric_key (per-profile DEK), byte-identical to dig-app

HEAD: b52a8433a66fcd3e4bcef54024cd29df75af7dc0 (branch feat/derive-symmetric-key; supersedes the WIP stub d7011b9, which left the version-increment check RED — now GREEN at 0.2.0).

Ported HKDF construction (EXACT)

Byte-identical to dig-app's profile DEK at modules/apps/dig-app/crates/dig-app-core/src/keystore/secrets.rs (dek_password, line ~158, feeding seal_data/open_data at line 141) + its to_sealed_bytes (line ~170):

  • KDF: HKDF-SHA256 (RFC 5869), hkdf 0.12 + sha2 0.10 — matching dig-app's crate majors.
  • IKM: 0x02 || identity_scalar — the versioned 33-byte layout SEALED_IDENTITY_VERSION(2) || scalar(32) (dig-app's to_sealed_bytes()), NOT the bare 32-byte scalar. This is the load-bearing catch.
  • identity_scalar: the 32-byte canonical derive_identity_sk(master).to_bytes() — the same bytes dig-session already stores via L1WalletBls (confirmed against enroll_identity).
  • salt: b"dig-app:dek-salt:v1" (dig-app DEK_SALT).
  • info: label verbatim; dig-app's profile-DEK label is b"dig-app:profile-dek:v2" (dig-app DEK_INFO).
  • output: 32 bytes.

Golden vector (fixed seed b"dig-session integration-test seed material", label dig-app:profile-dek:v2)

  • identity scalar: 357244b8580351ab857d765587e63742415904 2ed0a65f4972c1b3757d97c12a
  • DEK: 1ac813e491ba3d05f4be2836bba736b4ba0a2d 74bde45a5b02859d8acfb1cf7b

Verified two ways: (C-9) equality against an independent in-test reconstruction of dig-app's exact construction, and (C-10) equality against the frozen literal above.

Guarantees

  • Additive only (§5.1) — no existing API/behaviour changed; C-1..C-8 + L1WalletBls parity (C-2) + debug-redaction (C-8) all still green.
  • Scalar never leaves the facade (#908) — only the derived key by label is returned; DEK + all intermediates are Zeroizing.
  • fmt clean; clippy --all-targets -D warnings clean; 12 tests pass; coverage 97.32% lines (unlocked.rs 100%).

For the coordinator (SYSTEM.md + canonical): record the ecosystem invariant — per-profile DEK = HKDF-SHA256(salt="dig-app:dek-salt:v1", ikm=0x02||identity_scalar_32, info=label)→32B; profile label dig-app:profile-dek:v2; byte-identical across dig-app secrets.rs and dig-session derive_symmetric_key.

Not merging — handing to the review gate.

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness gate (fresh context, security-critical: per-profile DEK / key custody).

The crypto is CORRECT and I independently verified byte-identity against dig-app: dig-app-core/src/keystore/secrets.rs derives its DEK as HKDF-SHA256(salt=dig-app:dek-salt:v1, IKM=SEALED_IDENTITY_VERSION(2) || identity_scalar, info=dig-app:profile-dek:v2, 32B), and its identity_scalar is derive_identity_sk(&master).to_bytes() (secrets.rs:103) — the SAME derivation dig-session's expose_secret() returns (proven by C-9 passing). hkdf 0.12 / sha2 0.10 match dig-app's workspace pins. Construction, salt, version prefix, label, output length all match. Tests are meaningful: C-9 (independent reference reconstruction), C-10 (frozen golden literal + scalar-drift assert), varies-by-label + determinism. Secret handling is clean (exact-capacity Zeroizing IKM, Zeroizing DEK, scalar never returned). Additive-only, minor bump 0.2.0, version gate green, CHANGELOG correctly deferred to git-cliff at release. 8/8 checks green, 0 threads.

ONE gating finding (dig-constants HARD RULE): the shared byte-identical DEK construction constants are defined LOCALLY in BOTH dig-app and dig-session instead of in the canonical dig-constants crate. DEK_SALT = b"dig-app:dek-salt:v1", the IKM version byte 2, and the profile-DEK label b"dig-app:profile-dek:v2" are cross-repo byte-identical values a second repo MUST match forever (§5.1 permanent at-rest contract). This PR spreads the local definition to a second repo — exactly the shared-value drift §4.1 forbids. The frozen golden vector catches drift within dig-session but does NOT prevent dig-app and dig-session diverging independently. These belong in dig-constants (00-foundation), published + consumed by version in both repos (analogous to the DIG_ASSET_ID rule). See inline comment.

Verdict: CHANGES-REQUIRED (single blocker). Everything else PASSES.

Comment thread src/unlocked.rs Outdated
Replace the locally-duplicated DEK_SALT/IDENTITY_IKM_VERSION/
SYMMETRIC_KEY_LEN literals with the byte-identical consts published in
dig-constants 0.7.0 (DEK_SALT, IDENTITY_IKM_VERSION, PROFILE_DEK_LABEL,
SYMMETRIC_KEY_LEN), now that dig-app-core also consumes them from there
(dig_ecosystem #4.1/#5.1/NC-5 single source of truth). Behaviour-preserving:
the values are unchanged, so the derived DEK and the golden vector (C-9,
C-10) are unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Resolved the correctness blocker on `src/unlocked.rs:21`: swapped the local DEK-constant literals for the published `dig-constants` 0.7.0 consts.

New HEAD: `78050fe` (was `b52a843`)

Swap:

  • local `DEK_SALT` const (`b"dig-app:dek-salt:v1"`) → `dig_constants::DEK_SALT`
  • local `IDENTITY_IKM_VERSION` const (`2`) → `dig_constants::IDENTITY_IKM_VERSION`
  • local `SYMMETRIC_KEY_LEN` const (`32`) → `dig_constants::SYMMETRIC_KEY_LEN`
  • doc references to the default label now point at `dig_constants::PROFILE_DEK_LABEL` (the label itself is still passed in by the caller — `derive_symmetric_key` takes `label: &[u8]`, it never hardcoded the label)

`dig-constants = "0.7"` added as a direct crates.io dependency (no git dep). Behaviour-preserving — values are byte-identical, confirmed by re-running the existing golden-vector tests unchanged:

  • `derive_symmetric_key_golden_vector` (C-10) — PASS
  • `derive_symmetric_key_is_byte_identical_to_dig_app_dek` (C-9) — PASS
  • full suite: 12/12 tests pass, 1 doc-test pass
  • `cargo tree -i dig-constants` → single source, v0.7.0, no duplicates
  • coverage: 97.32% (unlocked.rs 100%, session.rs 94%) — well above the 80% floor
  • fmt clean, clippy clean (`--all-targets -D warnings`)

SPEC.md §3.2 DEK section updated to name dig-constants as the source of truth for these constants (satisfies the "dig-session consumes these consts" forward-looking claim the dig-constants reviewer flagged).

Version stays 0.2.0 (behaviour-preserving, additive dep only, no API change).

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PASS (correctness re-gate, swap-only, HEAD 78050fe).

Re-gated ONLY the local-literal -> dig-constants swap; crypto already cleared on b52a843.

  • Behaviour-preserving: DEK_SALT, IDENTITY_IKM_VERSION, SYMMETRIC_KEY_LEN now imported from dig_constants; derivation body (HKDF-SHA256, IKM = 0x02 || scalar, salt, info=label, 32-byte expand) is byte-identical to the prior HEAD. No logic change.
  • Golden vector intact: tests/facade.rs is UNCHANGED across b52a843..78050fe (empty test diff). GOLDEN_DEK/GOLDEN_SCALAR literals frozen; C-9 (byte-identical) + C-10 (golden vector) pass in CI Test Suite -> objective proof the DEK is unchanged.
  • Dep hygiene: dig-constants = "0.7" direct crates.io dep (no git dep); cargo tree -i dig-constants shows a single source (v0.7.0). Version 0.2.0 retained (behaviour-preserving; correct).
  • CI: all required checks green on 78050fe (Test Suite, Coverage >=80%, version-increment, commitlint, Analyze/CodeQL).
  • SPEC.md correctly cites dig-constants as the DEK-constant source of truth.

Prior blocker (src/unlocked.rs:21, duplicated local literals) is addressed. Resolving that thread. Orchestrator owns merge.

@MichaelTaylor3d
MichaelTaylor3d merged commit 58ebfe8 into main Jul 20, 2026
8 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the feat/derive-symmetric-key branch July 20, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant