Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ The push subscription endpoint (WebSocket, SSE, gRPC stream, etc.) is an operato

`WitnessEntry` is a v1.1 spec-defined element in a hash-chain append-only log (ROADMAP, Tamper-evident transaction streams). Any party may operate a Witness: Identities AI, an enterprise's own audit system, a third-party notary, or a blockchain-anchored system. Multiple witnesses MAY independently log the same events (redundancy).

v1.1 defines the shape and the signing semantics only. Operating a scalable witness is an implementation/product concern; the spec does not mandate deployment topology, storage backend, or consistency model. The fixture `witness_entry_valid` proves cross-SDK byte-identicality of the signable and signature.
v1.1 defines the shape and the signing semantics only. Operating a scalable witness is an implementation/product concern; the spec does not mandate deployment topology, storage backend, or consistency model. The fixture `witness_entry_valid` proves cross-SDK byte-identical signable bytes; the supplied reference signature is preserved and verifies in every SDK (signature bytes are not independently regenerated outside the Go reference; see §8.3).

### 5.13 SessionToken

Expand Down Expand Up @@ -868,13 +868,12 @@ To verify a `HybridSignature` against a `HybridPublicKey` over the same `msg`:
2. `ML-DSA-65.Verify(pub.ml_dsa_65, msg, sig.ml_dsa_65)` MUST return true.
3. If either fails, the hybrid verification fails. **Fail-closed.**

### 8.3 Determinism
### 8.3 Signature mode

v1 uses **deterministic** ML-DSA-65 signing (FIPS 204 §3.4 without hedged randomization). Rationale:
Implementations MAY use deterministic or hedged ML-DSA-65 signing (FIPS 204 §3.4). Independently generated signature bytes are not required to match across SDKs; the canonical signable bytes and the verification outcome MUST match. Because both components are verified against the same canonical bytes, a deterministic signature and a hedged signature over identical input are interchangeable to any verifier.

- Reproducible test vectors and audit trails.
- Same security properties as Ed25519's determinism under the lattice assumptions.
- A future version MAY add a hedged-randomization mode for side-channel hardening in hostile environments; v1 does not.
- The reference fixture generator (Go) uses deterministic signing solely to make fixture regeneration reproducible: the conformance corpus regenerates byte-for-byte from fixed seeds.
- The TypeScript, Python, Rust, and C SDKs sign with hedged randomization, the default of their ML-DSA-65 libraries. Hedged signing is side-channel hardening that depends on trustworthy host entropy; deterministic signing removes that dependence. Neither is categorically safer, and both produce valid, interchangeable signatures.

### 8.4 Key generation

Expand Down Expand Up @@ -1411,7 +1410,7 @@ The following table enumerates the adversaries, their capabilities, and how the
| T7 | **Stolen root private key** | Holds a human's root private key | Issue rogue delegations, competing rotation statements | Revocation of all certs issued by the compromised key. `KeyRotationStatement` (§5.15) to move to a new key. Registry policy for out-of-band verification. See §15.3. | Attacker can issue a competing `KeyRotationStatement`. Resolution requires registry/operator trust — see §15.3. |
| T8 | **Malicious verifier** | Legitimate verifier that acts dishonestly | Forward V1's challenge to the agent, replay the bundle at V1 | Session context binding (§5.8, §15.1): agent signs with the verifier's context. Cross-verifier replay fails because contexts differ. | Requires the agent to include session_context. Legacy unbound bundles are vulnerable on non-TLS transports. |
| T9 | **Rogue registry operator** (custodial mode) | Holds envelope-encrypted user keys | Decrypt keys and issue unauthorized delegations | Envelope encryption (DEK + KEK via KMS) limits blast radius. Self-custody mode (§15.2) eliminates this adversary entirely. Self-custody upgrade path via `KeyRotationStatement`. | In custodial mode, the operator IS trusted. This is documented in §15.2. Self-custody is the mitigation. |
| T10 | **Side-channel attacker** | Observes timing/power during signing on shared infrastructure | Extract private key material via side-channel analysis | Use well-audited crypto libraries. v1 uses deterministic ML-DSA-65 signing; future versions MAY add hedged-randomization for hostile environments. | Deterministic signing on shared VMs is a known industry-wide risk. See §8.3. |
| T10 | **Side-channel attacker** | Observes timing/power during signing on shared infrastructure | Extract private key material via side-channel analysis | Use well-audited crypto libraries. ML-DSA-65 signing MAY be deterministic or hedged (§8.3); the Go reference generator is deterministic, while the other SDKs sign with hedged randomization, which hardens against this side channel when host entropy is trustworthy. | Deterministic signing on shared VMs is a known industry-wide risk; hedged signing mitigates it given trustworthy entropy. See §8.3. |
| T11 | **Clock-skew attacker** | Manipulate the verifier's or agent's clock | Accept expired certs or stale challenges | Temporal checks use the verifier's clock. Clock skew beyond the challenge window (300s) causes rejection. See §15.6 for clock discipline requirements. | If the verifier's clock is compromised, temporal checks are meaningless. This is a deployment concern. |
| T12 | **Key-substitution attacker** | Controls or spoofs the channel a verifier uses to obtain principal public keys | Present a chain rooted in an attacker-generated key labeled "Alice" — every signature verifies, because the signatures are genuine over the attacker's key | None at the wire layer. Signature verification proves possession of a private key, not that the key belongs to the claimed principal. Trust bootstrap is a REQUIRED deployment decision — see §15.4. | Verification is only as strong as the verifier's key-discovery channel. A verifier that accepts principal keys from an unauthenticated source is fully spoofable regardless of the cryptography. |

Expand Down
15 changes: 7 additions & 8 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,13 @@ func DeriveID(pub HybridPublicKey) string {
// the same canonical bytes and the corresponding public components; either
// failure rejects the entire signature.
//
// ML-DSA signing in v1 uses the deterministic mode (FIPS 204 §3.4 without
// additional randomness). This gives:
// - Reproducible test vectors: regeneration of fixtures produces byte-
// identical output.
// - Deterministic audit trails: a principal replaying the same cert
// parameters produces the same signature bytes.
// Future versions may add a hedged-randomization option for side-channel
// hardening in hostile environments.
// This Go implementation is the reference fixture generator and signs ML-DSA-65
// deterministically (FIPS 204 §3.4 without additional randomness) so the
// conformance corpus regenerates byte-identically from fixed seeds. The signature
// mode is not fixed by the protocol: the other SDKs sign with hedged randomization,
// and a deterministic and a hedged signature over identical canonical bytes are
// interchangeable to any verifier (SPEC §8.3). Hedged signing is side-channel
// hardening that depends on trustworthy host entropy.
// ============================================================================

// signBoth signs msg with both component private keys and returns a
Expand Down
2 changes: 1 addition & 1 deletion demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ If any two languages disagree on *what* happens (identity_status values, whether

These demos are narrative, not exhaustive. For the rigorous validation:

- `testvectors/v1/` — 79 canonical fixtures, every SDK passes byte-identical.
- `testvectors/v1/` — 79 canonical fixtures; every SDK reproduces the canonical signable bytes and hashes byte-identically and verifies the reference signatures (signature bytes themselves are not regenerated outside the Go reference; see SPEC §8.3).
- Go unit tests: `go test ./...`
- TS conformance: `cd sdks/typescript && npm test`
- Python conformance: `cd sdks/python && pytest`
Expand Down
2 changes: 2 additions & 0 deletions sdks/typescript/README.body.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ A single failure means TypeScript and the Go reference have drifted.

No network code in this package. HTTP concerns (challenge issuance, revocation list fetching, API auth) live one layer up.

**Signing mode.** `@noble/post-quantum` signs ML-DSA-65 with hedged randomization by default, so two signings of the same message produce different bytes. This does NOT affect interop: the signatures verify in Go, Python, Rust, and C/C++, and vice versa. What must match across languages is the canonical signable bytes (SPEC §8.3), and those match byte-for-byte. Only the Go reference generator signs deterministically, so that fixtures regenerate reproducibly; passing `{ extraEntropy: false }` would make this SDK deterministic too, but hedged signing is side-channel hardening and is kept as the default.

## API added on main (ships in alpha.16, release unpublished)

The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; install from `main` to use it ahead of the release.
Expand Down
2 changes: 2 additions & 0 deletions sdks/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ A single failure means TypeScript and the Go reference have drifted.

No network code in this package. HTTP concerns (challenge issuance, revocation list fetching, API auth) live one layer up.

**Signing mode.** `@noble/post-quantum` signs ML-DSA-65 with hedged randomization by default, so two signings of the same message produce different bytes. This does NOT affect interop: the signatures verify in Go, Python, Rust, and C/C++, and vice versa. What must match across languages is the canonical signable bytes (SPEC §8.3), and those match byte-for-byte. Only the Go reference generator signs deterministically, so that fixtures regenerate reproducibly; passing `{ extraEntropy: false }` would make this SDK deterministic too, but hedged signing is side-channel hardening and is kept as the default.

## API added on main (ships in alpha.16, release unpublished)

The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; install from `main` to use it ahead of the release.
Expand Down
12 changes: 9 additions & 3 deletions sdks/typescript/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,16 +338,22 @@ export function keyRotationSignBytes(stmt: KeyRotationStatement): Uint8Array {

/**
* Produce a hybrid signature over `msg` with both component private keys.
* Both components sign identical bytes. ML-DSA-65 uses FIPS 204's canonical
* (non-hedged) variant — matches the Go reference's SignTo(randomized=false).
* Both components sign identical canonical bytes. ML-DSA-65 signing here is
* hedged (randomized) — @noble/post-quantum's default — so two signatures over
* the same input differ. This does not affect interop: the canonical signable
* bytes and the verification outcome are what must match across SDKs, and a
* hedged signature is interchangeable with the Go reference's deterministic one
* to any verifier (SPEC §8.3). Only the Go reference generator signs
* deterministically, for reproducible fixtures.
*/
export async function signBoth(
msg: Uint8Array,
priv: HybridPrivateKey,
): Promise<HybridSignature> {
const edSig = await ed.signAsync(msg, priv.ed25519);
// @noble/post-quantum: sign(msg, secretKey, opts?) — message FIRST.
// Default opts → deterministic signing (matches Go's SignTo(randomized=false)).
// Hedged (randomized) by default; { extraEntropy: false } would make it
// deterministic. We keep the hedged default; see SPEC §8.3.
const mlSig = ml_dsa65.sign(msg, priv.ml_dsa_65);
return { ed25519: edSig, ml_dsa_65: mlSig };
}
Expand Down