Freeze DSH1 crypto specification and implement the core (M1)

docs/crypto.md is now the normative, frozen specification. This had to land before
anything else in M1: the server holds ciphertext and no keys, so it can never
re-encrypt, and a format change after users hold data is a coordinated client rewrite
with no rollback.

Specification:
- DSH1 envelope layout, canonical 64-byte AAD encoding, SealTo construction, key
  hierarchy, Argon2id profiles, fingerprints, and the change rules for each version field.
- AAD encoding is fixed-width binary rather than delimited string concatenation, so no
  field value can forge a field boundary. This supersedes the illustrative form sketched
  in ADR 0001, which now points here.
- UUIDs are RFC 4122 big-endian. Guid.ToByteArray() emits the first three groups
  little-endian and would have made our ciphertext unreadable by any other implementation
  of this spec, failing only at a cross-implementation boundary.

Verified rather than assumed:
- PrimitiveAvailabilityTests proves X25519, Ed25519, XChaCha20-Poly1305, Argon2id and
  HKDF-SHA512 all function on net10.0. NSec 26.4.0 targets net9.0 and is consumed by
  forward compatibility; this closes one of the two package questions the plan flagged.
- Argon2Profile exists because NSec's MemorySize is in KIBIBYTES, not bytes. Passing bytes
  gives either a 256 GiB allocation or a 256 KiB KDF that cracks instantly. The type takes
  mebibytes so the unit cannot be got wrong at a call site. Found by benchmarking: the
  first measurements were ~1000x too slow, which turned out to be 19 GiB of work.
- Parameters measured, not guessed: 256 MiB/t=4 is 323 ms on this machine; the table of
  candidates is in the spec.

Implementation and tests (83 total, up from 17):
- AadDescriptor, DshEnvelope, DshCrypto (Seal/Open/SealTo/OpenSealed/fingerprints).
- Decryption returns null rather than throwing: ciphertext comes from a server that is
  explicitly not trusted, so a failed tag is an expected outcome.
- Envelope readers reject unknown algorithms and any non-zero flag bit, so an envelope
  that is not fully understood fails closed.
- Executable form of the spec's substitution claims: a server cannot move ciphertext
  between resources, roll back a key generation or item version, repurpose a payload as
  metadata, or confuse the two constructions.
- Golden vectors in tests/fixtures/crypto/vectors.json guard the format. Mutation-checked:
  a one-byte schema version change trips four tests including the guard.

Two build-infrastructure bugs found and fixed along the way:
- .editorconfig forced camelCase on const and static readonly fields. PascalCase is the
  .NET convention for both; the config was wrong, not the code.
- The golden fixture was resolved with [CallerFilePath], which ContinuousIntegrationBuild
  rewrites to /_/... under deterministic source paths. It passed locally and would have
  failed only in CI. Now copied to the output directory and read from there.
This commit is contained in:
2026-07-28 13:18:29 +02:00
parent ce43f397a6
commit b15af836a3
21 changed files with 2589 additions and 30 deletions
+190
View File
@@ -0,0 +1,190 @@
{
"_comment": "Generated by DodoSSH.Crypto.Tests.GoldenVectors. Normative source: docs/crypto.md.",
"_warning": "A failing assertion here is a regression or an intentional versioned format change. Do not regenerate to make it pass.",
"specVersion": 1,
"aad": [
{
"name": "item-payload",
"purpose": 4,
"resourceType": 5,
"resourceId": "0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"keyId": "0192f0c8-9999-7aaa-8bbb-cccccccccccc",
"keyGeneration": 7,
"itemVersion": 3,
"aadVersion": 1,
"schemaVersion": 1,
"canonicalEncoding": "647368310a0104050192f0c81a2b7c3d8e4f5a6b7c8d9e0f0192f0c899997aaa8bbbcccccccccccc000000070000000300010000000000000000000000000000",
"aad": "bb106e753e2fd9ac31142889a4356cbf9fc1f8db3777a1921ecbb5481bd4379e"
},
{
"name": "item-payload-other-resource",
"purpose": 4,
"resourceType": 5,
"resourceId": "0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e10",
"keyId": "0192f0c8-9999-7aaa-8bbb-cccccccccccc",
"keyGeneration": 7,
"itemVersion": 3,
"aadVersion": 1,
"schemaVersion": 1,
"canonicalEncoding": "647368310a0104050192f0c81a2b7c3d8e4f5a6b7c8d9e100192f0c899997aaa8bbbcccccccccccc000000070000000300010000000000000000000000000000",
"aad": "ae87f8da1a55b36286ed103a11fb51145adff18a222557db30422918eba6c29f"
},
{
"name": "item-metadata",
"purpose": 5,
"resourceType": 4,
"resourceId": "0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"keyId": "00000000-0000-0000-0000-000000000000",
"keyGeneration": 1,
"itemVersion": 1,
"aadVersion": 1,
"schemaVersion": 1,
"canonicalEncoding": "647368310a0105040192f0c81a2b7c3d8e4f5a6b7c8d9e0f00000000000000000000000000000000000000010000000100010000000000000000000000000000",
"aad": "cfb042451484a4f484b45e812a2c7667d12592bcb8ec47be5b5ef95c38b1d3ad"
},
{
"name": "vault-key-grant",
"purpose": 2,
"resourceType": 3,
"resourceId": "0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"keyId": "00000000-0000-0000-0000-000000000000",
"keyGeneration": 2,
"itemVersion": 0,
"aadVersion": 1,
"schemaVersion": 1,
"canonicalEncoding": "647368310a0102030192f0c81a2b7c3d8e4f5a6b7c8d9e0f00000000000000000000000000000000000000020000000000010000000000000000000000000000",
"aad": "5c9444daa7f74193b04abefb57d5a49a8782e8e1a7fd97771865f9e038f8c957"
},
{
"name": "user-secret-bundle",
"purpose": 1,
"resourceType": 1,
"resourceId": "0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"keyId": "00000000-0000-0000-0000-000000000000",
"keyGeneration": 1,
"itemVersion": 0,
"aadVersion": 1,
"schemaVersion": 1,
"canonicalEncoding": "647368310a0101010192f0c81a2b7c3d8e4f5a6b7c8d9e0f00000000000000000000000000000000000000010000000000010000000000000000000000000000",
"aad": "9f73034823c49cdfcad4fcc75e67ae22be151a92afed72ab7548097ef5a99f68"
},
{
"name": "all-zero-ids",
"purpose": 6,
"resourceType": 0,
"resourceId": "00000000-0000-0000-0000-000000000000",
"keyId": "00000000-0000-0000-0000-000000000000",
"keyGeneration": 0,
"itemVersion": 0,
"aadVersion": 1,
"schemaVersion": 1,
"canonicalEncoding": "647368310a0106000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000",
"aad": "cebc8d57709c0ebe47874c85fc39aa538e17c4202b767673c8636ef181cd1664"
}
],
"envelope": [
{
"name": "xchacha20poly1305",
"algId": 1,
"nonce": "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7",
"ciphertext": "000102030405060708090a0b0c0d0e0f10111213",
"prefixSize": 30,
"envelope": "445348310100a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7000102030405060708090a0b0c0d0e0f10111213"
},
{
"name": "aes256gcm",
"algId": 2,
"nonce": "b0b1b2b3b4b5b6b7b8b9babb",
"ciphertext": "000102030405060708090a0b0c0d0e0f10111213",
"prefixSize": 18,
"envelope": "445348310200b0b1b2b3b4b5b6b7b8b9babb000102030405060708090a0b0c0d0e0f10111213"
},
{
"name": "sealto-x25519",
"algId": 3,
"nonce": "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7",
"ephemeralPublicKey": "c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf",
"ciphertext": "000102030405060708090a0b0c0d0e0f10111213",
"prefixSize": 62,
"envelope": "445348310300c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7000102030405060708090a0b0c0d0e0f10111213"
}
],
"aead": [
{
"name": "xchacha20poly1305-with-canonical-aad",
"key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
"nonce": "101112131415161718191a1b1c1d1e1f2021222324252627",
"aad": "bb106e753e2fd9ac31142889a4356cbf9fc1f8db3777a1921ecbb5481bd4379e",
"plaintext": "636f727265637420686f727365206261747465727920737461706c65",
"ciphertext": "4793718431eb55f3feed50be98b0416d7bff929d804d53a7873495132465b6b1da6e73e042821964543ecd90",
"envelope": "445348310100101112131415161718191a1b1c1d1e1f20212223242526274793718431eb55f3feed50be98b0416d7bff929d804d53a7873495132465b6b1da6e73e042821964543ecd90"
}
],
"hkdf": [
{
"name": "passphrase-kek",
"algorithm": "HKDF-SHA512-Expand",
"prk": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"info": "dsh1/kek/passphrase/v1",
"outputLength": 32,
"output": "652b3a4a3ce03b235095ad32f1eed2cfdae915b5b0a98cc9f96face30853f4c7"
},
{
"name": "local-cache",
"algorithm": "HKDF-SHA512-Expand",
"prk": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"info": "dsh1/localcache/v1",
"outputLength": 32,
"output": "5b69ed9266ff5f297f11667ca693b0049b805365ee34d54d6e60b843e414b1f5"
}
],
"argon2id": [
{
"name": "passphrase-default",
"memoryMebibytes": 256,
"memoryKibibytes": 262144,
"passes": 4,
"parallelism": 1
},
{
"name": "passphrase-reduced",
"memoryMebibytes": 128,
"memoryKibibytes": 131072,
"passes": 3,
"parallelism": 1
},
{
"name": "passphrase-high",
"memoryMebibytes": 512,
"memoryKibibytes": 524288,
"passes": 4,
"parallelism": 1
},
{
"name": "random-secret",
"memoryMebibytes": 64,
"memoryKibibytes": 65536,
"passes": 3,
"parallelism": 1
},
{
"name": "random-secret-output",
"memoryMebibytes": 64,
"memoryKibibytes": 65536,
"passes": 3,
"parallelism": 1,
"passphrase": "correct horse battery staple",
"salt": "202122232425262728292a2b2c2d2e2f",
"outputLength": 32,
"output": "3573a601a50874c6c4222082d040f039ba4f557a0151e0357e8abb66fed7b29e"
}
],
"fingerprint": [
{
"name": "identity-fingerprint",
"x25519PublicKey": "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
"ed25519PublicKey": "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f",
"fingerprint": "fe8d8673f517688bf0d5d9b812327619a303c765af1f47dbd6a777db193c36e5"
}
]
}