From 16ab8f24c5c5c06ef8165aeb5bf806ba7eb7d332 Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 10:44:26 +0300 Subject: [PATCH 01/10] Release MPL-2.0 core with concise docs and crypto validation fixes --- CONTRIBUTING.md | 8 +- Cargo.lock | 16 +- Cargo.toml | 4 +- EXPORT-INVENTORY.json | 214 ++++---- LICENSE | 514 +++++++++++++----- LICENSE-OPENCRATE | 141 ----- README.md | 25 +- crates/oc-crypto/Cargo.toml | 3 +- crates/oc-crypto/LICENSE | 373 +++++++++++++ crates/oc-crypto/README.md | 10 +- crates/oc-crypto/src/aead.rs | 31 +- crates/oc-crypto/src/agreement.rs | 1 + crates/oc-crypto/src/kdf.rs | 311 ++--------- crates/oc-crypto/src/lib.rs | 1 + crates/oc-crypto/src/mac.rs | 1 + crates/oc-crypto/src/merkle.rs | 129 +---- crates/oc-crypto/src/mlkem_p256.rs | 59 +- crates/oc-crypto/src/rsa.rs | 41 +- crates/oc-crypto/src/rsa_test_signer.rs | 1 + crates/oc-crypto/src/seal.rs | 97 ++-- crates/oc-crypto/src/secret.rs | 1 + crates/oc-crypto/src/sign.rs | 1 + crates/oc-crypto/src/stream.rs | 33 +- crates/oc-crypto/src/tpm.rs | 1 + crates/oc-crypto/src/transcript.rs | 1 + crates/oc-crypto/src/wrap.rs | 1 + crates/oc-crypto/src/xwing.rs | 28 +- crates/oc-crypto/tests/kat.rs | 1 + .../oc-crypto/tests/measure_packing_path.rs | 1 + crates/oc-crypto/tests/review_regressions.rs | 60 ++ .../tests/security_probe_keyschedule.rs | 1 + crates/oc-crypto/tests/security_probe_seal.rs | 1 + .../security_probe_secretbuf_and_shape.rs | 1 + .../tests/security_probe_tree_signatures.rs | 1 + crates/oc-crypto/tests/spec_labels.rs | 1 + crates/oc-crypto/tests/zeroize_on_drop.rs | 1 + crates/oc-engine/Cargo.toml | 9 +- crates/oc-engine/LICENSE | 373 +++++++++++++ crates/oc-engine/README.md | 8 +- crates/oc-engine/src/edit.rs | 1 + crates/oc-engine/src/lib.rs | 63 +-- crates/oc-engine/src/wire.rs | 46 +- crates/oc-engine/tests/edit_writer.rs | 1 + .../oc-engine/tests/hardware_hybrid_golden.rs | 68 +-- crates/oc-engine/tests/p256_author_golden.rs | 95 +--- crates/oc-format/Cargo.toml | 9 +- crates/oc-format/LICENSE | 373 +++++++++++++ crates/oc-format/README.md | 8 +- crates/oc-format/examples/verify-header.rs | 1 + crates/oc-format/src/content.rs | 1 + crates/oc-format/src/edit.rs | 1 + crates/oc-format/src/encoding.rs | 35 +- crates/oc-format/src/footer.rs | 1 + crates/oc-format/src/frame.rs | 1 + crates/oc-format/src/header.rs | 187 ++----- crates/oc-format/src/lib.rs | 85 +-- crates/oc-format/src/policy_codec.rs | 122 +---- crates/oc-format/src/text.rs | 37 +- crates/oc-format/src/tlv.rs | 1 + crates/oc-format/src/verify.rs | 82 +-- crates/oc-format/tests/edit.rs | 1 + crates/oc-format/tests/kat_policy.rs | 1 + crates/oc-format/tests/parser_robustness.rs | 1 + .../tests/security_probe_hostile_input.rs | 1 + .../security_probe_policy_enforcement.rs | 1 + .../tests/skeptic_recheck_slot_tags.rs | 1 + crates/oc-policy/Cargo.toml | 3 +- crates/oc-policy/LICENSE | 373 +++++++++++++ crates/oc-policy/README.md | 10 +- crates/oc-policy/examples/access-policy.rs | 1 + crates/oc-policy/src/lib.rs | 19 +- .../oc-policy/tests/security_probe_skeptic.rs | 1 + .../tests/server_policy_composition.rs | 1 + crates/oc-protocol/Cargo.toml | 9 +- crates/oc-protocol/LICENSE | 373 +++++++++++++ crates/oc-protocol/README.md | 10 +- crates/oc-protocol/src/access.rs | 36 +- crates/oc-protocol/src/action.rs | 50 +- crates/oc-protocol/src/activation.rs | 51 +- crates/oc-protocol/src/agent.rs | 43 +- crates/oc-protocol/src/attestation.rs | 1 + crates/oc-protocol/src/attribute_rule.rs | 1 + crates/oc-protocol/src/control.rs | 1 + crates/oc-protocol/src/directory.rs | 1 + crates/oc-protocol/src/lease.rs | 34 +- crates/oc-protocol/src/lib.rs | 102 +--- crates/oc-protocol/src/order.rs | 1 + crates/oc-protocol/src/replica.rs | 1 + crates/oc-protocol/src/revocation.rs | 1 + crates/oc-protocol/src/standing.rs | 1 + crates/oc-protocol/src/unknown.rs | 33 +- crates/oc-protocol/src/witness.rs | 1 + crates/oc-protocol/tests/action_vectors.rs | 1 + crates/oc-protocol/tests/agent_vectors.rs | 1 + crates/oc-protocol/tests/control_vectors.rs | 1 + crates/oc-protocol/tests/kat_operation.rs | 1 + crates/oc-protocol/tests/lease_attestation.rs | 1 + .../oc-protocol/tests/lease_server_policy.rs | 1 + crates/oc-protocol/tests/lease_signature.rs | 1 + crates/oc-protocol/tests/session_mac_table.rs | 1 + crates/oc-protocol/tests/unknown_tags.rs | 1 + crates/opencrate/Cargo.toml | 16 +- crates/opencrate/LICENSE | 373 +++++++++++++ crates/opencrate/README.md | 103 +++- crates/opencrate/examples/seal-file.rs | 1 + crates/opencrate/src/lib.rs | 1 + deny.toml | 67 +-- docs/getting-started.md | 16 +- docs/licensing.md | 86 +-- docs/review-2026-09-26.md | 47 ++ 110 files changed, 3426 insertions(+), 2179 deletions(-) delete mode 100644 LICENSE-OPENCRATE create mode 100644 crates/oc-crypto/LICENSE create mode 100644 crates/oc-crypto/tests/review_regressions.rs create mode 100644 crates/oc-engine/LICENSE create mode 100644 crates/oc-format/LICENSE create mode 100644 crates/oc-policy/LICENSE create mode 100644 crates/oc-protocol/LICENSE create mode 100644 crates/opencrate/LICENSE create mode 100644 docs/review-2026-09-26.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46768f4..c0f5d48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,10 +11,10 @@ Do not update frozen vectors merely to make a failing test pass. A format change requires a documented compatibility decision. Add dependencies deliberately and review their licenses, advisories and effect on the no-I/O boundary. -Only contribute code you have the right to license. Commercial -licensing requires a contributor-rights policy before external code -contributions are accepted. Until then, please open an issue for design feedback -before sending code. No contributor agreement is implied by this document. +Contribute only code you have the right to license under MPL-2.0. +Explain the reason for a check or constraint in a short comment; keep change +history in commits or design documents. Keep safety contracts and wire formulas +close to the code they constrain. Formatting is not a repository-wide gate yet: inherited source has existing formatting differences. Keep edits focused; do not mix bulk reformatting with diff --git a/Cargo.lock b/Cargo.lock index 6595b97..859687c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "oc-crypto" -version = "0.0.2" +version = "0.0.3" dependencies = [ "blake3", "chacha20poly1305", @@ -456,7 +456,7 @@ dependencies = [ [[package]] name = "oc-engine" -version = "0.0.2" +version = "0.0.3" dependencies = [ "oc-crypto", "oc-format", @@ -467,7 +467,7 @@ dependencies = [ [[package]] name = "oc-format" -version = "0.0.2" +version = "0.0.3" dependencies = [ "oc-crypto", "oc-policy", @@ -477,11 +477,11 @@ dependencies = [ [[package]] name = "oc-policy" -version = "0.0.2" +version = "0.0.3" [[package]] name = "oc-protocol" -version = "0.0.2" +version = "0.0.3" dependencies = [ "oc-crypto", "oc-format", @@ -490,7 +490,7 @@ dependencies = [ [[package]] name = "opencrate" -version = "0.0.2" +version = "0.0.3" dependencies = [ "oc-crypto", "oc-engine", @@ -502,9 +502,7 @@ dependencies = [ [[package]] name = "opencrate-sdk" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6336e88161e12189c79f03536f7c8e023efdbf1892b7e524047435ac4b9a65ab" +version = "0.0.3" dependencies = [ "getrandom", "oc-crypto", diff --git a/Cargo.toml b/Cargo.toml index eb906dd..c0f18a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,10 @@ members = ["crates/oc-crypto", "crates/oc-format", "crates/oc-protocol", "crates oc-crypto = { path = "crates/oc-crypto" } [workspace.package] -version = "0.0.2" +version = "0.0.3" edition = "2024" rust-version = "1.90" -license-file = "LICENSE-OPENCRATE" +license = "MPL-2.0" publish = true [workspace.lints.rust] diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 37120d8..42c5bc3 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -139,107 +139,113 @@ ".github/ISSUE_TEMPLATE/licensing-enquiry.yml": "c4686b43c2b39718ab9b86713461ad919a10415c6b6a1b5ecdc4183b449e2524", ".github/workflows/ci.yml": "37f74c0187b1130489f60dc325d10f3d5c1f47e58df507e470cc5edb53542a33", ".gitignore": "ac05572173ab2ab7437511f230c35c4d767ebb9611496581f7bc9e54c57bc24a", - "CONTRIBUTING.md": "72a29ff17d21e903057986be6ee97cdfe7c20f519cc4845b0a58a9831a7c0eb7", - "Cargo.lock": "ce1a55412b61ad14b36e2bee4f617a6d87f95cfbaa3a04aee784ba5d0d0fc9b0", - "Cargo.toml": "c7f50ae96d964d3b5db205e5147a6ba17545d10bc161e44a736fe4b85f54c12b", - "LICENSE": "eebdd6a75537205ccfccdaac674d895247aded560225a519a01300b2e46bc70f", - "LICENSE-OPENCRATE": "eebdd6a75537205ccfccdaac674d895247aded560225a519a01300b2e46bc70f", - "README.md": "263456986a9dc156b5a85931d76933c8b971ef996ed03169bff6b99722dae1e7", + "CONTRIBUTING.md": "ffa723ec1487243ea5fdde2ffdd36d00c59ea2c076de5baf08a1bf1b67c1a098", + "Cargo.lock": "9c3d00291575bdb2daea76987ec12614a759b1f29002eb8e020f7c0f6a39ff18", + "Cargo.toml": "04d40e283aae2e3dc6facffcb4b0cf84f3d5caac3f483acde09f9e92bd954ff0", + "LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "README.md": "7628c9b38b4910d357569535374f1f9520e4822dfb18f8f0d32b1537e3d2ae73", "SECURITY.md": "839c254e62ea1cfa4628520ca6d1e145c5fc1f6b109ab4f2f9c711b3db26d930", "clippy.toml": "447c1c2ae9dc0dfef9bae35ca82cb04f08c09bf7943d3835823138b546c03621", - "crates/oc-crypto/Cargo.toml": "22a3f3142203716749ea5f7e9d626ae2b270d23f4f54f68f4db0b511d8bd5de2", - "crates/oc-crypto/README.md": "cb87aec71ad72c033f2c23ac02d2aeaf7fcffb6db6509f689e062ecc0e6bce83", - "crates/oc-crypto/src/aead.rs": "173799c3e6ce7bf46058377cdd54c0e9d43408752341cd49f720ebce74043ca8", - "crates/oc-crypto/src/agreement.rs": "469d4d3f4c1e3aa402ad7cd12e99653ab539fd879ce3b83afcd896b63fbc0aa6", - "crates/oc-crypto/src/kdf.rs": "9de22b06f3ac9ff2a5a105811df9a218509061c26e871abf2c8a949c0d75bcc0", - "crates/oc-crypto/src/lib.rs": "7276444188187be08c40a13541aa1c73e9da61f2741caf620667cc53c41181c1", - "crates/oc-crypto/src/mac.rs": "f4fd579fe91b90ea93af0cdff14266da639282e99bb7ad495e7b2b1cbb38a510", - "crates/oc-crypto/src/merkle.rs": "a66f23b25ac34fda78fe3678a1a744649e7728ea682c1ce9612f956d2cfada48", - "crates/oc-crypto/src/mlkem_p256.rs": "1d71b3e5625cfc83cc59de05923c7723335f95a49b3c3ede71fbe10054bf9f87", - "crates/oc-crypto/src/rsa.rs": "edab40364fedba4193630b36696fc267cebc46c3e75dcac7280f593d3e1ed444", - "crates/oc-crypto/src/rsa_test_signer.rs": "afd5e822568b342e5f6f698b7c73b88918b70b893cba856880840cc38bd441a3", - "crates/oc-crypto/src/seal.rs": "42cdef76a5f3560ce2fc2585524eec1523f5dd509e7637953a778d9015eb5d19", - "crates/oc-crypto/src/secret.rs": "0561d4a4e88d81d1867aee4262e600ad419b5972ac74f0e9e7e1965bb8ac02dd", - "crates/oc-crypto/src/sign.rs": "9f3e20121ec74a4ee172de8d4bb7388b73ee3fc135f8a9a980813620f0dc288e", - "crates/oc-crypto/src/stream.rs": "c7f13f0d22416fdb6356cea4c7e672eb9ea3369a3bc3e4328b8ca018a8fa7a1d", - "crates/oc-crypto/src/tpm.rs": "5addad71a6add491a854a1239b7d4d1c78b7cdca00d0cd1e8a436aa1b4917a7f", - "crates/oc-crypto/src/transcript.rs": "f9b67a10789559f1fdea8ac46e3b07fe6dfafe7730c9637c67ea28b11e44a347", - "crates/oc-crypto/src/wrap.rs": "eb860fd056219483df17ee3db198cb09ece6bb8f6680162d8465c8e20897cf99", - "crates/oc-crypto/src/xwing.rs": "0b252e66c1a799c588d18810a9269d347dbae172b4055e44378287fd8a33329b", - "crates/oc-crypto/tests/kat.rs": "6b52dc543b8ce37e7c3d2ec814183c37ad37d0344a825afe4a3e778d057a4b23", - "crates/oc-crypto/tests/measure_packing_path.rs": "a8a05f654cb64b0b8139dc24daffd1a4cceedf98040653391b2a01b926d45d7f", - "crates/oc-crypto/tests/security_probe_keyschedule.rs": "901b7672a69263d4e0b5160a055d72c06fa606a37ba70e96b7e7778d4f79c355", - "crates/oc-crypto/tests/security_probe_seal.rs": "3fac800232b93ff554513c9add8a6cd548c40a8684b1cf5e70c939fd689336c6", - "crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs": "a4b609d862ec7c6e56fe9319618679bf3065560f5b760a3bb07a71a0673d901b", - "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "e8d9f1d83bb258002cc26193b005164aed7bfb013939ca0a8dc0fb665e2dd1d1", - "crates/oc-crypto/tests/spec_labels.rs": "cbe4cb4198efddf426b02d25e92c9d59dc867065a76c3dee24cb31e5a8b8034d", - "crates/oc-crypto/tests/zeroize_on_drop.rs": "fa1bd7770cfaf01f08b24cb8131c848fd26f551dbf7f2b94a016ad9aff43d679", - "crates/oc-engine/Cargo.toml": "f64d88174eacceb868beb3621c98c45fb80a7de830d4b3128a7ef621da1a8232", - "crates/oc-engine/README.md": "2676481c405099b07528b277b459c2723f4061b1c85e7b3edf390d0a74a1419e", - "crates/oc-engine/src/edit.rs": "0ca917d571b1a6845247c7ef0b850f216df16a893d6be4b6a37c2b316b739aac", - "crates/oc-engine/src/lib.rs": "cf10dda5a19e795f98823b3a42ea05847c421ad8343b03931917d8817ad38798", - "crates/oc-engine/src/wire.rs": "5a015ab02a5bd7249d09ffddb0b3fc4f18cc718e264ce652148eabba31775ec8", - "crates/oc-engine/tests/edit_writer.rs": "a2a4a298ff9e7f745847785283aaf62bf8a7aebfefbe5e501bdab83870aebde3", - "crates/oc-engine/tests/hardware_hybrid_golden.rs": "95d20e98b857b065b0ba02e32a7685018be8394414045537e143b8188156337d", - "crates/oc-engine/tests/p256_author_golden.rs": "6bf77dcc1e172715c31e088fb044d34e11d0f14dd98ba004b93ca46e0dd56ea7", - "crates/oc-format/Cargo.toml": "92fed5d3bcd9fc6c4fb5404831b762adad14213d0390f558f52f53aa6825ca2e", - "crates/oc-format/README.md": "36af480e569969e4ec58f501e6beb3b2045d83569ba0d0c101bca3e4f1712567", - "crates/oc-format/examples/verify-header.rs": "82fef149cdae9b3fbf75db7ce7ad77c3adf13c3cbcf2c20998f4d53f63f9d713", - "crates/oc-format/src/content.rs": "c71922d037cfebb63941634a6e8c38b7d1d1a38d1c120da84d632b0e8896866a", - "crates/oc-format/src/edit.rs": "e750af07d3624c1b41054ef3e88354ee2450285e94e643cee5a12170553603a6", - "crates/oc-format/src/encoding.rs": "afa7b4b03710050820d9d49eda04df29af919932b6e3ecc984842a346dd00b95", - "crates/oc-format/src/footer.rs": "389be327ce20894677dd2876c2446407de81553e58abe24456fc7b4b01e28753", - "crates/oc-format/src/frame.rs": "e554a898c489f039281001d0d33877c83d816582562ff55824f1eec6ceb248eb", - "crates/oc-format/src/header.rs": "6001fb608261d4a85607695a7f1a7add8af9d5c70230022e5514a6acdecb2b91", - "crates/oc-format/src/lib.rs": "d8f0f71e79094dc0786b9df06d6f54bf1c224aafb06d18a9ae8521d6e3a55866", - "crates/oc-format/src/policy_codec.rs": "2955fef8cad3a6b31e53f494f1b596597892345c4bd2c316fdabd265f5100aad", - "crates/oc-format/src/text.rs": "880c9343e85a8c6539b55f28ea3b33aca06e7934c44cf6ab984c12ad1f83b5f3", - "crates/oc-format/src/tlv.rs": "48b9f593859be97b07f6ffcbe364eccbc73bc292a1436c18fc08338a4871ca4a", - "crates/oc-format/src/verify.rs": "3664d124fc689c14a354014af9d3be6a4badca5d98f6dcd495520b526fff874e", - "crates/oc-format/tests/edit.rs": "cf529249ebf882274e8b96a1a6dfd8e0df84cc23b0b62b765235cfbfd7b86e7c", - "crates/oc-format/tests/kat_policy.rs": "ed2c31ac3856787e09260f75976ce4aaf5979e58e880d83d91311bc49d0fb33d", - "crates/oc-format/tests/parser_robustness.rs": "068da6d5b5f830c769c60443503ed30782238ef05a2a4c0bcd52ae55dbd328a4", - "crates/oc-format/tests/security_probe_hostile_input.rs": "7a8f2c99d9720af5c6e1bd2d0b023a0bfd3fc7e0de780c7cd558bcf3fc430058", - "crates/oc-format/tests/security_probe_policy_enforcement.rs": "0fc8d6e4b0c2b71a552ef2deba8972208f42f787f374b1084f5703ea1541c6e2", - "crates/oc-format/tests/skeptic_recheck_slot_tags.rs": "89ea63efa807d1e4c44c95b27df6677d77b2ccda7ee58a18677c29a9fb0e635f", - "crates/oc-policy/Cargo.toml": "daf309b367433dbb51d016c490acf6eff8f855a24b22e9bedbdedbc8a29952f7", - "crates/oc-policy/README.md": "781d826f5467727ee848d0f0ebe294ef0f1fdf8d7b526caf76098fff5327a16a", - "crates/oc-policy/examples/access-policy.rs": "dc2912832af92b86d394e1bb48aaaa36e8b10e6342fc908862b455d7b3a928bd", - "crates/oc-policy/src/lib.rs": "df6149fbd31b71cf1cf9016c0dbfcce1d134d5c86bb4810706292edc77346df7", - "crates/oc-policy/tests/security_probe_skeptic.rs": "643226418b27a4afdd2f4c04de37770fd5f14fea84ffa8ed9e01fbd09abff35c", - "crates/oc-policy/tests/server_policy_composition.rs": "41a7447828ced259660829ada1d950ff29fbf6705fec2883c18be0768e49d6fb", - "crates/oc-protocol/Cargo.toml": "e75d306598726e2ed014da0fbeb1327123437c3414516e0204353d7ff3f661cd", - "crates/oc-protocol/README.md": "38315a2a4544e9adc1e786e94a7d6741fdaf8dc3c1e5c259abbb79a964ce3352", - "crates/oc-protocol/src/access.rs": "d80cee99a7103d751f283d4f140714f7342072b24167966d08779f1c96477eb7", - "crates/oc-protocol/src/action.rs": "d5ad66ed3090f7bac3f9acc59450a017ba076b02ef4522db30d68aa8c1423af2", - "crates/oc-protocol/src/activation.rs": "b2a89c2de5907eb490a0e367e2f583504122fb53cb74dc0e53fe891f9e376e96", - "crates/oc-protocol/src/agent.rs": "b2c56f403086988ab8fe8495506e9c41e441fb1965fa8870b70470f7704f4336", - "crates/oc-protocol/src/attestation.rs": "95a190832e76bfe52a8e221c3008bdcf71306fc176ee4bf05f1502780257a061", - "crates/oc-protocol/src/attribute_rule.rs": "865d89358e20d5a418b0419c0f0f79c0f9fdbeea45194e7947208952b3fbb3db", - "crates/oc-protocol/src/control.rs": "630d6e38e4577620c21093138f25abeb8910ef9ae99cd37b97757ef601be66c4", - "crates/oc-protocol/src/directory.rs": "cadc721a38fbb35a418526fabc8fe8daaee81cdc0c19cf2c994b2972c32b2276", - "crates/oc-protocol/src/lease.rs": "d278eaf560e4a15088c0b4658c645802875424e92a13b85dea6442a343f87306", - "crates/oc-protocol/src/lib.rs": "00226402a94c25268c62fcd38e0ee4bfb240b99a39f25b06c5189c98cbdaf451", - "crates/oc-protocol/src/order.rs": "6a63a4e7dfc333247d4f88365868f7775d177a313a1660196aa8051de665cd77", - "crates/oc-protocol/src/replica.rs": "69ab4b5e077e0894564fb67c9d3a76794432ee6cad0b77fc7e6e992734ee842c", - "crates/oc-protocol/src/revocation.rs": "3362a46da449d26a48739891f83e8e3c92c3ff088f856a5106579662a60971ef", - "crates/oc-protocol/src/standing.rs": "b4c24362b6576f5d3dad11d71b4b146fb1468d54e32dff782307fd73c6d5a82f", - "crates/oc-protocol/src/unknown.rs": "4d7237f371e08ace27210905ccd84f2eeca6f72c38d3d08ad8f371271561e3c3", - "crates/oc-protocol/src/witness.rs": "f2438caa33106ec5140b1141c120e49f9b0048bda81875da7339020c6991db89", - "crates/oc-protocol/tests/action_vectors.rs": "edd01cc6e93010b456601d0e7599a54bde82ae1652d08691dced77046ef854fd", - "crates/oc-protocol/tests/agent_vectors.rs": "377b4f6729c9f94a422abd079cffa32d94eee60f4e17d78f08ef9d4a8bb83219", - "crates/oc-protocol/tests/control_vectors.rs": "2fec944073dcd32335c90648d8d5c3cebee29bc6d73a552b60581c29b04d1fa7", - "crates/oc-protocol/tests/kat_operation.rs": "1880e87524330975393031a9e232c951d979ba65140b9d0fe0b1153e96bc276d", - "crates/oc-protocol/tests/lease_attestation.rs": "cd2bc28d5e4c2c47fe893fab3ba8bd739e0d4c19f63af2825046bdefc1faa503", - "crates/oc-protocol/tests/lease_server_policy.rs": "8fcda983cace7e4482f64ecb9a90df452e855ff77734d591cb378d25cdb42cef", - "crates/oc-protocol/tests/lease_signature.rs": "a7136a640483be24523e3bfc64a6027a41e6efe8681527d4fd01d29e3ce947e2", - "crates/oc-protocol/tests/session_mac_table.rs": "9ee07caece5581e02de07e32a8f1833ab8ed47dc60b62ddf8ee57723ceb38807", - "crates/oc-protocol/tests/unknown_tags.rs": "de2e3a346d0db0f9380974f675ac6e6b21095d0b6c46cc6c5a7f43953c303aa0", - "crates/opencrate/Cargo.toml": "2c8fe575cd61706f75665947501cc660b9d187becc0e6a213c4f4d3b82cb1675", - "crates/opencrate/README.md": "cde5a1df4285070791ff32fa4cdd92c594672d3a5506f2573cb316802b5cc436", - "crates/opencrate/examples/seal-file.rs": "fd435aaee04d62b3a2dbae23a4c58c38ad5bee00a1f54a96c8eb168d43a43b5c", - "crates/opencrate/src/lib.rs": "1a64b27375a63aad5396bbb56f6dbf36f643ab8e3bf0550351a523905993c8fb", - "deny.toml": "8c5d560e337a419a378c0cd65fdc513046b59953c43b8210dea95e23f8af47b8", + "crates/oc-crypto/Cargo.toml": "63a4268cf85daca35746b15080aee8b51fd703e59ae3a08ed76cf479f7970804", + "crates/oc-crypto/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "crates/oc-crypto/README.md": "9384c17fef50f17bbf95132c02b63c2a412c7515842f4f9b3d37a4539e1b12cf", + "crates/oc-crypto/src/aead.rs": "7ec9b62343d04b441965a598309122593e4b910e193346acfcfa363cdece4207", + "crates/oc-crypto/src/agreement.rs": "dfc0e652f6ff4653fd66c413b2eed08b084134a09e2b10c7ceb4b156ffcc7a49", + "crates/oc-crypto/src/kdf.rs": "589b16690fdc8b8901676831609c8b1813072120a71c97a57601382c443eb3a1", + "crates/oc-crypto/src/lib.rs": "a79823c2e5a733606a8416848a803bcfd17c6fb9457b485fd885ab3bfc0c1bbf", + "crates/oc-crypto/src/mac.rs": "75d6636bfbcbe9d7f9cd307ce5101d9d15e2023553038f42fe90a08efeb08edb", + "crates/oc-crypto/src/merkle.rs": "63e117f274790f36943e7d0e69897241049ab102c14bcae958d1f80941fe93d2", + "crates/oc-crypto/src/mlkem_p256.rs": "cd83d36aba74857fac878c66f7a4d5c3624bf8b9a053541deb1d22bec258a823", + "crates/oc-crypto/src/rsa.rs": "0a36fc1b91c322ac0a09fdb9c5b47db79b15b2c4a27129af83ab75a1d0ddc8f4", + "crates/oc-crypto/src/rsa_test_signer.rs": "c34082bb4d6cd0df456a31eb4af37ca9721b8dc56162a9d6821cf3fcb6fd7f93", + "crates/oc-crypto/src/seal.rs": "8abd16b224ea7c96d45ad5df647c59cbff51f5ad8d93fea81302779de7ccf1eb", + "crates/oc-crypto/src/secret.rs": "55bff2d293719d34fcb2de7780a4dce7d81c0c0420b5c5da3b6b2dda83bcb9dd", + "crates/oc-crypto/src/sign.rs": "e231b7a66a86039c1a4e9830bbb3470250a23f046dccc70f1a6c260fc1ef54db", + "crates/oc-crypto/src/stream.rs": "6269ea48bdd1d9e494c4a8fa91bfcd73d6b84bd32d01223f686816972638a38f", + "crates/oc-crypto/src/tpm.rs": "73a0c2aa437bf92b67d4967fcb4ff463b77801b8690db9a47b19fc0bef8022b1", + "crates/oc-crypto/src/transcript.rs": "54ed996ac650c3b5a4aa0e804508603981ef3a596b2e9fd52d24953657a733af", + "crates/oc-crypto/src/wrap.rs": "99138975a39b1f6c671a88a1756defe9edf8a17b6f601d2d3ee00b8dbdaf06f2", + "crates/oc-crypto/src/xwing.rs": "139884797ecbdbe12d22425812e775a182ccf7b1d7d12f488982137f3539c449", + "crates/oc-crypto/tests/kat.rs": "0dc188ad01491e554049b078cfb3647b4b51de5e57715949d178bd10ff562b2a", + "crates/oc-crypto/tests/measure_packing_path.rs": "085b9a74b7931fe970b30263851026c2cbbf8495898638a0861d4c3d07d99a18", + "crates/oc-crypto/tests/review_regressions.rs": "a19dbf804026c3007fba261670f8218c7ad19520bd1490ba240181ae096a5cbb", + "crates/oc-crypto/tests/security_probe_keyschedule.rs": "1eb129e068b2138b2a0fd0465a62866f66e842a2bb4440ea0fd1c7b469e3d859", + "crates/oc-crypto/tests/security_probe_seal.rs": "2315397bfeaf3b2c81a17d934081ae72d966b84d5cf044c351f59e2b13aa196c", + "crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs": "7d619a7845d35b33426cbbc70441c1f4622742073c4a0bafdde27c2f50ac520e", + "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "44ee682d809c6211311e89c6193652adccb432b5e2cc2feabd5f94c1377c09ca", + "crates/oc-crypto/tests/spec_labels.rs": "c5a15f4792ffa62c1f49d7b08aa8ef95c13d28b0a795575155e98f6af65022e4", + "crates/oc-crypto/tests/zeroize_on_drop.rs": "6049affea1696a0235a3f47685d7126c9e022ca61ca3000d4abcffa6e1c4dc9e", + "crates/oc-engine/Cargo.toml": "00a95e126327629404f697eb2131735159eb09d2a6994849e63fa7faae17f336", + "crates/oc-engine/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "crates/oc-engine/README.md": "1eece36c00aecf16e31c12ce8c7cdbaa1f7b8299b435879e73f6cfec86bc42b2", + "crates/oc-engine/src/edit.rs": "ea729a45d50b190badd275549eef70f2f84a94be1bcf5acd0bc5e5a7192b5316", + "crates/oc-engine/src/lib.rs": "1456716cab8831eaf2737f2959fa33718779739e479aafc59753997a52a64bd6", + "crates/oc-engine/src/wire.rs": "a3cfdefb386786f97a9359b75383c9bc218b8d0b1982cf0dd2f1618c7b5d2922", + "crates/oc-engine/tests/edit_writer.rs": "8d8d77313772bf184677c79757322073ab29bcca203154050dd0a856dd055c1c", + "crates/oc-engine/tests/hardware_hybrid_golden.rs": "cec004a062ab838a2acfbe634f17c54cda1053a14337b57c51d2711e4b2033b7", + "crates/oc-engine/tests/p256_author_golden.rs": "6f20067edbc1259cd981e10347c4e1df4f80af6935456b01f42a7e246451f947", + "crates/oc-format/Cargo.toml": "d79856d1eeac0cb15f7064c2b9313e9b8871fd26daa9d449464e5c28dfcaa4eb", + "crates/oc-format/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "crates/oc-format/README.md": "54e78a93669649329fa9b68022c84bd69df7751fca007941b60f2ea0e1b16b2c", + "crates/oc-format/examples/verify-header.rs": "5c9ca98e85e1aade70875f9a935ae34618f94737e1b14336dba756d8fd62d59c", + "crates/oc-format/src/content.rs": "c0d32050f9d31ce913fa62bb9b82457612b46f20cf6fca2e98f5f1d6a7977b66", + "crates/oc-format/src/edit.rs": "ebb9edc8f5da165e72bef91cc6d269e1b4422abd79e076485429374291712348", + "crates/oc-format/src/encoding.rs": "f38e4708305c9007888b7bc155af891bd6b03157ae1f768c2204765b150f46f5", + "crates/oc-format/src/footer.rs": "6c1728344dde540085c9cec9955fbf8579147e50f5832c2df654eca8ea15b141", + "crates/oc-format/src/frame.rs": "dc2638063a297d61586181f507665f03e027fa1a8c3ce5616cda8cbe95418c87", + "crates/oc-format/src/header.rs": "f64e47093f3730edf22d36e7d734bd7827f23d5f2b3bf4a7013925816a0f7624", + "crates/oc-format/src/lib.rs": "2eb89b68917cbdf605f6d0d3252dedf82ae2eeb2f45cc8eda4d26c8d83cd28db", + "crates/oc-format/src/policy_codec.rs": "ad498c2c177ac76d64818e94c1e0eb708947400273c4d2f3f7f35158ced3a25b", + "crates/oc-format/src/text.rs": "2c84dc2ec2a0e9d57ee52f1478401753fc3798a5d6ff15ccaacaf53d0bc27d2a", + "crates/oc-format/src/tlv.rs": "8565cf55f7a91ea7f2c23d45e0f4144aff3a6f1bf515830a7ba1ea2acb4d50c5", + "crates/oc-format/src/verify.rs": "48fb0398d98131167192eeb0b0edbde55eaaeab9fb603030aeda9854a4da08a5", + "crates/oc-format/tests/edit.rs": "2e840455d87c97a1dc90145a048711a656211a4baef30b8e883ac6a96b642b2b", + "crates/oc-format/tests/kat_policy.rs": "bb04d93c27a5c86f7fa533dbf224b801378229875d5fcc147cffd1c049a6a5c4", + "crates/oc-format/tests/parser_robustness.rs": "5c53c6053bd3322fa11d6c184305907cc03f9b23ef463dac842b55a96b9f1ecf", + "crates/oc-format/tests/security_probe_hostile_input.rs": "3bb25839ccdbe063a866cb2828088e82591c9d52234209d0a774a5347e270bb6", + "crates/oc-format/tests/security_probe_policy_enforcement.rs": "7806a2a6a5aca6f287f6d58dc2b0c2be5e397e806587913bbd22264599322628", + "crates/oc-format/tests/skeptic_recheck_slot_tags.rs": "706524f0927ea9759b2c9e88b350bf2287a679dd7609f9bda428b8918a6f440b", + "crates/oc-policy/Cargo.toml": "2136eb1e010d9b5f7846d91e9f580036647e5659e7071cea2207448cd92d9a86", + "crates/oc-policy/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "crates/oc-policy/README.md": "b832b57b1da0d89897ced3a9e4ae0580d2dd9aef2d84e0f96bcae1fc07f1913d", + "crates/oc-policy/examples/access-policy.rs": "4a06f17c144d0f6c0eecb35cb166131c2f8755c333825567139852b7b23a9208", + "crates/oc-policy/src/lib.rs": "743eaf0550615071ef98ffab4b42c271981332fe19ad2290e20f1e21dea0ade4", + "crates/oc-policy/tests/security_probe_skeptic.rs": "c60f720854250afc356466c6a3d5227ee80e50a41d28d0e3c53a580082d6f769", + "crates/oc-policy/tests/server_policy_composition.rs": "c7069b4151bb0660e03050b249fe0f9dcafab44ed3bb3db70ad05977308bb87d", + "crates/oc-protocol/Cargo.toml": "a051746d07d110c0329846bb39b92b37be5ea7a207ae945fd0f1549882e50f07", + "crates/oc-protocol/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "crates/oc-protocol/README.md": "42ca1744fee5ac93b7bbdfcafcd016cf19c427ca208ae2491e3351d65a107c42", + "crates/oc-protocol/src/access.rs": "5a846d5285cd6beb48b5bb753a5ae836912580c8d91dc35f7a5f8e03324c37eb", + "crates/oc-protocol/src/action.rs": "1620404953a33fa6b3f636ad4eabf0a211f94c3c9be1c383a178514d85fb949e", + "crates/oc-protocol/src/activation.rs": "08b870f5ee24831ebbe02e9ada50e84cd66fe44dd58d98a4ce8113935fbb9da7", + "crates/oc-protocol/src/agent.rs": "539989796d3f1526a65ee91d1aa3bd852de4dce569369ffe1dbc3508d92b5856", + "crates/oc-protocol/src/attestation.rs": "02d1464d33f65dc97b7852dfde2866cfcae8dc13d9c073815c66beeef2530709", + "crates/oc-protocol/src/attribute_rule.rs": "d7de24657fd3226037304bb1940d6813528f52afb69544afdc3d15756da82309", + "crates/oc-protocol/src/control.rs": "b4fbeb59ac45c1ad3d4e6251691b8debc02aa7ca0bc838e95549e262d00b2db0", + "crates/oc-protocol/src/directory.rs": "980855f4c0d8eae8b0adca595c04150375583845be0b6392984c472b4a7dfc65", + "crates/oc-protocol/src/lease.rs": "def55db5224208f47aa79d517040bd54d127df7bf6128c310fe662ff064c8987", + "crates/oc-protocol/src/lib.rs": "d3021e2dda152298f31faed1769e32ab8139937a0edcfe1f0fad450a816e7c79", + "crates/oc-protocol/src/order.rs": "aaaad74887af649c627e4accca124a1d88e4f2f46c43ff69d8b351a01632a413", + "crates/oc-protocol/src/replica.rs": "40a14190e291ebff1feb95a005736de2d7529abd27ce09edca36c4e672d8d26a", + "crates/oc-protocol/src/revocation.rs": "95c9fca669f223fd145d6d4f4a918c11161def38961d4744107ee97424a7fa4d", + "crates/oc-protocol/src/standing.rs": "3c98c330382769c869fab6ac40c23d77cfc0c4dc362662c06bfbee68879ee1f1", + "crates/oc-protocol/src/unknown.rs": "5a8d7006089d636f315b752a1a67109337cfcb5b00a9824fab140af0064d18c8", + "crates/oc-protocol/src/witness.rs": "bf712ea8812e6f53a56e4e81b58a04f0734edd5da799859e9056f26c46393cff", + "crates/oc-protocol/tests/action_vectors.rs": "27901e8ea9f23ac2f4727d11c90b89e638534a6249474f6ca4a653b643b0080c", + "crates/oc-protocol/tests/agent_vectors.rs": "0764c3650b0b92f05706f9483c34a3b9a1c55abadbf8845b8b733b1c7c944f81", + "crates/oc-protocol/tests/control_vectors.rs": "86249ac910fcf4e9d42962f4d6916fcfcb545e016bc27420dada35b4bf8f1a2a", + "crates/oc-protocol/tests/kat_operation.rs": "5ce9366e992ff1a62cc681e7d98439dbf8d5dae5d14812351233a0657a71ff0c", + "crates/oc-protocol/tests/lease_attestation.rs": "009f06a398e64be2a5a85d09baba559a1c9e396a6bdafb64edc38a322980007e", + "crates/oc-protocol/tests/lease_server_policy.rs": "3dd4d56dcba4f847e6f1e629fa9bf925a1d6982efb62574bfb38df57a62da257", + "crates/oc-protocol/tests/lease_signature.rs": "f8f9de0fd92d546f34cdbf3dd036e664e682d4f40aa7f6f907e680a6d81ea37a", + "crates/oc-protocol/tests/session_mac_table.rs": "67d28231e53eb1bcde0e75b3703a62715aab24f9870aa0bcfe7eaad5a7103296", + "crates/oc-protocol/tests/unknown_tags.rs": "ddb5efc2c168b863c8b176bb327098bc657c9324ffb92700d98df1854c85d85f", + "crates/opencrate/Cargo.toml": "0cc071d2772fc611d9ced97e6331222bdb5d3f55e678275838df953444753bb7", + "crates/opencrate/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", + "crates/opencrate/README.md": "99049cb0966af57619ace7151dabd7f3ecfe97ddb292f28ca6f8dbb588fee8a2", + "crates/opencrate/examples/seal-file.rs": "802e2d887f54e60386e02c500d12667468b8acb5a89654d4cfc5aa7edaa22709", + "crates/opencrate/src/lib.rs": "8a6effdca9c9709505ea59bca87879d432be8ed70808ef4d67c18dcb05612555", + "deny.toml": "e06d8f3d9ac96158cc073b84076ca98a9efedb5e5e9e460c60715506cf5c6daa", "docs/architecture.md": "9b606db5d5b7a27bf25d90c2cef3f823a2c07989d25aeb0282bd5d6ef2f70e43", "docs/assets/access-decisions.svg": "b106699adb59aeb9466d94b48ab505045faf7e91f9bd5739074911e244a4c030", "docs/assets/core-map.svg": "716048cb3069ba81a1b23da228ee9a82385199b423337f8dd17bc77e533ecce2", @@ -248,11 +254,12 @@ "docs/assets/open-crate-mark.svg": "f3039e6557a4a5cac7aff44ec4457119efc1fa543caf17320fafc15489ed0dc0", "docs/assets/quick-start.gif": "4f0232560ff9bd8e1e7a1c34c4c04b5aa94deeb134e22ea722307e0b3aa5bee1", "docs/format.md": "b4e98a8bd6ccbe09e81f10637068a9d878f996387e6d36096d8dde21534ca1c5", - "docs/getting-started.md": "1699b2a39f3299aa4e66beb9b70824eac50a55b69d37291c221607537f0b99f4", + "docs/getting-started.md": "9a5236295d05cd8c8b3e415babd6182c0bb5cfb4250dbd8105342b8868c12051", "docs/how-it-works.md": "5a15ef7455a46a9b550a85359fcf0b8e8dd85bcbc45e2ded612676b1cc8826e7", "docs/index.md": "328e5e1d6ea45daae7ef1651459a83c759f4b698b5d81a571f5eb1f9a3c985fa", - "docs/licensing.md": "6711f5603b9198fdaacf6c769dddb26c6d6f324d319b66a971859ba06dcf90ef", + "docs/licensing.md": "ef99fc145a6d57dbd823c664c460affaa181340ea0e230527a2eb786c49c5d2a", "docs/protocol.md": "ec582526e61914ee6251ba2188d6680d3aa88aacaf2056d49ee6a463ce1e6990", + "docs/review-2026-09-26.md": "1515c513e2e070ac39f97d1861b4b38322f131d0db0d08b72fd3d7b955471910", "docs/translation-notes.md": "c290d69d83f42cd1f9d9311e32b8ad2b08391a94856e68a0b4993dfa1d103065", "examples/README.md": "7a95174a360ec6527fcb86cef5a78c011e4281010bde5ddfac8fc280f7194738", "rust-toolchain.toml": "41308dbe82a83679b7b2081be4d5ae7f9aa52f642bcc3eab104ac77501560e79", @@ -304,5 +311,8 @@ "version": "0.0.1", "facade": "opencrate", "registry_upload": false - } + }, + "reviewed_public_baseline": "7e607dfcc6e3cc948e9ee7de9c1da56a76d8242e", + "release_candidate": "0.0.3", + "license": "MPL-2.0" } diff --git a/LICENSE b/LICENSE index c274f9d..a612ad9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,141 +1,373 @@ -Open Crate Community License 1.0 -Copyright (c) 2026 Alexey Andreev - -1. Scope and acceptance - -This license applies to the Open Crate code and accompanying material explicitly -marked with this license (the Software). The licensor is Alexey Andreev. Use, -copying, modification or distribution of the Software requires compliance with -this license or a separate written commercial agreement with the licensor. -Third-party components retain their own licenses. This license grants no rights -to the separate Close Crate application, server, hosting or support services. - -2. Free permitted use - -Subject to the conditions below, the licensor grants a worldwide, non-exclusive, -royalty-free copyright license to use, reproduce, inspect, modify, create -derivative works of and distribute the Software for: - -(a) personal, non-commercial use by an individual, including learning, hobby - projects and independent non-commercial security research; -(b) non-commercial teaching and academic research by educational institutions; -(c) commercial or internal business use by a Qualifying Small Business; and -(d) evaluation, testing and security auditing that does not provide a production - service or perform ordinary production business operations. - -There is no registration, activation, license-server connection or reporting -requirement for free permitted use. The Software contains no license-enforcement -telemetry required by this license. Making a permitted modification does not -require publishing your own application's source code. - -3. Qualifying Small Business - -A business qualifies only while BOTH of the following are true: - -(a) its Group Revenue is less than USD 1,000,000 for the immediately preceding - twelve months; and -(b) its Group Headcount is fewer than 25 people. - -A business includes a self-employed person or sole proprietor when acting for -commercial purposes. Group means the business and all entities controlling, -controlled by, or under common control with it. Control means holding more than -50 percent of voting rights or having the power to direct management. A small -team, department, project or subsidiary inside a larger group is not measured -separately. - -Group Revenue is consolidated gross revenue from all business activities before -expenses and taxes, excluding sales/VAT taxes collected for governments and -intra-group transactions. Equity investment, loan proceeds and charitable -donations are not revenue for this test. For a business younger than twelve -months, use actual revenue since formation without annualizing it. Convert -non-USD revenue using a consistently applied, publicly documented central-bank -average exchange rate for the measured period. - -Group Headcount is the number of employees plus individual contractors working -for the group, counted once per person. Count people, not software users or -developer seats. A contractor using the Software solely on behalf of a customer -must assess that customer's group as well as the contractor's own business; -both must qualify unless the customer's commercial agreement covers that use. - -4. Crossing a threshold - -A previously qualifying business receives one transition period of 90 calendar -days from the first date either threshold is reached. Its existing and new use -may continue under this license during that period. At its end, continuing -production business use requires a written commercial license. A business that -already exceeds either threshold when it first uses the Software has no -production transition period, but may evaluate under section 2(d). - -There are no retroactive fees for use that was permitted at the time. The -transition period cannot be restarted by reorganizing a group or temporarily -reducing revenue or headcount. A business that subsequently qualifies again may -resume permitted use, but does not receive a second transition period. - -5. Commercial licensing - -Any production business use outside section 2 or section 4 requires a separate -commercial agreement BEFORE that use. The fee, scope, term, payment method, -support and any warranty are agreed separately in writing. This license does -not impose an automatic charge or grant paid rights merely because a payment -was offered. Contact the maintainer through the licensing issue category of the -official OpenCrate GitHub repository or the contact route at closecrate.com. - -6. Redistribution and modifications - -Keep this license and applicable copyright and attribution notices with copies -of the Software, including copies embedded in binaries. Clearly identify -modifications. The Software and modifications to it remain subject to these -terms; you may not offer recipients broader rights in the Software than you -hold. Each recipient obtains its rights from the licensor under these terms -and must independently qualify or hold a commercial license. Your separate -application code may use its own license, provided it does not override these -conditions for the Software. Reading a document created by the Software does -not by itself require a license to the Software. - -7. Patent grant - -For permitted use, the licensor grants a worldwide, non-exclusive, royalty-free -patent license to make, have made, use, offer to sell, sell and import the -Software, limited to patent claims controlled by the licensor that are -necessarily infringed by the Software as supplied. No rights in third-party -patents are promised. This patent grant terminates if you initiate patent -litigation alleging that the Software itself infringes a patent. - -8. Termination and cure - -Rights terminate if you materially breach this license. For a first breach, -rights are reinstated if you cure the breach within 30 days after learning of -it. Continuing business use after a transition period without a commercial -license is a breach. Termination does not affect rights independently held by -compliant recipients or rights you hold under a different valid license. - -9. Separate rights and future changes - -This license does not claim exclusive ownership of third-party contributions -or limit rights independently obtained under another valid license. Changes to -these terms apply only to versions distributed with changed terms, not -retroactively to this copy. - -10. Trademarks - -No trademark rights are granted, except reasonable descriptive use to identify -the Software and retain required notices. Do not imply endorsement of a fork, -modification, service or security claim by the licensor. - -11. Warranty and liability - -TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED AS IS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. -THE LICENSOR IS NOT LIABLE FOR CLAIMS, DAMAGES OR OTHER LIABILITY ARISING FROM -THE SOFTWARE OR ITS USE, WHETHER IN CONTRACT, TORT OR OTHERWISE. NOTHING HERE -EXCLUDES LIABILITY THAT APPLICABLE LAW DOES NOT ALLOW TO BE EXCLUDED. - -12. Interpretation - -This is a source-available license with commercial-use eligibility conditions; -it is not an OSI-approved open-source license. No automatic conversion to a -different license is promised. If a provision is unenforceable, the remaining -provisions continue to apply to the extent allowed by law. Mandatory rights -under applicable law are not limited. Only a written agreement with the -licensor can grant exceptions to these terms. +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/LICENSE-OPENCRATE b/LICENSE-OPENCRATE deleted file mode 100644 index c274f9d..0000000 --- a/LICENSE-OPENCRATE +++ /dev/null @@ -1,141 +0,0 @@ -Open Crate Community License 1.0 -Copyright (c) 2026 Alexey Andreev - -1. Scope and acceptance - -This license applies to the Open Crate code and accompanying material explicitly -marked with this license (the Software). The licensor is Alexey Andreev. Use, -copying, modification or distribution of the Software requires compliance with -this license or a separate written commercial agreement with the licensor. -Third-party components retain their own licenses. This license grants no rights -to the separate Close Crate application, server, hosting or support services. - -2. Free permitted use - -Subject to the conditions below, the licensor grants a worldwide, non-exclusive, -royalty-free copyright license to use, reproduce, inspect, modify, create -derivative works of and distribute the Software for: - -(a) personal, non-commercial use by an individual, including learning, hobby - projects and independent non-commercial security research; -(b) non-commercial teaching and academic research by educational institutions; -(c) commercial or internal business use by a Qualifying Small Business; and -(d) evaluation, testing and security auditing that does not provide a production - service or perform ordinary production business operations. - -There is no registration, activation, license-server connection or reporting -requirement for free permitted use. The Software contains no license-enforcement -telemetry required by this license. Making a permitted modification does not -require publishing your own application's source code. - -3. Qualifying Small Business - -A business qualifies only while BOTH of the following are true: - -(a) its Group Revenue is less than USD 1,000,000 for the immediately preceding - twelve months; and -(b) its Group Headcount is fewer than 25 people. - -A business includes a self-employed person or sole proprietor when acting for -commercial purposes. Group means the business and all entities controlling, -controlled by, or under common control with it. Control means holding more than -50 percent of voting rights or having the power to direct management. A small -team, department, project or subsidiary inside a larger group is not measured -separately. - -Group Revenue is consolidated gross revenue from all business activities before -expenses and taxes, excluding sales/VAT taxes collected for governments and -intra-group transactions. Equity investment, loan proceeds and charitable -donations are not revenue for this test. For a business younger than twelve -months, use actual revenue since formation without annualizing it. Convert -non-USD revenue using a consistently applied, publicly documented central-bank -average exchange rate for the measured period. - -Group Headcount is the number of employees plus individual contractors working -for the group, counted once per person. Count people, not software users or -developer seats. A contractor using the Software solely on behalf of a customer -must assess that customer's group as well as the contractor's own business; -both must qualify unless the customer's commercial agreement covers that use. - -4. Crossing a threshold - -A previously qualifying business receives one transition period of 90 calendar -days from the first date either threshold is reached. Its existing and new use -may continue under this license during that period. At its end, continuing -production business use requires a written commercial license. A business that -already exceeds either threshold when it first uses the Software has no -production transition period, but may evaluate under section 2(d). - -There are no retroactive fees for use that was permitted at the time. The -transition period cannot be restarted by reorganizing a group or temporarily -reducing revenue or headcount. A business that subsequently qualifies again may -resume permitted use, but does not receive a second transition period. - -5. Commercial licensing - -Any production business use outside section 2 or section 4 requires a separate -commercial agreement BEFORE that use. The fee, scope, term, payment method, -support and any warranty are agreed separately in writing. This license does -not impose an automatic charge or grant paid rights merely because a payment -was offered. Contact the maintainer through the licensing issue category of the -official OpenCrate GitHub repository or the contact route at closecrate.com. - -6. Redistribution and modifications - -Keep this license and applicable copyright and attribution notices with copies -of the Software, including copies embedded in binaries. Clearly identify -modifications. The Software and modifications to it remain subject to these -terms; you may not offer recipients broader rights in the Software than you -hold. Each recipient obtains its rights from the licensor under these terms -and must independently qualify or hold a commercial license. Your separate -application code may use its own license, provided it does not override these -conditions for the Software. Reading a document created by the Software does -not by itself require a license to the Software. - -7. Patent grant - -For permitted use, the licensor grants a worldwide, non-exclusive, royalty-free -patent license to make, have made, use, offer to sell, sell and import the -Software, limited to patent claims controlled by the licensor that are -necessarily infringed by the Software as supplied. No rights in third-party -patents are promised. This patent grant terminates if you initiate patent -litigation alleging that the Software itself infringes a patent. - -8. Termination and cure - -Rights terminate if you materially breach this license. For a first breach, -rights are reinstated if you cure the breach within 30 days after learning of -it. Continuing business use after a transition period without a commercial -license is a breach. Termination does not affect rights independently held by -compliant recipients or rights you hold under a different valid license. - -9. Separate rights and future changes - -This license does not claim exclusive ownership of third-party contributions -or limit rights independently obtained under another valid license. Changes to -these terms apply only to versions distributed with changed terms, not -retroactively to this copy. - -10. Trademarks - -No trademark rights are granted, except reasonable descriptive use to identify -the Software and retain required notices. Do not imply endorsement of a fork, -modification, service or security claim by the licensor. - -11. Warranty and liability - -TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED AS IS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. -THE LICENSOR IS NOT LIABLE FOR CLAIMS, DAMAGES OR OTHER LIABILITY ARISING FROM -THE SOFTWARE OR ITS USE, WHETHER IN CONTRACT, TORT OR OTHERWISE. NOTHING HERE -EXCLUDES LIABILITY THAT APPLICABLE LAW DOES NOT ALLOW TO BE EXCLUDED. - -12. Interpretation - -This is a source-available license with commercial-use eligibility conditions; -it is not an OSI-approved open-source license. No automatic conversion to a -different license is promised. If a provision is unenforceable, the remaining -provisions continue to apply to the extent allowed by law. Mandatory rights -under applicable law are not limited. Only a written agreement with the -licensor can grant exceptions to these terms. diff --git a/README.md b/README.md index 870ce2c..42d44ba 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Core CI on main - Open Crate Community License 1.0 + MPL-2.0 license Total opencrate crate downloads

@@ -24,11 +24,11 @@ decisions. It is the five-library core behind Close Crate. For a Rust project, add the `opencrate` facade to use all five libraries, or depend on the individual `oc-*` crates you need. The facade is version -`0.0.2`; the five core libraries are also `0.0.2` on crates.io: +`0.0.3`; the five core libraries are also `0.0.3` on crates.io: ```toml [dependencies] -opencrate = "=0.0.2" +opencrate = "=0.0.3" ``` Your application supplies storage, transport, keys, secure randomness and time. @@ -63,7 +63,7 @@ extension, or JSON and messages, for one recipient. It re-exports the separate ```toml [dependencies] -opencrate = { version = "=0.0.2", features = ["app-data"] } +opencrate = { version = "=0.0.3", features = ["app-data"] } ``` ```sh @@ -126,20 +126,16 @@ See [the documentation map](docs/index.md) for examples and library references. ## License -The [Open Crate Community License 1.0](LICENSE) is free for personal -non-commercial use and businesses with both less than **$1M annual group revenue** -and **fewer than 25 people**. Larger businesses need a commercial agreement: -write to god@closecrate.com or see [Licensing](docs/licensing.md). -There is a 90-day transition for existing qualifying businesses. - -This is source-available software, not OSI-approved open source. See -[licensing](docs/licensing.md) for scope and third-party terms. +Open Crate is licensed under [MPL-2.0](LICENSE). Commercial use is allowed; +distributed changes to covered files stay under MPL-2.0. Separate application +files can use other terms. See +[licensing](docs/licensing.md) for release history and third-party terms. ## Support the project Open Crate is built by one independent developer. If the core is useful to you, a tip helps keep it going. Tips are voluntary: they are not a license fee, do -not buy a commercial license and do not create any support obligation. +not create any support obligation. | Asset | Network | Address | | --- | --- | --- | @@ -147,8 +143,7 @@ not buy a commercial license and do not create any support obligation. Send only USDT on the TRON (TRC20) network to this address. Other tokens or other networks (Ethereum, BNB Chain and so on) will be lost. No memo or tag is -needed. Commercial licensing goes through god@closecrate.com, not through this -address. +needed. ## Feedback diff --git a/crates/oc-crypto/Cargo.toml b/crates/oc-crypto/Cargo.toml index 2457f56..65759d0 100644 --- a/crates/oc-crypto/Cargo.toml +++ b/crates/oc-crypto/Cargo.toml @@ -7,8 +7,7 @@ readme = "README.md" version.workspace = true edition.workspace = true rust-version.workspace = true -# Source-available core; see the complete grant and earlier-version rights. -license-file = "../../LICENSE-OPENCRATE" +license = "MPL-2.0" publish.workspace = true # `default-features = false` везде, где дефолт тянет getrandom. diff --git a/crates/oc-crypto/LICENSE b/crates/oc-crypto/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/crates/oc-crypto/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crates/oc-crypto/README.md b/crates/oc-crypto/README.md index 6ac4a9e..7755a18 100644 --- a/crates/oc-crypto/README.md +++ b/crates/oc-crypto/README.md @@ -2,11 +2,11 @@ Cryptographic operations used by the Open Crate format and protocol. -[Documentation](../../docs/index.md) · [Key scheme](../../docs/format.md) · [Source](src/lib.rs) +[Documentation](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/index.md) · [Key scheme](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/format.md) · [Source](https://github.com/AlexiAxAxA/OpenCrate/blob/main/crates/oc-crypto/src/lib.rs) The crate supplies authenticated encryption, key derivation, signatures, sealing, integrity trees and hybrid key mechanisms. Frozen known-answer vectors in -[`tests/kat`](../../tests/kat/) support reproducibility of these operations. +[`tests/kat`](https://github.com/AlexiAxAxA/OpenCrate/blob/main/tests/kat/) support reproducibility of these operations. Its low-level operations accept byte slices; they do not check filenames or extensions. For a small standalone sealed-byte envelope with OS randomness, @@ -20,7 +20,11 @@ bytes and different security properties. Secure randomness is supplied by the ho Hybrid protection is a property of the chosen recipient path. It does not turn classical signatures or every alternative slot into post-quantum protection. -See the [security boundary](../../SECURITY.md). +See the [security boundary](https://github.com/AlexiAxAxA/OpenCrate/blob/main/SECURITY.md). From the repository root, build the reference with `cargo doc -p oc-crypto --no-deps`. Test-only explicit-nonce, signer and label facilities are not production defaults. + +## License + +Licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). diff --git a/crates/oc-crypto/src/aead.rs b/crates/oc-crypto/src/aead.rs index 598b1f6..2f87d04 100644 --- a/crates/oc-crypto/src/aead.rs +++ b/crates/oc-crypto/src/aead.rs @@ -1,28 +1,13 @@ -//! Payload chunk encryption. +// SPDX-License-Identifier: MPL-2.0 +//! Payload chunk encryption: `nonce(24) ‖ ciphertext ‖ tag(16)`. //! -//! On-disk frame: `nonce(24) ‖ ciphertext ‖ tag(16)`. +//! The sender hedges each nonce over a random seed, plaintext, and AAD. The reader +//! uses the stored nonce. Deriving it only from file ID and chunk index would reuse +//! the keystream when a chunk is edited under the same key. //! -//! The nonce is **stored, never derived by the reader**. Deriving it from `(file_id, chunk -//! index)` is fatal: `file_id` must remain constant during editing, so -//! rewriting a chunk under the same key would reuse a nonce with different -//! plaintexts, recovering both plaintexts through XOR and recovering -//! the one-time Poly1305 key. -//! -//! 192 bits suffice against random collisions, but NOT against repeated -//! RNG state, so the SENDER derives the nonce through hedging (§6.1 -//! and §3.1). This used to say that the width exists "precisely to make -//! random values safe", a statement that contradicted §3.1 and -//! survived decision C-13. -//! -//! This determines the crate's API shape, which is the main point here: production -//! sealing paths accept a SEED rather than a nonce: [`seal_chunk_hedged`] and -//! [`seal_metadata_hedged`]. There is no way to pass a ready-made nonce; this is a property -//! of the signature, not caller discipline. Forms accepting a nonce -//! (`seal_chunk`, `seal_metadata`) remain only behind the -//! `explicit-nonce` feature, absent from production builds. -//! -//! An inviolable rule: unauthenticated bytes never leave the function. -//! On error, the output buffer is wiped. +//! Production entry points accept seeds, not caller-chosen nonces. Explicit-nonce +//! forms are available only through the `explicit-nonce` feature for testing. +//! Unauthenticated plaintext is never returned; failed output buffers are wiped. use crate::merkle::{leaf_of, Leaf}; use crate::secret::{MetaKey, PayloadKey, SecretBuf}; diff --git a/crates/oc-crypto/src/agreement.rs b/crates/oc-crypto/src/agreement.rs index 69d0526..16786e4 100644 --- a/crates/oc-crypto/src/agreement.rs +++ b/crates/oc-crypto/src/agreement.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Key agreement behind a trait: X25519 in software, P-256 in software or in a TPM. //! //! This module exists for one property: the private key may never leave diff --git a/crates/oc-crypto/src/kdf.rs b/crates/oc-crypto/src/kdf.rs index 6fc3f93..7df4690 100644 --- a/crates/oc-crypto/src/kdf.rs +++ b/crates/oc-crypto/src/kdf.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Key schedule: derivations K1…K9 from `docs/format.md`, section 3.5. //! //! Each function corresponds to exactly one table row. No label is @@ -47,39 +48,17 @@ fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[&[u8]]) -> Zeroi okm } -/// A nonce not wholly dependent on RNG state (hedged nonce). +/// Hedge a nonce against repeated RNG state (`docs/format.md` §3.1, §6.1). /// -/// **Why.** A random nonce is safe only while the RNG does not -/// repeat, and nothing guarantees that: virtual-machine snapshot rollback, -/// disk-image cloning, and backup restoration return -/// the RNG to a previous state. A separate random value does not -/// help: it comes from the same RNG and repeats with it, -/// repeating the entire keystream. For slot sealing the cost is -/// maximal: plaintexts are shares of a 2-of-2 secret-sharing scheme, and repetition gives -/// `ct₁ ⊕ ct₂ = pt₁ ⊕ pt₂`, hence both shares, the KEK, and the content key without a single -/// private key. +/// Since version 3, `HKDF-Extract(salt = RNG seed, +/// ikm = u32be(len(pt)) ‖ pt ‖ u32be(len(aad)) ‖ aad)`, followed by +/// `Expand(info = label)`. Lengths separate variable-width inputs; AAD is exactly +/// what AEAD authenticates. Unrepresentable lengths return `BadLength`. /// -/// **How.** Since version 3, `HKDF-Extract(salt = RNG seed, -/// ikm = u32be(len(pt)) ‖ pt ‖ u32be(len(aad)) ‖ aad)`, then `Expand(info = label)`. -/// AAD is exactly what AEAD receives: with a repeated RNG, identical plaintext -/// in a different context gets a different nonce. This is hedging, not SIV: -/// the key is not part of the seed; fully repeated inputs still repeat the nonce. -/// Both lengths are mandatory because the parts have variable widths (the K1 argument). -/// Lengths unrepresentable as u32 and excessive output lengths return BadLength. -/// -/// **This does not contradict "nonces are stored, not derived".** The rule -/// exists to prevent a nonce being a function of values available to the reader: -/// derived from the shared secret, it would match whenever that secret matched. -/// Here, conversely, the nonce derives from what the reader lacks (seed, -/// plaintext) and **is stored in the file**; the reader still takes it -/// ready-made and never computes it. -/// -/// The seed is never stored anywhere: it is needed only during computation. -/// -/// The label has type [`label::Label`]: nonce hedging is also a domain, and -/// its four labels (K17–K20) are registered alongside the others. Passing a -/// fifth label invented on the spot would derive a nonce in a domain -/// unknown to §3.6. +/// Changed plaintext or AAD changes the nonce even if the random seed repeats. +/// This is not SIV: the key is not an input, and fully repeated inputs still +/// repeat the nonce. The sender stores the nonce; the reader never derives it. +/// The seed is transient, and [`label::Label`] restricts the derivation domain. pub fn hedged_nonce( label: label::Label, seed: &[u8], @@ -117,53 +96,23 @@ fn hmac_sha256(key: &[u8], message: &[&[u8]]) -> [u8; 32] { tag } -/// K23: proof of opening the challenge without revealing the secret itself. -/// The challenge remains secret between two parties: the public echo cannot derive K24. -/// All 64 bytes of the hardware challenge enter the HMAC key; output is always 32 bytes. +/// Legacy K23 challenge echo, retained only for its frozen vector. /// -/// **NOT USED by the protocol since 2026-09-21.** [`echo_transcript`] computes the echo -/// (K31): the old form bound nothing but the -/// secret to the conversation, so a recorded echo worked in any conversation repeating that secret. -/// For the decision and its limits, see `docs/format.md`, section "ECHO BOUND TO THE CONVERSATION -/// 2026-09-21". -/// -/// Retained for the frozen `k23_prove_echo` vector -/// (`tests/kat/derivations_wire.kat`): I-14 forbids changing frozen artifacts, and -/// removing the derivation would leave the vector without the operation it checks. -/// It must acquire no new callers; the guard -/// `the_old_unbound_echo_has_no_callers_outside_its_vector` enforces that. +/// All challenge bytes enter the HMAC key; output is 32 bytes. This form does not +/// bind the conversation. The protocol uses [`echo_transcript`] (K31) instead, +/// and `the_old_unbound_echo_has_no_callers_outside_its_vector` guards that boundary. pub fn prove_echo(challenge: &[u8], device_fpr: &[u8; 32]) -> [u8; 32] { hmac_sha256(challenge, &[label::PROVE_ECHO.as_bytes(), device_fpr]) } -/// K31, step 1: handshake transcript hash (`docs/protocol.md` §9.4). +/// K31 handshake transcript hash (`docs/protocol.md` §9.4). /// /// `SHA-256("CC/v1/echo-transcript" ‖ 0x00 ‖ u32le(len(hello)) ‖ hello ‖ /// u32le(len(challenge)) ‖ challenge)`. /// -/// # What is included and why -/// -/// Exactly two frames, both available in full to BOTH parties by echo time: -/// the device hello (`kind ‖ TLV`: presented keys, `device_fpr`, -/// mechanism number) and server challenge (`kind ‖ TLV`: sealed halves with -/// their `enc`, nonces, and ciphertexts). Neither connection numbers nor clock -/// readings are included deliberately: only the server knows the number, the parties have different clocks, -/// and a value known to only one party cannot be a transcript. -/// -/// # Why RAW bytes rather than reconstructed values -/// -/// The same argument as I-5: reconstructing parsed data would yield one byte sequence for -/// a canonical document and another for one whose canonicality is checked -/// by nothing except our own encoder. An intermediary modifying a frame in transit -/// must diverge from the honest party, which happens only when the hash -/// covers what actually traveled over the wire. -/// -/// # Why lengths are included -/// -/// [`Transcript::field`](crate::transcript::Transcript::field) prefixes -/// each piece with its length: otherwise ("ab", "c") and ("a", "bc") would give -/// one hash, allowing an attacker to move bytes between hello and challenge -/// without changing the echo. +/// Both peers have these complete raw frames. Length prefixes prevent moving +/// bytes between them without changing the hash. Re-encoding parsed values would +/// lose changes to their original representation. #[must_use] pub fn handshake_transcript(hello_framed: &[u8], challenge_framed: &[u8]) -> [u8; 32] { use sha2::Digest as _; @@ -172,27 +121,14 @@ pub fn handshake_transcript(hello_framed: &[u8], challenge_framed: &[u8]) -> [u8 Sha256::digest(t.as_bytes()).into() } -/// K31, step 2: proof-of-possession echo bound to the conversation. +/// K31 proof-of-possession echo bound to the conversation. /// /// `HMAC-SHA256(key = challenge secret, "CC/v1/echo-transcript" ‖ device_fpr ‖ -/// handshake)`, where `handshake` is the output of [`handshake_transcript`]. -/// -/// # What this fixes +/// handshake)`, where `handshake` comes from [`handshake_transcript`]. /// -/// K23 included only the fingerprint in the message. An echo recorded from the wire worked in -/// ANY conversation with the same device whenever the secret repeated, and the server -/// secret repeats after snapshot rollback (I-1, argument C-13; K30 addressed repetition, -/// but not the construction itself). The safety margin depended on the server currently issuing -/// nothing on `Proven` without a session MAC, making it just one edit thick. -/// The echo is now a function of the conversation: the server seals anew -/// in each conversation, with its own ephemeral `Seal` pair, so challenge bytes differ. -/// -/// # What this does NOT provide -/// -/// It does not fix full snapshot rollback TOGETHER with the clock: then both -/// the secret (time is in K30's preimage) and the ephemeral sealing pair repeat, -/// repeating the entire transcript. It does not address an attacker possessing -/// the device private key: that attacker legitimately proves possession. +/// Including the transcript prevents an echo from moving to a different handshake +/// when the challenge secret repeats. It cannot distinguish a fully repeated +/// transcript after snapshot and clock rollback, or protect against a stolen key. #[must_use] pub fn echo_transcript( challenge: &[u8], @@ -209,37 +145,16 @@ pub fn derive_session_mac_key(challenge: &[u8], device_fpr: &[u8; 32]) -> Sessio )) } -/// K27: device fingerprint for ANY agreement mechanism. -/// -/// The fingerprint is the single 32-byte value a person checks -/// through a second channel and to which the author later seals a share. For X25519 these two -/// roles coincide in the key itself: it is 32 bytes, and its fingerprint equals the key. -/// This is exactly what the bequest, quorum, and approval doors relied on, and precisely -/// why they were closed to anything longer: for P-256 (65 bytes) and -/// hybrids (1216, 1249), the fingerprint was UNDEFINED, and an intermediary presenting -/// someone else's fingerprint with its own key could receive the share under its key -/// in someone else's name (review 2026-09-06, N-1 and N-4). -/// -/// This definition closes the gap for every mechanism at once: the fingerprint becomes -/// a COMMITMENT to the key. Whoever receives `(fpr, kem, public)` must -/// check `fpr == device_fpr(kem, public)` BEFORE using any of -/// the three; for X25519 this is the same comparison as before. -/// -/// **The form is deliberately asymmetric.** For `kem_id = 1`, it returns the key itself, -/// not its hash: K11, K21, K23, K24 vectors and the `Hello` layout freeze that behavior, -/// and unifying the fingerprints would require reissuing them without a single -/// new security fact. For the other mechanisms: -/// `SHA-256("CC/v1/device-fpr" ‖ 0x00 ‖ u8(kem_id) ‖ public)`. The mechanism number -/// is in the preimage: the same key bytes under two numbers produce different -/// fingerprints, so relabeling a mechanism changes the device name. +/// K27 device fingerprint bound to its agreement mechanism and public key. /// -/// Key length is checked against the mechanism, not taken "as received" (I-8): a fingerprint -/// of an incorrectly sized key would do no harm, but one rule applies everywhere, -/// and making an exception here would cost more than the check. +/// X25519 returns the public key itself for compatibility with frozen vectors. +/// Other supported mechanisms use +/// `SHA-256("CC/v1/device-fpr" ‖ 0x00 ‖ u8(kem_id) ‖ public)`. +/// Callers must compare this result with the presented fingerprint before using +/// the key; the mechanism byte prevents relabeling the same bytes. /// /// # Errors -/// `public` length does not match the mechanism, or the mechanism defines no length -/// (RSA-OAEP: variable-length keys, with no defined fingerprint). +/// `public` has the wrong length, or the mechanism has no defined fingerprint. pub fn device_fpr(kem: crate::KemAlg, public: &[u8]) -> Result<[u8; 32], CryptoError> { use crate::KemAlg; // Исполнимость спрашивается ОДНИМ именем, а не вторым `match` рядом с @@ -297,28 +212,14 @@ pub fn request_mac(key: &SessionMacKey, framed: &[u8]) -> [u8; 32] { hmac_sha256(key.expose(), &[framed]) } -/// K28: wire operation identity (`docs/protocol.md` §9.10). +/// K28 wire operation identity (`docs/protocol.md` §9.10). /// -/// `SHA-256("CC/v1/operation-id" ‖ 0x00 ‖ seed(32) ‖ u8(kind) ‖ body)`, where body is -/// the encoded request WITHOUT the identity field itself (for an activation request, without -/// tag 9: a value cannot include itself). +/// `SHA-256("CC/v1/operation-id" ‖ 0x00 ‖ seed(32) ‖ u8(kind) ‖ body)`. +/// `body` is the encoded request without the identity field itself. /// -/// # Why not bare random bytes -/// -/// By the C-13 argument (I-1): RNGs repeat after VM snapshot rollback, image -/// cloning, and backup restoration. A bare random identifier could match for -/// two DIFFERENT requests, giving the second request the stored outcome of the first: -/// "issued" for someone else's request or "already used" rejection for its own. Including the body in the seed -/// separates different requests even when the RNG repeats; only identical -/// requests remain identical, and sharing an outcome is correct for them. -/// -/// No secret is present or needed: the identifier travels openly on the wire, -/// and requires uniqueness rather than unpredictability. An intermediary observing -/// the wire already learns it; there is nothing to forge: replay with a different body -/// is rejected by the server's body hash, and activation bodies carry a session MAC. -/// -/// The kind enters the seed because activation and renewal have the same body: -/// without it, one seed would give two different operations one identity. +/// Binding kind and body separates different operations when the RNG repeats; +/// identical requests retain the same identity. This public value provides +/// deduplication, not authentication. #[must_use] pub fn operation_id(seed: &[u8; 32], kind: u8, body: &[u8]) -> [u8; 32] { use sha2::Digest as _; @@ -350,51 +251,17 @@ pub const FRESH_CREDENTIAL_SEED: u8 = 4; /// with an RSA EK. pub const FRESH_CREDENTIAL_OAEP: u8 = 5; -/// K30: a fresh value generated by the SERVER (`docs/format.md`, section -/// "SERVER FRESHNESS IS DERIVED 2026-09-20"). +/// K30 server freshness (`docs/format.md`, "SERVER FRESHNESS IS DERIVED"). /// /// `prk = SHA-256("CC/v1/server-fresh" ‖ 0x00 ‖ seed(32) ‖ u8(kind) ‖ -/// i64be(now) ‖ device_fpr(32))`, then `HKDF-Expand(prk, info = label)` into a buffer -/// of the required length. -/// -/// # Why not bare random bytes -/// -/// By the C-13 argument (I-1): RNGs repeat after VM snapshot rollback, image -/// cloning, and backup restoration. This is worse on the server than on the client: its state -/// rolls back together with the RNG, so an "already issued" value -/// becomes unissued again, and nobody notices the repetition. +/// i64be(now) ‖ device_fpr(32))`, then `HKDF-Expand(prk, info = label)`. /// -/// # Why TIME separates values rather than request data -/// -/// NONE of the consumers has plaintext capable of distinguishing repeats: -/// an attestation challenge is pure freshness, as is a proof secret, and so are the three -/// TPM credential values (`kind` 3–5, added 2026-09-21). -/// A seed based only on -/// request data (the device fingerprint) would separate DIFFERENT devices but still -/// repeat for the same device, which is precisely the danger: two conversations with one -/// device would receive one secret, hence one echo and one session MAC key. -/// The server has a clock, and `now` already enters the handler as a parameter. -/// -/// The fingerprint is a second separator rather than a replacement for time: within one -/// second, two devices must receive different values. -/// -/// # What this does NOT provide -/// -/// Snapshot rollback TOGETHER with the clock cannot be distinguished: `now` returns to its old value, -/// and the value repeats. This protects against RNG repetition, not an adversary -/// controlling the server clock. -/// -/// # Why Expand rather than a second hash with a counter -/// -/// The proof-of-possession secret consists of several 32-byte halves, one per -/// presented key, and its length is known only at runtime. Appending -/// counter-based hashes would introduce a homemade KDF beside an existing one: -/// `HKDF-Expand` uses the same counter, but standardized and already verified. -/// Thus `N = 32` is no exception: even the attestation challenge gets its 32 bytes -/// through Expand, giving one form for both applications. +/// Time separates calls for one device when random state repeats; the fingerprint +/// separates devices within one second. Fully repeated inputs, including clock +/// rollback, still produce the same output. The caller supplies the clock and seed. /// /// # Errors -/// [`CryptoError::BadLength`]: more than 255×32 bytes requested. +/// [`CryptoError::BadLength`] if more than 255×32 bytes are requested. pub fn server_fresh( seed: &[u8; 32], kind: u8, @@ -513,25 +380,11 @@ pub fn derive_content_mac_key(cek: &Cek, header_salt: &[u8; 32], file_id: &[u8; )) } -/// K12: access-state witness key. -/// -/// Authenticates the lease-cache head stored in two places: inside and outside the profile. The label -/// `"CC/v1/cached-lease"` was reserved in the §3.6 registry in advance and until now -/// existed only as a constant; here it gains a consumer. -/// -/// Derived from the DEVICE secret rather than a file key: there is one witness per -/// machine, unrelated to any container. Otherwise a head would be needed -/// for every file, defeating the meaning of "one number outside the profile". +/// K12 key for authenticating a device's lease-cache head. /// -/// The salt is deliberately empty. HKDF salt is optional, and none is available here: -/// there is no file identifier or header salt, only a device. The label -/// in `info` sets the domain, which suffices: separation comes from it, not -/// from salt. -/// -/// What this key does NOT provide: protection against the machine's owner. The device secret is in -/// their profile, so they can derive the key too. Authentication here addresses a fellow machine user -/// with write access to a shared directory, and accidental corruption, not -/// whoever owns the profile. +/// Derived from the device secret with empty salt and the registered cached-lease +/// label. It is independent of any file. It protects against corruption and other +/// users with write access to shared storage, not the owner of the device secret. pub fn derive_witness_key(device: &crate::secret::X25519Secret) -> MacKey { MacKey::from_bytes(*hkdf_sha256::( &[], @@ -567,28 +420,11 @@ pub fn mark_choice(key: &MacKey, layout: &[u8; 32], token: u64, point: u32, vari u32::try_from(value).map_err(|_| CryptoError::BadLength) } -/// K14: claim-code secret derived from its canonical text. -/// -/// Input is the code's **canonical characters**: no separators, uppercase -/// (§3.4). Canonicalization belongs to the interface rather than the key schedule: -/// alphabet, grouping, and whitespace tolerance determine how easily a code can -/// be dictated, without affecting any key byte. Transforming text into a -/// secret, however, affects everything and therefore lives here with the rest of the schedule and its -/// domain label. +/// K14 claim secret from canonical code text: uppercase, without separators. /// -/// Hashes the text rather than bits assembled from the characters. The distinction matters: -/// assembling bits is a second encoding of the same thing, and even a bit-order discrepancy -/// would make the code printed by the author differ from the code -/// entered by the recipient. Text is unambiguous by construction. -/// -/// BLAKE3 rather than HKDF: there is neither salt nor a division into extract and -/// expand; there is exactly one compression of variable-length input into 32 bytes. The separator -/// `0x00` follows the label because text length is not predetermined; -/// without it, `label‖code` would parse ambiguously. -/// -/// Input entropy is **not checked here and cannot be**: the code is already -/// text by this point, and its original randomness is not visible. The -/// [`crate::MIN_CLAIM_BITS`] boundary is checked where the code is generated. +/// Hashing the text avoids a second bit-level encoding. The interface performs +/// canonicalization and checks generated entropy against [`crate::MIN_CLAIM_BITS`]; +/// this function cannot infer entropy from an already chosen string. pub fn claim_secret_from_code(canonical: &[u8]) -> ClaimSecret { let mut hasher = blake3::Hasher::new(); hasher.update(label::CLAIM_CODE.as_bytes()); @@ -597,26 +433,11 @@ pub fn claim_secret_from_code(canonical: &[u8]) -> ClaimSecret { ClaimSecret::from_bytes(*hasher.finalize().as_bytes()) } -/// K7 and K8: recipient share derived from the claim code, and the code's commitment. -/// -/// Returns `(secret_B, commitment)`. Only the commitment enters the -/// container; the code itself is delivered through a second channel. -/// Heir-device X25519 private key derived from the code. -/// -/// # Why a keypair rather than a share -/// -/// The heir's share is FIXED: this file's share B, which cannot be derived from -/// an arbitrary code. The code therefore derives a pair, and the bequest -/// is sealed to its public key with ordinary `seal`: to the server and wire, -/// such an heir is indistinguishable from a device, and neither side -/// knows anything about the code. +/// Heir-device X25519 secret derived from a claim code. /// -/// `salt = file_id`, just as for the code-derived share, for the same reason: the same -/// code issued twice produces different pairs for different files; shared tables cannot -/// be constructed. -/// -/// X25519 scalar multiplication "accepts" any 32 bytes: `from_bytes` performs -/// clamping itself, so no special validation of KDF output is required. +/// The bequest seals the existing share B to this derived device key; it does not +/// derive a replacement share. `file_id` is the salt, separating reuse of the same +/// code across files. X25519 performs scalar clamping when the key is used. #[must_use] pub fn device_secret_from_claim(file_id: &[u8; 16], claim: &ClaimSecret) -> [u8; 32] { *hkdf_sha256::(file_id, claim.expose(), &[label::CLAIM_DEVICE.as_bytes()]) @@ -635,26 +456,12 @@ pub fn secret_b_from_claim(file_id: &[u8; 16], claim: &ClaimSecret) -> (SecretB, (SecretB::from_bytes(*share), *commitment) } -/// K9: slot commitment, `HMAC-SHA256(KEK, "CC/v1/slot-commit"‖core_hash)`. -/// -/// Checked in constant time **before** opening AEAD. Without it, the claim code -/// becomes a partitioning oracle: XChaCha20-Poly1305 is not -/// key-committing, and an attacker constructs a wrapper that opens under many -/// candidate KEKs. -/// -/// Bound to `core_hash`, not `file_id`, for two reasons. First: -/// the binding is strictly stronger: the header-core hash already includes `file_id` and changes -/// when any other author-signed field is substituted. Second: `core_hash` -/// is available where the CEK wrapper is computed, whereas `file_id` is not -/// passed in its signature. -/// -/// No circular dependency: `core_hash` covers the header **without** the key-slot -/// record, precisely why that record is excluded. +/// K9 slot commitment: `HMAC-SHA256(KEK, "CC/v1/slot-commit"‖core_hash)`. /// -/// There is exactly one function. Previously there were two, bound to `file_id` and -/// `core_hash`, violating "no domain label is used -/// twice": one label served two distinct bindings, meaning a tag from one -/// context could be presented in the other. +/// Checked in constant time before AEAD opening: XChaCha20-Poly1305 alone is not +/// key-committing and could expose a claim-code partitioning oracle. `core_hash` +/// binds the header while excluding the slot and wrapped-key records, avoiding +/// a circular dependency. pub fn slot_commitment(kek: &Kek, core_hash: &[u8; 32]) -> [u8; 32] { hmac_sha256(kek.expose(), &[label::SLOT_COMMIT.as_bytes(), core_hash.as_slice()]) } diff --git a/crates/oc-crypto/src/lib.rs b/crates/oc-crypto/src/lib.rs index 356f210..2bba09f 100644 --- a/crates/oc-crypto/src/lib.rs +++ b/crates/oc-crypto/src/lib.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Cryptographic core of the `.cc` container. //! //! A crate-wide, lint-enforced rule: **no I/O, diff --git a/crates/oc-crypto/src/mac.rs b/crates/oc-crypto/src/mac.rs index 85e7be4..ba41708 100644 --- a/crates/oc-crypto/src/mac.rs +++ b/crates/oc-crypto/src/mac.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Message authentication codes. //! //! A separate module because MACs serve where signatures cannot: diff --git a/crates/oc-crypto/src/merkle.rs b/crates/oc-crypto/src/merkle.rs index 965b709..48698cb 100644 --- a/crates/oc-crypto/src/merkle.rs +++ b/crates/oc-crypto/src/merkle.rs @@ -1,37 +1,14 @@ -//! Integrity tree over chunk frames. +// SPDX-License-Identifier: MPL-2.0 +//! Integrity tree over authenticated chunk frames. //! -//! A leaf binds chunk **ciphertext** together with its nonce and tag. Previously it -//! bound only nonce and tag, which was insufficient: Poly1305 tags are not -//! collision-resistant under a known key, allowing CEK holders to rewrite content -//! while retaining the author-signed root. Details and executable verification: -//! [`leaf_of`]. +//! Leaves bind ciphertext as well as index, nonce, and tag: Poly1305 tags alone +//! are not collision-resistant for a holder of the payload key. Hashing ciphertext +//! also lets readers check the author-signed root before decrypting. //! -//! Provides verifiable random reads in O(log n), incremental -//! updates on writes, and a root for future anchoring. File truncation is detected -//! **not** by the tree, but by `total_len` and `chunk_count` under the mutable-region -//! MAC. -//! -//! An odd node is **promoted** to the next level, as in RFC 6962, rather than -//! duplicated: duplicating the final node reproduces the vulnerability class -//! CVE-2012-2459, where different leaf sets yield identical roots. -//! -//! The tree apex (`apex`) is exactly RFC 6962 `MTH`, but the exposed value is instead -//! a root bound to leaf count: `H(0x02 ‖ … ‖ u32be(n) ‖ apex)`. Without -//! this binding, a proof does not determine which tree position it -//! authenticates. The verification hash chain sees only a sequence of "sibling appended -//! on the left or right", and a promoted level contributes nothing, -//! so one chain matches many (index, leaf count) -//! pairs: (0, 3) and (0, 4) have bitwise identical paths; (1, 2) -//! matches (2, 3), (4, 5), (8, 9), … Enumeration through 20 leaves finds -//! 43 such classes out of 47. Path shape cannot provide distinguishing information, so leaf -//! count must enter the hash; otherwise an honest path for leaf i -//! in an n-leaf tree also verifies another position in a tree -//! of another size. RFC 6962 closes the same gap externally: tree size is included -//! in the signed STH. -//! -//! With leaf count fixed, path shape determines the index uniquely -//! (verified exhaustively through 4096 leaves), so binding `n` suffices for -//! the proof to determine the entire pair. +//! Odd nodes are promoted, as in RFC 6962, rather than duplicated. The exposed root +//! binds the leaf count as well as the tree apex, so a proof cannot authenticate +//! a position in a differently sized tree. The mutable-region MAC separately +//! authenticates `total_len` and `chunk_count` for truncation checks. use crate::{CryptoError, TreeHashAlg}; @@ -42,24 +19,10 @@ use crate::{CryptoError, TreeHashAlg}; /// `tree_hash_id` in the signed header must equal. pub const IMPLEMENTED_TREE_HASH: TreeHashAlg = TreeHashAlg::Blake3; -/// Whether this build can compute a tree using the declared algorithm. -/// -/// A second boundary for `tree_hash_id`. Without it, an identifier in a signed -/// header controls nothing: a file declaring SHA-256 would still be read -/// using BLAKE3 and accepted. This is the defect class that produced -/// `alg: none` in JWS: the declared algorithm is not enforced, making the declaration -/// decorative. Another failure follows: two readers (ours and an -/// honest SHA-256 implementation) get different roots from the same -/// file, disagreeing on which file is authentic. +/// Reject a tree algorithm this build does not implement. /// -/// Rejection was chosen over supporting a second hash: implementing SHA-256 trees -/// means a second set of format constants and a second branch at each of four sites -/// encoding the promotion rule, for an algorithm used by none of our -/// files. Rejecting what the build cannot execute is more honest and does not -/// increase the surface. -/// -/// The `match` deliberately lacks `_`: adding a [`TreeHashAlg`] member must break -/// the build here, beside the hasher, rather than pass silently. +/// The algorithm declared by the signed header must select the computation; +/// accepting SHA-256 while computing BLAKE3 would misinterpret that header. pub fn ensure_supported(alg: TreeHashAlg) -> Result<(), CryptoError> { match alg { TreeHashAlg::Blake3 => Ok(()), @@ -76,54 +39,11 @@ pub struct Leaf(pub [u8; 32]); /// Compute the leaf: /// `H(0x00 ‖ "CC/v1/leaf" ‖ u32be(index) ‖ u64be(ct_len) ‖ nonce ‖ tag ‖ ct)`. /// -/// Prefix `0x00` distinguishes a leaf from an internal node (`0x01`), preventing -/// an internal node from masquerading as a leaf. -/// -/// ## Why ciphertext enters the leaf -/// -/// Previously the leaf used only `index ‖ nonce ‖ tag`, a mistake -/// underlying the format's central promise. The author-signed -/// `original_root` is the only thing a CEK holder cannot forge: the mutable-region MAC -/// uses a CEK-derived key, allowing them to rewrite `tree_root`, -/// length, and version counter themselves. The root therefore must depend on -/// content, yet depended only on tags. -/// -/// A tag cannot do that job. Poly1305 is a universal hash function, not a -/// collision-resistant one: it is unforgeable only to someone who **does not know** the key. A -/// CEK holder derives the payload key, takes `(r, s)` as ChaCha20 block 0 for -/// `(key, nonce)`, where nonce is public in the file, and solves the linear equation -/// `Δ_j·r^a + Δ_k·r^b ≡ 0 (mod 2^130−5)` for one block. This is -/// **computation, not search**: one modular inversion. The tag is truncated to 2^128 with -/// p = 2^130−5, so each tag corresponds to roughly four accumulator values, -/// and a solution takes only a few attempts. Executable verification: a 64 KiB forgery -/// with a bitwise identical tag was accepted by the reference -/// XChaCha20-Poly1305 implementation, costing 16 garbage bytes in a block **chosen -/// by the attacker**. -/// -/// Hence `ct` in the preimage. BLAKE3 is collision-resistant regardless of what -/// the adversary knows, so the signed root again binds every byte -/// of content. -/// -/// The property "root verified before decryption" remains intact: -/// hashing ciphertext needs no key, so the reader's first pass remains -/// keyless. O(log n) incremental updates remain too: only the leaf preimage -/// changes, not tree shape. -/// -/// ## Why length is explicit -/// -/// `ct` comes last, so the encoding is unambiguous even without length. `u64be(ct_len)` -/// still comes first: injectivity should not rest on "the tag is -/// always the final 16 frame bytes, so the boundary is visible". That argument is true -/// today and silently breaks at the first frame-layout change. -/// -/// Width is `u64`, not `u32`, although chunk size is capped at a megabyte: `u32` -/// would require fallible conversion from `usize`, and a function with no -/// reason to fail should not return `Result` for an impossible branch. -/// -/// No "algorithm" parameter, deliberately: the only algorithm -/// this build agrees to execute is selected earlier, at header parsing through -/// [`ensure_supported`]. An argument with exactly one acceptable value would -/// add no check while creating an illusion of choice at every call. +/// The prefix separates leaves from nodes. Ciphertext enters the hash because a +/// payload-key holder can construct Poly1305 tag collisions; tags alone cannot +/// bind content to the author-signed root. Hashing needs no key, so verification +/// can precede decryption. The explicit length fixes the ciphertext boundary. +/// [`ensure_supported`] selects the supported tree algorithm before this call. pub fn leaf_of(index: u32, nonce: &[u8; 24], tag: &[u8; 16], ct: &[u8]) -> Leaf { let mut h = blake3::Hasher::new(); h.update(&[0x00]); @@ -150,19 +70,8 @@ pub fn node_of(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { /// Root: `H(0x02 ‖ "CC/v1/node" ‖ u32be(leaf_count) ‖ apex)`. /// -/// `apex` is the RFC 6962 tree apex; the root adds the leaf count. -/// This is the only place tree shape enters a hash: the verification chain itself -/// cannot distinguish it (see module documentation); without this binding one -/// proof would authenticate different positions in differently sized trees. -/// -/// A third prefix byte, not a third label: the byte itself separates -/// domains (`0x00` leaf, `0x01` node), and fixed-width `u32be` prevents -/// parsing from "slipping". Introducing `label::ROOT` would require another entry in -/// the prefix-free registry for a domain already separated unambiguously. -/// -/// Binding leaf count does not prevent appending: adding a chunk -/// already changes the entire root, unlike `chunk_count` in chunk AAD, forbidden by -/// §6.4 precisely because it would invalidate every chunk at once. +/// `apex` follows RFC 6962. Binding leaf count prevents a proof from being reused +/// for a different tree size; prefix `0x02` separates roots from leaves and nodes. pub fn root_of(leaf_count: u32, apex: &[u8; 32]) -> [u8; 32] { let mut h = blake3::Hasher::new(); h.update(&[0x02]); diff --git a/crates/oc-crypto/src/mlkem_p256.rs b/crates/oc-crypto/src/mlkem_p256.rs index ec9b985..4f21e6c 100644 --- a/crates/oc-crypto/src/mlkem_p256.rs +++ b/crates/oc-crypto/src/mlkem_p256.rs @@ -1,37 +1,14 @@ -//! MLKEM768-P256: ML-KEM-768 and ECDH P-256 hybrid, slot mechanism `kem_id = 5`. +// SPDX-License-Identifier: MPL-2.0 +//! ML-KEM-768 and ECDH P-256 hybrid (`kem_id = 5`). //! -//! Normative source: `draft-irtf-cfrg-concrete-hybrid-kems-03`, §4.1 and A.1. -//! This is a CFRG DRAFT, not a completed RFC, explicitly described that way in the decision -//! (`docs/format.md`, version 4, item 1). Feasibility evidence and a live -//! probe with a real TPM: `spikes/p256-mlkem-tpm/`. +//! Implements `draft-irtf-cfrg-concrete-hybrid-kems-03` §4.1 and A.1. This pinned +//! draft construction has its own SHA3-256 combiner; outer K10 does not replace it. +//! The order of all five combiner inputs, including the final label, is fixed. //! -//! # Why alongside X-Wing rather than replacing it -//! -//! X-Wing is defined over X25519, while Platform Crypto Provider offers only ECDH -//! P-256. With just one hybrid, one had to choose between post-quantum -//! protection and a key never leaving the TPM. Here no choice is necessary: the classical -//! half resides in the TPM, the post-quantum half beside it, and an adversary needs BOTH: -//! disk theft yields only the latter, a quantum computer only the former. -//! -//! # Why not the TLS group `SecP256r1MLKEM768` (RFC 10024) -//! -//! Because its rationale relies on the TLS transcript (§6 of that RFC), -//! which cannot be transferred to a standalone KEM inside a file. IANA number 4587 -//! identifies a TLS group, not our mechanism. That mistake was written into -//! the specification and corrected there. -//! -//! # What readers of this code should know -//! -//! **It has its own combiner; outer K10 does not replace it.** The shared secret uses -//! one SHA3-256 call over five values, the last being the label. -//! Order is essential: reordering produces a different secret while breaking nothing on -//! one's own side. -//! -//! **The P-256 scalar uses REJECTION SAMPLING.** Any thirty-two-byte string is -//! valid for X25519, but not for P-256: the scalar must be within the group order. -//! Both the key seed and encapsulation seed therefore include spare bytes, -//! from which the first valid piece is taken. Without the reserve, the number of attempts -//! would make construction nondeterministic; with it, bytes determine the result. +//! P-256 agreement can stay behind the TPM adapter while the ML-KEM half is stored +//! by the host. Scalars use rejection sampling from a fixed seed reserve, keeping +//! the construction deterministic without accepting out-of-range scalars. +//! The TLS group `SecP256r1MLKEM768` is a different construction. use crate::CryptoError; use crate::agreement::{KeyAgreement, P256Agreement}; @@ -113,12 +90,7 @@ fn scalar_from(bytes: &[u8]) -> Result { Err(CryptoError::BadKey) } -/// A pair grown from a single seed. -/// -/// A separate type, not a triple: in a tuple, callers distinguish three semantically different -/// values only by order; confusing the ML-KEM seed with the public -/// half would evade the compiler. -#[derive(Debug)] +/// ML-KEM seed, classical key and composite public key. Secrets are redacted in `Debug`. pub struct Keypair { /// ML-KEM seed: `d‖z`. pub ml_kem_seed: Zeroizing<[u8; ML_KEM_SEED_LEN]>, @@ -128,6 +100,17 @@ pub struct Keypair { pub public_key: [u8; PUBLIC_KEY_LEN], } +// Zeroizing затирает память при Drop, но его Debug раскрывает байты. +impl core::fmt::Debug for Keypair { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Keypair") + .field("ml_kem_seed", &"") + .field("classical", &self.classical) + .field("public_key", &self.public_key) + .finish() + } +} + /// Pair from a single seed: vector and software-party path. /// /// # Errors diff --git a/crates/oc-crypto/src/rsa.rs b/crates/oc-crypto/src/rsa.rs index 1ef947d..8f099d7 100644 --- a/crates/oc-crypto/src/rsa.rs +++ b/crates/oc-crypto/src/rsa.rs @@ -1,38 +1,13 @@ -//! RSA-PSS-SHA256 signature verification: editing-device signature. +// SPDX-License-Identifier: MPL-2.0 +//! RSA-PSS-SHA256 verification for editing-device signatures. //! -//! # Why our own implementation rather than a crate +//! The format fixes RSA-2048, exponent 65537, and a 32-byte salt; other shapes are +//! rejected (`docs/format.md`, version 3, item 7). Only public operations run here, +//! using `crypto-bigint`, already supplied by `p256`. The verifier also builds for +//! WASM; signing belongs to the host's TPM adapter. //! -//! Verification must live in a PURE crate: `oc-format` and `oc-protocol` verify signatures and -//! build for `wasm32-unknown-unknown`, ruling out -//! platform CNG. The dependency tree lacks the `rsa` crate, and taking it is undesirable: -//! it carries RUSTSEC-2023-0071 (Marvin); although only indirectly relevant -//! here (the attack concerns private operations, while the TPM signs), an exception -//! would have to be granted manually. -//! -//! `crypto-bigint` is already in the tree via `p256`. Beyond it, all we need -//! is modular exponentiation with a PUBLIC exponent and parsing -//! the PSS encoding. **The operation contains no secrets**, so constant -//! time is unnecessary; that is exactly why "our own crypto" is acceptable here, -//! not because it is cheaper. -//! -//! # The danger and its consequences -//! -//! Historically, VERIFIERS were what broke: Bleichenbacher's `e = 3` attack targeted -//! careless padding parsing, not RSA strength. Therefore: -//! -//! * **the exponent is not a parameter.** Fixed internally at 65537, with no way to pass `e = 3`. -//! A verifier need not support anything our signer does not produce; -//! * **salt length is required, not recovered.** The format fixes it at 32 bytes -//! (`docs/format.md`, "VERSION 3 OPENED", item 7), so another salt length means -//! rejection, not "accept if it verifies". Recovering length from a delimiter -//! accepts more than the format allows; -//! * **modulus length is specified by type.** RSA-2048 only. -//! -//! # What has been verified by execution -//! -//! The vector in `tests/kat/rsa_pss.kat` came from this machine's LIVE TPM -//! (`spikes/rsa-pss-tpm/`), not invention or documentation. It cannot be rederived -//! from the specification. +//! `tests/kat/rsa_pss.kat` contains a frozen signature obtained from a TPM. +//! The custom padding parser remains a security-review surface. use crate::CryptoError; use crypto_bigint::modular::{BoxedMontyForm, BoxedMontyParams}; diff --git a/crates/oc-crypto/src/rsa_test_signer.rs b/crates/oc-crypto/src/rsa_test_signer.rs index ba14cf7..1c0dbe1 100644 --- a/crates/oc-crypto/src/rsa_test_signer.rs +++ b/crates/oc-crypto/src/rsa_test_signer.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! SOFTWARE RSA-PSS-SHA256 signer: ONLY for probes. //! //! # Why it exists at all diff --git a/crates/oc-crypto/src/seal.rs b/crates/oc-crypto/src/seal.rs index 97d26ca..e663b5a 100644 --- a/crates/oc-crypto/src/seal.rs +++ b/crates/oc-crypto/src/seal.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Seal secrets to a slot recipient's public key. //! //! The construction is Base-mode DHKEM(X25519, HKDF-SHA256) with @@ -27,42 +28,17 @@ use zeroize::Zeroizing; /// X25519 public-key length. pub const PUBLIC_KEY_LEN: usize = 32; -/// KEM this build uses for sealing by default. +/// Default slot-sealing mechanism. /// -/// Previously named `DEFAULT_SEALING_KEM`, correctly when exactly one -/// mechanism was implemented. Format version 2 implements two, making the old name -/// untrue; [`supports_kem`] answers "can we execute it", not a -/// constant. This rename is not cosmetic: a name that outlives its meaning -/// is more dangerous than no name. -/// -/// The default deliberately remains X25519. P-256 is needed when a key -/// lives in a TPM: the provider lacks X25519. Choosing P-256 without that reason -/// would pay for 65 bytes per slot and a slower curve for nothing. +/// X25519 is the software default. P-256 supports TPM agreement keys, and the +/// hybrids use their respective paths. [`supports_kem`] reports executability. pub const DEFAULT_SEALING_KEM: KemAlg = KemAlg::X25519HkdfSha256; -/// Whether this build can open a slot using the declared mechanism. -/// -/// Without this check, a slot's KEM identifier controls nothing: a slot -/// relabeled from X25519 to P-256 would still open using X25519, -/// making algorithm declaration decorative. The same defect class that -/// produced `alg: none` in JWS. -/// -/// The specification (§3.3) promises slots with different KEMs can coexist in one -/// file, so an unknown KEM should not reject the entire file but -/// skip that particular slot: another slot in the same file may be usable. -/// The caller decides "skip or reject"; this only answers -/// "supported or not". +/// Whether this build implements the declared slot mechanism. /// -/// The `match` deliberately lacks `_`: adding a [`KemAlg`] member must break -/// the build here, beside implementation, rather than pass silently. Precisely why -/// the table belongs here rather than on the type itself: sealing and opening implementations -/// are neighboring functions in this file. -/// -/// This is the SOLE answer to "is the mechanism executable" across the -/// repository; [`KemAlg::ensure_supported`] is the same table expressed as `Result`, -/// not a second opinion. Length tables (`oc_format::header`, [`crate::kdf`]) -/// answer a DIFFERENT question, "what shape are the fields"; agreement of their -/// gaps with this table is tested rather than remembered. +/// The caller decides whether an unsupported slot is skipped or rejected. +/// Format-version and length checks belong to `oc-format`; this table answers +/// only executability. The exhaustive match forces new KEMs to be considered. pub fn supports_kem(kem: KemAlg) -> bool { match kem { KemAlg::X25519HkdfSha256 => true, @@ -90,23 +66,10 @@ pub fn supports_kem(kem: KemAlg) -> bool { } } -/// Construct slot-sealing `info`. -/// -/// One function for both parties rather than two identical ones: writer and -/// reader constructions diverging by even one byte would yield a file our own -/// packer sealed but our own unpacker could not open, with the cause found -/// only by comparing bytes. +/// Construct the shared writer/reader slot-sealing `info`. /// -/// The KEM identifier enters `info` and therefore key derivation. Otherwise §3.3 -/// would promise coexistence of different mechanisms without cryptographic -/// support: an adversary relabeling a slot with another `kem_id` would get -/// exactly the same key. RFC 9180 addresses this with its `suite_id`, for -/// the same reason: once multiple mechanisms exist, mixing them must be -/// impossible rather than merely undesirable. -/// -/// Purpose has type [`Label`], not bytes: it distinguishes slots, -/// and a string invented outside the registry would introduce a fourth slot kind -/// unknown to specification §3.6 and every I-12 probe. +/// The registered [`Label`], KEM identifier, and file ID bind the derived key to +/// the slot purpose and algorithm (`docs/format.md` §3.3, §3.6). pub fn slot_info(purpose: Label, kem: KemAlg, file_id: &[u8; 16]) -> Vec { let purpose = purpose.as_bytes(); let mut info = Vec::with_capacity(purpose.len().saturating_add(17)); @@ -124,21 +87,7 @@ pub fn slot_info(purpose: Label, kem: KemAlg, file_id: &[u8; 16]) -> Vec { /// info = "CC/v1/a-to-device" ‖ u8(kem_id) ‖ file_id(16) ‖ device_fpr(32) ‖ u64be(seq) /// ``` /// -/// # Why in the core although this is a server message -/// -/// For the same reason as [`slot_info`]: TWO parties assemble the bytes, the server -/// sealing the share and the device unwrapping it. Before the move, this function -/// was copied verbatim in `cc-authority` and `cc-cli`; each copy -/// explained why duplication was acceptable: the client cannot depend on the server. -/// The argument remains valid; the conclusion was wrong: a shared dependency -/// belongs in the CORE both parties depend on, not in a second copy -/// of normative bytes. -/// -/// The cost of diverging copies is precise: different `info` yields a different key, -/// appearing as "share does not unwrap", a cryptographic failure where -/// there is none. -/// -/// The lease sequence uses `u64be`, not `u64le`: K11's vector freezes the order. +/// Both parties use this helper. The sequence's big-endian encoding is frozen. #[must_use] pub fn a_to_device_info( kem: KemAlg, @@ -264,6 +213,11 @@ pub fn seal( plaintext: &[u8], rng: &mut R, ) -> Result { + // K10 связывает байты ключа, а получатель восстанавливает каноническую + // координату. Алиас с тем же DH дал бы конверт, который не откроется. + if !canonical_x25519_public(recipient_public) { + return Err(CryptoError::BadKey); + } // Эфемерная пара на каждый вызов даёт уникальность ключа AEAD. let mut eph_bytes = Zeroizing::new([0u8; SHARED_LEN]); rng.fill_bytes(eph_bytes.as_mut_slice()); @@ -313,6 +267,10 @@ pub fn seal_p256( plaintext: &[u8], rng: &mut R, ) -> Result { + // K10 связывает байты; public_key() получателя возвращает несжатую точку. + if recipient_public.len() != 65 || recipient_public.first() != Some(&0x04) { + return Err(CryptoError::BadLength); + } use crate::agreement::KeyAgreement as _; let eph = crate::agreement::P256Agreement::generate(rng); @@ -361,6 +319,19 @@ fn seal_core( Ok(SealedBlob { enc: eph_public.to_vec(), nonce, ct }) } +// Открытая little-endian координата должна быть меньше p = 2^255 - 19. +// Это ограничение Seal; низкоуровневый X25519 по-прежнему принимает RFC7748 aliases. +fn canonical_x25519_public(public: &[u8; PUBLIC_KEY_LEN]) -> bool { + let mut modulus = [0xff; PUBLIC_KEY_LEN]; + if let Some(first) = modulus.first_mut() { + *first = 0xed; + } + if let Some(last) = modulus.last_mut() { + *last = 0x7f; + } + public.iter().rev().cmp(modulus.iter().rev()).is_lt() +} + /// Seal a slot using the X-Wing HYBRID, `kem_id = 4`. /// /// A separate function beside [`seal`] and [`seal_p256`] for the same reason diff --git a/crates/oc-crypto/src/secret.rs b/crates/oc-crypto/src/secret.rs index 958e357..1142472 100644 --- a/crates/oc-crypto/src/secret.rs +++ b/crates/oc-crypto/src/secret.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Secret types. //! //! Each secret has its own type rather than `[u8; 32]`, for two reasons. First: diff --git a/crates/oc-crypto/src/sign.rs b/crates/oc-crypto/src/sign.rs index 36fcc41..f961f42 100644 --- a/crates/oc-crypto/src/sign.rs +++ b/crates/oc-crypto/src/sign.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Signatures. //! //! Only [`Transcript`] can be signed, and its constructor requires a domain diff --git a/crates/oc-crypto/src/stream.rs b/crates/oc-crypto/src/stream.rs index 7550d68..900cf4d 100644 --- a/crates/oc-crypto/src/stream.rs +++ b/crates/oc-crypto/src/stream.rs @@ -1,31 +1,10 @@ -//! Payload chunking discipline. +// SPDX-License-Identifier: MPL-2.0 +//! Streaming payload encryption shared by native and WASM hosts. //! -//! # Why here rather than alongside I/O -//! -//! Before 2026-09-09 the loop lived in `cc_cli::payload::seal_stream`, appropriately -//! while there was one host. Now there are two: the same loop is needed by a wrapper for -//! other languages that builds for `wasm32` and cannot reach `cc-cli`, -//! which does not build for wasm at all. -//! -//! Reimplementing the loop elsewhere is forbidden, not to save lines. Its behavior -//! is a CRYPTOGRAPHIC CONTRACT rather than file handling: -//! -//! * empty input yields ONE zero-length chunk rather than zero chunks; otherwise a file -//! would have no authentication tag and no tree leaf; -//! * each chunk nonce is derived by HEDGING over the seed and plaintext -//! (I-1, decision C-13), not taken from the RNG; -//! * the tree leaf is computed from `nonce ‖ tag ‖ ct`, with leaf order matching -//! chunk order. -//! -//! If two implementations differed on any of these three rules, files built by different -//! hosts would no longer open in each other, with no way to -//! notice: both would pass their own tests. -//! -//! # How this remains pure -//! -//! No files, clocks, or internal RNG. Source and sink arrive -//! as closures, the RNG as a parameter. `std::io` is not mentioned here: -//! the `Read`/`Write` adapter belongs to the host that has them. +//! Source and sink are closures; randomness is supplied by the caller. Empty +//! input produces one authenticated, zero-length chunk. Nonces use hedging, and +//! Merkle leaves bind each chunk's index, nonce, tag, and ciphertext in stream order. +//! Keeping this loop shared preserves the same bytes across host adapters. use crate::aead::{NONCE_LEN, TAG_LEN, seal_chunk_hedged}; use crate::merkle::{Leaf, MerkleTree}; diff --git a/crates/oc-crypto/src/tpm.rs b/crates/oc-crypto/src/tpm.rs index 144db43..b5a7d77 100644 --- a/crates/oc-crypto/src/tpm.rs +++ b/crates/oc-crypto/src/tpm.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! TPM 2.0 credential protection: the computable part of `TPM2_MakeCredential` //! (B6a, `docs/protocol.md` §9.11). //! diff --git a/crates/oc-crypto/src/transcript.rs b/crates/oc-crypto/src/transcript.rs index 302d4f2..e434dfe 100644 --- a/crates/oc-crypto/src/transcript.rs +++ b/crates/oc-crypto/src/transcript.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Transcript: the only way to obtain bytes for a signature or MAC. //! //! This type exists for one invariant: **signing bytes without domain diff --git a/crates/oc-crypto/src/wrap.rs b/crates/oc-crypto/src/wrap.rs index ee66193..fef7416 100644 --- a/crates/oc-crypto/src/wrap.rs +++ b/crates/oc-crypto/src/wrap.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Content-key wrapping. //! //! CEK is random and **wrapped** under KEK, not derived from it. The distinction diff --git a/crates/oc-crypto/src/xwing.rs b/crates/oc-crypto/src/xwing.rs index 5da46a7..97e4297 100644 --- a/crates/oc-crypto/src/xwing.rs +++ b/crates/oc-crypto/src/xwing.rs @@ -1,26 +1,12 @@ -//! X-Wing: X25519 and ML-KEM-768 hybrid, slot mechanism `kem_id = 4`. +// SPDX-License-Identifier: MPL-2.0 +//! X-Wing: X25519 and ML-KEM-768 hybrid (`kem_id = 4`). //! -//! Normative source: `draft-connolly-cfrg-xwing-kem-10` (March 2, 2026), -//! §5.2–5.5; decision choosing this construction and its rationale: -//! `docs/format.md`, "VERSION 3 OPENED", item 1 (the item itself lives in version 4). -//! Measurements supporting the decision: `spikes/ml-kem-cost/`. +//! Implements `draft-connolly-cfrg-xwing-kem-10` §5.2–5.5, pinned by the format +//! decision and `tests/kat/xwing.kat`. Both keypairs expand from a 32-byte seed. +//! The combiner label follows the other inputs; moving it changes the construction. //! -//! # What readers of this code should know -//! -//! **The private half is 32 bytes, not 2400.** Both pairs grow from one -//! seed through SHAKE256, so `device.key` remains a thirty-two-byte -//! file, as before the hybrid. This is a construction property, not our -//! optimization, and must not be lost: storing an expanded ML-KEM key would introduce -//! a second key-file format. -//! -//! **The combiner label comes LAST.** An implementation written from memory placed -//! it first, a silent mistake: two parties making the same mistake agree -//! with each other and disagree with the rest of the world. Caught by the draft's -//! vectors (`tests/kat/xwing.kat`), not by reasoning. -//! -//! **The hybrid targets X25519, hence a SOFTWARE key.** TPM keys are P-256, -//! where X-Wing is undefined. Post-quantum protection and recipient hardware -//! binding are currently mutually exclusive; see `docs/threat-model.md` §2. +//! X-Wing uses software X25519 keys. For a TPM-backed P-256 classical half, use +//! [`crate::mlkem_p256`], a separate hybrid with its own combiner. use crate::CryptoError; use ml_kem::array::{Array, ArrayN}; diff --git a/crates/oc-crypto/tests/kat.rs b/crates/oc-crypto/tests/kat.rs index 9520dd4..c801ba1 100644 --- a/crates/oc-crypto/tests/kat.rs +++ b/crates/oc-crypto/tests/kat.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 #![allow( clippy::unwrap_used, clippy::expect_used, diff --git a/crates/oc-crypto/tests/measure_packing_path.rs b/crates/oc-crypto/tests/measure_packing_path.rs index de14454..28fc753 100644 --- a/crates/oc-crypto/tests/measure_packing_path.rs +++ b/crates/oc-crypto/tests/measure_packing_path.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Литы сняты для ЗАМЕРА. `unwrap`/`expect`/`panic` — словарь инструмента, // останавливающегося на первой неудаче; арифметика и индексация — счёт по // чанкам; часы запрещены чистым крейтам, а здесь измеряется именно время. diff --git a/crates/oc-crypto/tests/review_regressions.rs b/crates/oc-crypto/tests/review_regressions.rs new file mode 100644 index 0000000..89dd40b --- /dev/null +++ b/crates/oc-crypto/tests/review_regressions.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 +#![allow(clippy::unwrap_used, clippy::panic, clippy::indexing_slicing)] +use oc_crypto::agreement::{KeyAgreement, P256Agreement}; +use oc_crypto::{CryptoError, mlkem_p256, seal, secret::X25519Secret}; + +// Repeatable RNG for format and rejection checks only. +struct Fixed; +impl rand_core::TryRng for Fixed { + type Error = core::convert::Infallible; + fn try_next_u32(&mut self) -> Result { Ok(0x42424242) } + fn try_next_u64(&mut self) -> Result { Ok(0x4242424242424242) } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { + dest.fill(0x42); + Ok(()) + } +} +impl rand_core::TryCryptoRng for Fixed {} + +#[test] +fn hybrid_pair_debug_redacts_the_mlkem_seed() { + let pair = mlkem_p256::keypair_from_seed(&[0x31; 32]).unwrap(); + let secret_rendering = format!("{:?}", pair.ml_kem_seed.as_slice()); + assert!(!format!("{pair:?}").contains(&secret_rendering), + "Keypair Debug exposes the ML-KEM seed"); + assert!(format!("{pair:?}").contains("")); +} + +#[test] +fn compressed_p256_recipient_is_refused_before_sealing() { + let recipient = P256Agreement::from_be_bytes(&[0x11; 32]).unwrap(); + let public = recipient.public_key(); + let valid = seal::seal_p256(&public, b"info", b"aad", b"secret", &mut Fixed).unwrap(); + assert_eq!(&*seal::open_with(&recipient, &valid, b"info", b"aad").unwrap(), b"secret"); + let mut compressed = vec![0x02 | (public[64] & 1)]; + compressed.extend_from_slice(&public[1..33]); + // The point and DH result match; only the transcript encoding differs. + assert!(recipient.agree(&compressed).unwrap().ct_eq(&recipient.agree(&public).unwrap())); + let result = seal::seal_p256(&compressed, b"info", b"aad", b"secret", &mut Fixed); + if let Ok(ref unusable) = result { + assert!(seal::open_with(&recipient, unusable, b"info", b"aad").is_err()); + } + assert!(matches!(result, Err(CryptoError::BadLength))); +} + +#[test] +fn noncanonical_x25519_recipient_is_refused_before_sealing() { + let recipient = X25519Secret::from_bytes([0x11; 32]); + let public = seal::x25519_public(&recipient); + let valid = seal::seal(&public, b"info", b"aad", b"secret", &mut Fixed).unwrap(); + assert_eq!(&*seal::open(&recipient, &valid, b"info", b"aad").unwrap(), b"secret"); + let mut alias = public; + alias[31] |= 0x80; + let agreement = oc_crypto::agreement::X25519Agreement::new(&recipient); + assert!(agreement.agree(&alias).unwrap().ct_eq(&agreement.agree(&public).unwrap())); + let result = seal::seal(&alias, b"info", b"aad", b"secret", &mut Fixed); + if let Ok(ref unusable) = result { + assert!(seal::open(&recipient, unusable, b"info", b"aad").is_err()); + } + assert!(matches!(result, Err(CryptoError::BadKey))); +} diff --git a/crates/oc-crypto/tests/security_probe_keyschedule.rs b/crates/oc-crypto/tests/security_probe_keyschedule.rs index b30d3c5..03c318c 100644 --- a/crates/oc-crypto/tests/security_probe_keyschedule.rs +++ b/crates/oc-crypto/tests/security_probe_keyschedule.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки безопасности. Линтерные запреты рабочего // кода к пробам не применяются — проба вправе делать то, чего продукт делать не // должен. diff --git a/crates/oc-crypto/tests/security_probe_seal.rs b/crates/oc-crypto/tests/security_probe_seal.rs index 97de844..6f1ced0 100644 --- a/crates/oc-crypto/tests/security_probe_seal.rs +++ b/crates/oc-crypto/tests/security_probe_seal.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки безопасности. Линтерные запреты рабочего // кода к пробам не применяются — проба вправе делать то, чего продукт делать не // должен. diff --git a/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs b/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs index a04264a..8e19c9b 100644 --- a/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs +++ b/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки. Запреты рабочего кода к пробам не // применяются — проба вправе делать то, чего продукт делать не должен. // diff --git a/crates/oc-crypto/tests/security_probe_tree_signatures.rs b/crates/oc-crypto/tests/security_probe_tree_signatures.rs index cf7edc1..761add3 100644 --- a/crates/oc-crypto/tests/security_probe_tree_signatures.rs +++ b/crates/oc-crypto/tests/security_probe_tree_signatures.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки безопасности. Часть тестов здесь КРАСНАЯ // НАМЕРЕННО: падение теста и есть доказательство находки. Линтерные запреты // рабочего кода к пробам не применяются — проба вправе делать то, чего продукт diff --git a/crates/oc-crypto/tests/spec_labels.rs b/crates/oc-crypto/tests/spec_labels.rs index ffc0322..030a756 100644 --- a/crates/oc-crypto/tests/spec_labels.rs +++ b/crates/oc-crypto/tests/spec_labels.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Чтение спеки на этапе компиляции: `include_str!` не рантайм-I/O, чистота крейта // не нарушена. Прочие послабления — обычные для тестов. #![allow( diff --git a/crates/oc-crypto/tests/zeroize_on_drop.rs b/crates/oc-crypto/tests/zeroize_on_drop.rs index bba41d1..9e1e470 100644 --- a/crates/oc-crypto/tests/zeroize_on_drop.rs +++ b/crates/oc-crypto/tests/zeroize_on_drop.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Secrets from dependency crates wipe on destruction too, not just ours. //! //! `seal::open` COPIES the long-term device secret into diff --git a/crates/oc-engine/Cargo.toml b/crates/oc-engine/Cargo.toml index a828eee..d54590d 100644 --- a/crates/oc-engine/Cargo.toml +++ b/crates/oc-engine/Cargo.toml @@ -7,14 +7,13 @@ readme = "README.md" version.workspace = true edition.workspace = true rust-version.workspace = true -# Source-available core; see the complete grant and earlier-version rights. -license-file = "../../LICENSE-OPENCRATE" +license = "MPL-2.0" publish.workspace = true [dependencies] -oc-format = { version = "=0.0.2", path = "../oc-format" } -oc-crypto = { version = "=0.0.2", path = "../oc-crypto" } -oc-policy = { version = "=0.0.2", path = "../oc-policy" } +oc-format = { version = "=0.0.3", path = "../oc-format" } +oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } +oc-policy = { version = "=0.0.3", path = "../oc-policy" } # Те же версии, что у `oc-crypto` и `cc-cli`: новых крейтов в дереве ноль. rand_core = { version = "0.10.1", default-features = false } zeroize = { version = "1.9.0", features = ["derive"] } diff --git a/crates/oc-engine/LICENSE b/crates/oc-engine/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/crates/oc-engine/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crates/oc-engine/README.md b/crates/oc-engine/README.md index a166076..8821257 100644 --- a/crates/oc-engine/README.md +++ b/crates/oc-engine/README.md @@ -2,14 +2,14 @@ Packing plans, per-file secrets, recipient slots and header assembly. -[Documentation](../../docs/index.md) · [Packaging guide](../../docs/getting-started.md#packing-integration) · [Source](src/lib.rs) +[Documentation](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/index.md) · [Packaging guide](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/getting-started.md#packing-integration) · [Source](https://github.com/AlexiAxAxA/OpenCrate/blob/main/crates/oc-engine/src/lib.rs) Use this crate in a document packaging pipeline. The host provides recipients, policy and secure randomness to `plan`, streams content encryption, constructs `SealedInfo` from the actual output, and consumes the session with `Session::assemble`. -![Where packaging fits in a document's journey](../../docs/assets/document-lifecycle.svg) +![Where packaging fits in a document's journey](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/assets/document-lifecycle.svg) Keep the request consistent between planning and assembly. The application retains responsibility for the author's signing key, signs the final header @@ -18,3 +18,7 @@ transcript and publishes the completed file atomically. The engine's separation from filesystem, transport and clocks makes its hosting an application choice. It is a library, rather than a document-sharing server. Build the reference with `cargo doc -p oc-engine --no-deps` from the repository root. + +## License + +Licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). diff --git a/crates/oc-engine/src/edit.rs b/crates/oc-engine/src/edit.rs index 208a901..2729bff 100644 --- a/crates/oc-engine/src/edit.rs +++ b/crates/oc-engine/src/edit.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Edit writer: frames, tree, mutable region (`docs/format.md`, //! "EDITING IS EXECUTABLE", item G). //! diff --git a/crates/oc-engine/src/lib.rs b/crates/oc-engine/src/lib.rs index 83d80ca..dbe6238 100644 --- a/crates/oc-engine/src/lib.rs +++ b/crates/oc-engine/src/lib.rs @@ -1,61 +1,18 @@ -//! Packing engine: everything that DECIDES, nothing that processes content bytes. +// SPDX-License-Identifier: MPL-2.0 +//! Packing keys, recipient slots, and the container header. //! -//! # Why this crate exists -//! -//! The product plan divides work as follows: the server decides (content -//! key, shares, slots, header construction and hashes); the device -//! computes (AEAD over document bytes). The boundary is established NOW -//! so the key-holding component can later move into an enclave through a hosting change -//! rather than a rewrite. -//! -//! Before this crate there was no boundary: `cc_cli::container::protect` performed -//! both halves consecutively, making "move the engine into an enclave" meaningless: -//! the repository contained nothing to move. -//! -//! # What is forbidden here and why the build checks it -//! -//! No I/O, clocks, or internal RNG: the RNG arrives -//! as a parameter. This crate passes the same purity gate as `oc-format`, -//! `oc-protocol`, `oc-crypto`, and `oc-policy`: compilation for -//! `wasm32-unknown-unknown` and the `SystemTime` ban in `clippy.toml`. -//! -//! For the other four, the gate preserves deterministic tests. Here it preserves the -//! boundary itself: an enclave cannot accept a component already dependent on -//! the host machine. Relying on a reviewer to check every change is equivalent to -//! not checking. -//! -//! # Call order and why there are two calls rather than one +//! The host supplies randomness and processes document bytes. The engine has no +//! I/O or clocks, and retains the CEK while releasing only the derived payload key. //! //! ```text -//! plan() → secrets, payload key (engine) -//! seal_chunks → ciphertext, tree root, length (device) -//! assemble() → header, signature transcript, descriptor (engine) -//! sign+write → author signature and file write (device) +//! plan() -> session and payload key +//! seal_chunks -> ciphertext, length, and tree root (host) +//! assemble() -> header and signing transcript +//! sign+write -> author signature and container (host) //! ``` //! -//! The middle step is `oc_crypto::stream::seal_chunks`. It belongs to crypto, -//! not here, a substantive placement: the engine DECIDES, while that loop PROCESSES -//! bytes and therefore belongs to the device. It moved from `cc_cli::payload` into -//! crypto on 2026-09-09 when there were two devices: the recipient's machine plus -//! a wasm host for other languages. A second implementation of the same loop would -//! silently diverge from the first (`oc_crypto::stream` module documentation). -//! -//! No other split works: private metadata contains document LENGTH, known -//! only to whoever processed the stream. The engine must therefore release the key, await -//! the result, and only then assemble the header. -//! -//! # What the engine does not do, by decision rather than omission -//! -//! **It does not sign.** The AUTHOR's key signs the entire header, -//! including slots (I-6); the engine supplies the transcript and the device -//! applies the signature. Literally following the product plan, "the server signs -//! the policy", would require a second header signature and thus a new format -//! version during a split that must preserve bytes. -//! -//! **The engine does not release CEK.** It exposes only `payload_key`, derived from -//! CEK for the specific chunk size and algorithm. If it released CEK, the enclave -//! would prove image integrity and nothing more: the key unwrapping everything -//! else would reside outside. +//! Assembly follows streaming because private metadata includes the final length. +//! The host signs the entire header with the author's key; the engine does not sign. use rand_core::CryptoRng; use oc_crypto::secret::{Cek, ClaimSecret, PayloadKey, SecretA, SecretB}; diff --git a/crates/oc-engine/src/wire.rs b/crates/oc-engine/src/wire.rs index 48ebbc4..f4c6cfa 100644 --- a/crates/oc-engine/src/wire.rs +++ b/crates/oc-engine/src/wire.rs @@ -1,43 +1,13 @@ -//! Wire between device and engine: message bytes and nothing else. +// SPDX-License-Identifier: MPL-2.0 +//! Message codec between the host and packing engine. //! -//! # Why the codec lives in a PURE crate +//! Messages carry packing requests, public keys, the payload key, stream results, +//! and the completed header. Document content and the CEK stay on their respective +//! sides of the boundary. Transport and process management belong to the host. //! -//! Because it handles bytes, not a pipe. Parsing a message arriving -//! from another process is the same hostile-input parsing as parsing -//! a header: strict lengths, rejection rather than guessing, no panics. Pure-crate -//! lints already enforce all this; introducing a second parser in an I/O -//! crate would remove those protections. -//! -//! Pipes, process spawning, and RNGs live outside, in `cc-engined` and -//! the `cc-cli` client. -//! -//! # What crosses the boundary and what does NOT -//! -//! Crosses: packing requests (rules, public keys, filename), the payload -//! key, sealed-stream information, and the completed header. -//! -//! **Does not cross: any document-content byte.** The engine neither -//! sees nor should see content; the device runs streaming AEAD. This is the -//! product promise "content bytes never leave the author's machine", made -//! verifiable here: record everything passing through the pipe and -//! search for a canary. -//! -//! The payload key deliberately crosses: the device cannot encrypt -//! the stream without it. `CEK` remains inside the engine: -//! `payload_key` cannot derive it, while `CEK` derives everything else. -//! -//! # Why messages are assembled in a buffer that NEVER GROWS -//! -//! Because secrets travel over this wire, the payload key in responses and -//! claim code in requests, while a growing `Vec` returns its old block to the allocator -//! **before any `Drop`** (I-11). An assembler pushing repeatedly left a heap -//! copy of the message at every capacity doubling, where `Zeroizing` is powerless: -//! destruction has not happened, leaving nothing it can wipe. -//! -//! The ENTIRE message size is therefore computed before the first write; the buffer -//! is allocated exactly once to that size and wiped on destruction -//! ([`Message`]). The calculation must match written bytes exactly: a mismatch means -//! [`WireError::BadSize`], not appending into a grown buffer. +//! Messages may contain secrets. Their size is computed before allocating +//! [`Message`], so its fixed-capacity, zeroizing buffer never leaves old copies +//! behind through reallocation. A size mismatch returns [`WireError::BadSize`]. use core::ops::Deref; diff --git a/crates/oc-engine/tests/edit_writer.rs b/crates/oc-engine/tests/edit_writer.rs index 3576f72..6f6b01d 100644 --- a/crates/oc-engine/tests/edit_writer.rs +++ b/crates/oc-engine/tests/edit_writer.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Edit writer: frame reuse, fresh nonces, tree, signature. //! //! Central property: I-1 during editing. Different plaintext at the same index receives diff --git a/crates/oc-engine/tests/hardware_hybrid_golden.rs b/crates/oc-engine/tests/hardware_hybrid_golden.rs index a814dc5..bb23a9b 100644 --- a/crates/oc-engine/tests/hardware_hybrid_golden.rs +++ b/crates/oc-engine/tests/hardware_hybrid_golden.rs @@ -1,64 +1,14 @@ -//! HARDWARE-HYBRID SLOT-LAYOUT GOLDEN ARTIFACT (`kem_id = 5`), AT ENGINE LEVEL. +// SPDX-License-Identifier: MPL-2.0 +//! Frozen engine header with MLKEM768-P256 slots (`kem_id = 5`). //! -//! # Why here rather than beside container golden artifacts +//! Fixed software key material makes the layout reproducible without a live TPM. +//! The artifact covers the complete assembled header, including server, recipient, +//! and author slot order, 1153-byte encapsulations, and 1249-byte public keys. //! -//! Format version 5 was cut for ONE mechanism, MLKEM768-P256, -//! but nothing froze its on-disk layout. A container golden artifact through -//! `cc-cli` is fundamentally impossible, for the reason recorded in -//! `a_hardware_hybrid_golden_is_not_producible_by_this_writer` -//! (`crates/cc-cli/tests/golden.rs`): with mechanism five, the AUTHOR slot must -//! use the same mechanism, while the writer obtains the author's public P-256 point -//! only from the TPM. Every TPM has its own key, making "a byte-identical container on -//! another machine" cease to be meaningful. -//! -//! The engine neither has nor should have this limitation: it is pure and accepts -//! public keys as PARAMETERS (`PublicKeys::device_hardware_hybrid`), so it -//! does not care whether a point came from a TPM or a fixed scalar. This -//! makes a golden artifact possible here and impossible one layer above. -//! -//! # What is frozen -//! -//! `Assembled::header` bytes: the entire ready-to-write header, with server, -//! recipient, and author slots. This is the probe's purpose: both -//! hybrid records (1153-byte `enc`, 1249-byte `key_fpr`, nonce, ciphertext, -//! commitment), their ORDER and position among other header fields. -//! -//! # What is NOT frozen, stated explicitly -//! -//! **No author signature here.** The engine deliberately has none (I-6): the author -//! key signs the entire header and the caller appends the signature -//! (`cc-cli/src/container.rs`, `write_all(output, &signature)` immediately after -//! the header). These frozen bytes therefore are NOT a container and do not -//! pass `verify::verify_and_parse`: `Header::decode` parses them, the same -//! `oc-format` parser without the signature boundary. The artifact freezes -//! layout, not signature presence. -//! -//! **`Assembled::content_desc` is excluded.** Also bytes and also -//! deterministic, but unrelated to slots: derived from -//! `CEK` and `SealedInfo`, it is identical for any recipient kind with the same RNG -//! seed. Freezing it here would create an artifact -//! that cannot distinguish mechanism five from any other, promising -//! broader coverage than it provides. -//! -//! **`Plan::payload_key` is excluded.** Deterministic, but a KEY; secrets never -//! enter the repository in any form. -//! -//! # Opening as well as comparing -//! -//! An artifact nobody can open freezes garbage, permanently. -//! Byte comparison therefore sits beside -//! [`both_hardware_hybrid_slots_open_for_their_owners`]: both slots open -//! using software halves through the same code a device will use, with the path -//! continued through to private-metadata plaintext. -//! -//! # When version 6 is cut -//! -//! Container artifacts have a witness at `tests/golden/v5/`, captured BEFORE -//! switching the writer. This artifact has NO witness: created for the -//! current writer, it will require the same decision as its neighbors when version six -//! is cut. This is recorded here, not performed: creating a witness directory -//! with nothing to hold today except a copy of an adjacent file would create -//! a copy with no reader. +//! It is not a signed container and does not cover TPM isolation, the content +//! descriptor, or payload keys. Companion tests open both hybrid slots and reach +//! the private metadata. Changing the artifact requires a format decision; +//! its version-6 witness has not been created. // Литы отключены только здесь и только те, без которых тест нечитаем: // `unwrap`/`expect`/`panic` — потому что провал пробы и есть паника, а diff --git a/crates/oc-engine/tests/p256_author_golden.rs b/crates/oc-engine/tests/p256_author_golden.rs index 9788758..c85d7cb 100644 --- a/crates/oc-engine/tests/p256_author_golden.rs +++ b/crates/oc-engine/tests/p256_author_golden.rs @@ -1,89 +1,16 @@ -//! P-256 AUTHOR-SLOT LAYOUT GOLDEN ARTIFACT (`kem_id = 2`), AT ENGINE LEVEL. +// SPDX-License-Identifier: MPL-2.0 +//! Frozen engine headers with P-256 author slots (`kem_id = 2`). //! -//! # Why it exists +//! `p256-author.header` covers an X25519 recipient; `p256-author-none.header` +//! covers no recipient and therefore different slot positions. The engine accepts +//! a fixed software P-256 public point, making these headers reproducible without +//! a machine-specific TPM key. //! -//! This slot is written TODAY and BY DEFAULT on every machine where PCP supplies -//! a device key (`cc-cli/src/keys.rs`, sources `Auto` and `Hardware`), yet -//! nothing previously guarded its layout. Review -//! `docs/review/2026-09-21-p256-slot.md` found precisely this gap: all client probes -//! use `CC_DEVICE_BINDING=software`, so no test container -//! carries this slot. The ENDS are frozen (the primitive -//! `tests/kat/seal_p256.kat` and synthetic length-table probe slots), -//! but the middle, what the engine assembles from them, is not. -//! -//! The cost of blindness deserves repeating: the author slot is the only one -//! carrying BOTH shares together. If a reader stopped recognizing `kem_id = 2`, the author -//! would see "no key matched" for their own encrypted file while NOT ONE -//! probe failed. This file introduces that failure detection. -//! -//! # Why here rather than beside container artifacts -//! -//! For the same reason as neighboring `hardware_hybrid_golden.rs`: no container -//! artifact can be produced through `cc-cli`, which obtains its P-256 point only -//! from a TPM; every TPM has its own key, making "a byte-identical container on -//! another machine" cease to be meaningful -//! (`cc-cli/tests/golden.rs`, `fixed_keys`, field `device_tpm`). -//! -//! The engine neither has nor should have this limitation: pure, it accepts -//! the public point as a PARAMETER (`PublicKeys::device_tpm`), indifferent to -//! whether it came from a TPM or fixed scalar. It needs only 65 -//! uncompressed-point bytes. This makes an artifact possible here and impossible -//! one layer above. -//! -//! # What is frozen -//! -//! `Assembled::header` bytes for two recipient kinds, chosen not for -//! exhaustive enumeration but because they are TODAY'S MAJORITY: -//! -//! * `p256-author.header`: a classical X25519 recipient. The ordinary path: -//! with a nonhardware, nonhybrid recipient and an available TPM point, the engine -//! writes an author slot with `kem_id = 2` (`oc-engine/src/lib.rs`, branch -//! `(false, _, Some(tpm_public))`). -//! * `p256-author-none.header`: no recipient at all. Frozen because it is -//! nearly free and covers a DIFFERENT layout: no recipient slot appears in the header, -//! putting the author slot second rather than third. Record ordering and adjacency -//! are as much format properties as their contents. -//! -//! Both follow one engine branch, without duplication: the branch is shared, -//! but the headers differ and can diverge independently. -//! -//! # What is NOT frozen, stated explicitly -//! -//! **No author signature here.** The engine deliberately has none (I-6): the author -//! key signs the entire header and the caller appends the signature -//! (`cc-cli/src/container.rs`, `write_all(output, &signature)` immediately after -//! the header). These frozen bytes therefore are NOT a container and do not -//! pass `verify::verify_and_parse`: `Header::decode` parses them, the same -//! `oc-format` parser without the signature boundary. The artifact freezes -//! layout, not signature presence. -//! -//! **`Assembled::content_desc` is excluded**, for the same reason as its neighbor: -//! it derives from `CEK` and `SealedInfo`, yielding identical bytes for every -//! recipient kind with the same seed; it cannot distinguish mechanism two from -//! any other. -//! -//! **`Plan::payload_key` is excluded.** Deterministic, but a KEY; secrets never -//! enter the repository in any form. -//! -//! **No live TPM here.** The scalar is software, explicitly: the artifact -//! speaks to slot LAYOUT, not actual key non-exportability. -//! The runs recorded in `CLAUDE.md` address live TPMs; this file does not. -//! -//! # Opening as well as comparing -//! -//! An artifact nobody can open freezes garbage, permanently. -//! Opening probes therefore accompany byte comparison: the author -//! slot opens using a SOFTWARE P-256 agreement party, through the same -//! `seal::open_with` and `KeyAgreement` trait the device will use -//! (`cc-cli/src/container.rs`, agreement-party iteration). The production -//! party is hardware; here it is software. Key derivation changes by -//! no bytes, since it depends on the shared secret rather than the key. -//! -//! # When version 6 is cut -//! -//! Like its neighbor, this artifact has NO witness: created for the current -//! writer, it will require the same decision as container artifacts when -//! version six is cut. This is recorded here, not performed. +//! These are headers, not complete signed containers. They freeze slot layout, +//! not author signatures, content descriptors, payload keys, or TPM isolation. +//! Opening tests use software P-256 agreement through the same trait as the host. +//! Changing fixtures requires a format decision; a version-6 witness has not been +//! created for these artifacts. // Литы отключены только здесь и только те, без которых тест нечитаем: // `unwrap`/`expect`/`panic` — потому что провал пробы и есть паника, а diff --git a/crates/oc-format/Cargo.toml b/crates/oc-format/Cargo.toml index cc0b111..29eb40d 100644 --- a/crates/oc-format/Cargo.toml +++ b/crates/oc-format/Cargo.toml @@ -7,21 +7,20 @@ readme = "README.md" version.workspace = true edition.workspace = true rust-version.workspace = true -# Source-available core; see the complete grant and earlier-version rights. -license-file = "../../LICENSE-OPENCRATE" +license = "MPL-2.0" publish.workspace = true [dependencies] sha2 = "0.11.0" -oc-crypto = { version = "=0.0.2", path = "../oc-crypto" } -oc-policy = { version = "=0.0.2", path = "../oc-policy" } +oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } +oc-policy = { version = "=0.0.3", path = "../oc-policy" } zeroize = "1.9.0" # Программный подписант RSA-PSS — только пробам правки (`tests/edit.rs`); # продуктовая сборка признака не видит (resolver 2 не сливает признаки # dev-зависимостей в обычную сборку). [dev-dependencies] -oc-crypto = { version = "=0.0.2", path = "../oc-crypto", features = ["test-signer"] } +oc-crypto = { version = "=0.0.3", path = "../oc-crypto", features = ["test-signer"] } [lints] workspace = true diff --git a/crates/oc-format/LICENSE b/crates/oc-format/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/crates/oc-format/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crates/oc-format/README.md b/crates/oc-format/README.md index 8b358d7..15b993c 100644 --- a/crates/oc-format/README.md +++ b/crates/oc-format/README.md @@ -2,7 +2,7 @@ Container layout, parsing and authenticated header verification. -[Documentation](../../docs/index.md) · [Format specification](../../docs/format.md) · [Source](src/lib.rs) +[Documentation](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/index.md) · [Format specification](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/format.md) · [Source](https://github.com/AlexiAxAxA/OpenCrate/blob/main/crates/oc-format/src/lib.rs) Use this crate when your application reads or assembles `.cc` container structures. It owns TLV layout, structural bounds and the checks that must happen @@ -18,8 +18,12 @@ cargo run --locked -p oc-format --example verify-header -- tests/golden/basic.cc The example verifies a fixture's header and reports an unknown author. Supply your application's author trust policy before treating an authenticated header -as trusted. The [walkthrough](../../docs/getting-started.md) includes a rejection +as trusted. The [walkthrough](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/getting-started.md) includes a rejection control and expected output. The crate receives bytes; the host owns file access, input-size limits and the recipient workflow. Generate the API reference with `cargo doc -p oc-format --no-deps`. + +## License + +Licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). diff --git a/crates/oc-format/examples/verify-header.rs b/crates/oc-format/examples/verify-header.rs index 9c87d9b..87fd570 100644 --- a/crates/oc-format/examples/verify-header.rs +++ b/crates/oc-format/examples/verify-header.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Authenticate a local header without treating an unknown author as trusted. // The example is the I/O host, not part of the pure library. #![allow(clippy::disallowed_methods)] diff --git a/crates/oc-format/src/content.rs b/crates/oc-format/src/content.rs index 56484ad..8458d93 100644 --- a/crates/oc-format/src/content.rs +++ b/crates/oc-format/src/content.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! The container's mutable region. //! //! The author is not present when a file is edited and therefore cannot sign diff --git a/crates/oc-format/src/edit.rs b/crates/oc-format/src/edit.rs index 9bf6357..c32697c 100644 --- a/crates/oc-format/src/edit.rs +++ b/crates/oc-format/src/edit.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Editing: edit key certificate, editor signature, session head, and //! revision verification (`docs/format.md`, "EDITING IS EXECUTABLE", decision 2026-09-17). //! diff --git a/crates/oc-format/src/encoding.rs b/crates/oc-format/src/encoding.rs index f98f322..2fa48a0 100644 --- a/crates/oc-format/src/encoding.rs +++ b/crates/oc-format/src/encoding.rs @@ -1,34 +1,9 @@ -//! Single-byte Cyrillic encodings. +// SPDX-License-Identifier: MPL-2.0 +//! Single-byte Cyrillic decoding shared by display and renderer hosts. //! -//! # Why this belongs in a pure crate -//! -//! Because BOTH halves of the product need encoding support; keeping it in one -//! would require a second copy in the other. The viewer displays text -//! files itself; the renderer decodes email headers. A table duplicated across -//! two crates diverges with the first edit, and divergence here means -//! DIFFERENT LETTERS in two places in one product, rather than a rejection. -//! -//! There is no I/O, clock, or generator here: a table is a table. -//! -//! # Why the tables are generated -//! -//! Because a table of one hundred and twenty-eight values entered by hand -//! almost certainly contains a typo, and that typo displays not garbage but -//! A DIFFERENT LETTER. Neither code review nor looking at the output catches it: -//! the text remains coherent. -//! -//! # Why guessing is acceptable here but not in email -//! -//! Because these are DIFFERENT situations. An email declares its own encoding: -//! guessing there contradicts the document and introduces plausible but wrong -//! letters for no reason. A text file declares nothing, so the choice -//! is between "display using a guess" and "display dots". -//! -//! Dots mean refusing to display a perfectly readable document. Therefore -//! a guess is made but **explicitly identified**: the caller receives the -//! encoding name and must show it to the user. A user seeing meaningful -//! text loses nothing; a user seeing nonsense knows where to look -//! for the cause. +//! Generated tables avoid maintaining separate mappings. Email uses its declared +//! encoding; text files without a declaration may use a guess. The guessed +//! encoding name is returned so the host can show it to the reader. /// The encoding in which the bytes could be read. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/oc-format/src/footer.rs b/crates/oc-format/src/footer.rs index 485073f..9a83857 100644 --- a/crates/oc-format/src/footer.rs +++ b/crates/oc-format/src/footer.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Footer: self-authenticating content after the frames (`docs/format.md`, "FOOTER AND //! TIMESTAMP", decision 2026-09-17, D3). //! diff --git a/crates/oc-format/src/frame.rs b/crates/oc-format/src/frame.rs index c07cb9f..084b79b 100644 --- a/crates/oc-format/src/frame.rs +++ b/crates/oc-format/src/frame.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Message framing: four length bytes, least significant byte first. //! //! # Why this lives here rather than with the first consumer diff --git a/crates/oc-format/src/header.rs b/crates/oc-format/src/header.rs index 6b0d057..58db24c 100644 --- a/crates/oc-format/src/header.rs +++ b/crates/oc-format/src/header.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Container header: the immutable region signed by the author. //! //! Encoding uses TLV from [`crate::tlv`]. Parsing returns both a structure and @@ -118,25 +119,12 @@ pub const FIRST_CONTAINER_VERSION: u16 = 1; pub const CONTAINER_VERSION: u16 = 5; /// Maximum client version: version 3 always requires reader 3 (§2.1). pub const SUPPORTED_READER_VERSION: u16 = 5; -/// Minimum readable format version: the parsing LOWER bound. +/// Minimum readable container version. /// -/// Five rather than one: decision R-1 of 2026-09-19, not incidental -/// tightening. Reading versions 1–4 is PERMANENTLY RETIRED: no container of those versions with -/// magic `CLOSECR1` was ever issued. The 2026-09-08 rename changed -/// the magic and all domain labels on the day version 5 was cut -/// (`docs/format.md`, section "RENAME 2026-09-08"). The promise "versions -/// 1–4 are readable" applied to an EMPTY SET and was tested only by iterating -/// version numbers in a range, never by opening a single file. -/// -/// The underlying rule is broader (R-3): **before the first file goes outside, -/// the reader supports only the writer's version.** Reading version -/// 5 will be removed just as reading 1–4 is today, on the day -/// version 6 is cut and the writer switches. The rule ends -/// when the first external file is issued; that date must be recorded in -/// `docs/format.md`. -/// -/// Numbering is NOT reset: [`FIRST_CONTAINER_VERSION`] remains -/// a historical record, numbers 1–4 are not reused, and version 5 bytes do not change. +/// Versions 1–4 were retired by decision R-1 (2026-09-19): no `CLOSECR1` containers +/// of those versions had been issued. Historical numbers remain reserved. +/// Future compatibility decisions belong in `docs/format.md`; do not change this +/// bound merely to make a fixture pass. pub const MIN_READABLE_CONTAINER_VERSION: u16 = 5; /// Maximum readable format version. /// @@ -249,23 +237,11 @@ pub struct KnownSlot { /// a recipient key directory: without a pinned value it could /// substitute its own key for the recipient's and assemble both shares. pub key_fpr: Option>, - /// Claim-code commitment (K8). Only for [`SlotKind::RecipientClaim`]. - /// - /// A separate field rather than a reused `ct`, which deserves explanation. - /// A claim-code slot has **nothing to seal**: the recipient's share - /// is derived from a code they already hold. The container instead needs - /// to let the recipient check that the entered code is correct before expensive - /// work begins. That is K8. + /// Claim-code commitment K8, only for [`SlotKind::RecipientClaim`]. /// - /// Putting it in `ct` would be tempting and wrong: throughout the format, `ct` - /// means "sealed secret with tag". A field meaning one thing in one slot - /// kind and another in a different kind is precisely the overloading - /// that is later misread. - /// - /// Field composition depends on slot kind, checked in both directions - /// (`encode`/`decode`): for sealing kinds the claim-code commitment - /// is **forbidden**, while `enc`, `nonce`, and `ct` are required; for claim-code slots, - /// the reverse applies. + /// A claim slot derives its share from the code, so it has no sealed ciphertext. + /// [`encode`](Header::encode) and [`decode`](Header::decode) enforce the slot-kind + /// field layout: claim slots carry this commitment; sealing slots carry ciphertext. pub claim_commit: Option<[u8; 32]>, } @@ -289,27 +265,11 @@ pub struct Authority { /// Address list: rotation and self-hosted deployments are inevitable; a single /// hardcoded URL would eventually cause rejection. pub urls: Vec, - /// Server sealing key identifier. - /// - /// A RECORD, NOT A CHECK: easy to confuse. The value is the key to - /// which the writer sealed the `Server` slot (`oc_engine`: `sealing_kid` and - /// the slot recipient are the same value), but NOBODY compares it - /// with anything: neither `cc-cli` nor `cc-viewer` contains a comparison. Binding - /// to the server key comes from the fact that another key simply cannot - /// open the slot. - /// - /// Why no comparison exists or should be added: both copies, this field and the - /// `Server` slot's `key_fpr`, are under the author signature covering the whole header - /// (I-6), so only our own writer can desynchronize them; - /// a check would catch our bug, not an attacker. Worse, "`sealing_kid` - /// verified" would read as "we confirmed we are talking to - /// the right server", although the field never leaves the author's machine and is never - /// shown to the server: `ActivateReq` does not contain it. + /// Identifier of the server key used when packing. /// - /// Enforcing equality during parsing would cost even more: it would bake "the server - /// slot is always 32-byte X25519" into `[u8; 32]`, while `key_fpr` - /// depends on `kem_id` and occupies 1249 bytes for mechanism five. Removing - /// that constraint later would require a format version. + /// This is a signed record, not proof of the identity of a contacted server. + /// Opening the server slot requires the corresponding private key; transport + /// identity and the lease-signing key are separate concerns. pub sealing_kid: [u8; 32], /// **Pinned** lease verification key. /// @@ -409,22 +369,11 @@ pub struct HeaderSpans { pub wrapped_cek_record: Range, } -/// File protection strength, determined by the AUTHOR slot. +/// File protection strength, determined by the author slot. /// -/// Container strength equals that of the weakest sufficient route to the CEK, and -/// the author route is always sufficient. The specification therefore requires the author -/// slot to be at least as strong as every recipient slot (`docs/format.md`, "AUTHOR -/// SLOT MUST BE AT LEAST AS STRONG AS RECIPIENT SLOT"). Thus file strength is the -/// author slot's strength and needs no other recipient slot: a recipient absent -/// from the header and an heir learn it from the same source. -/// -/// Variant order is the specification's requirement table: "five requires -/// five, four requires four or five, classical requires nothing". -/// This is NOT a scale of mechanism numbers: P-256 (`kem_id = 2`) is classical despite -/// having a number greater than one, while `RsaOaep` (3) is not implemented at all. -/// -/// Lives in the parser, not the client (E2, B8): the server applies the same rule -/// to access requests; two copies of one rule would silently diverge. +/// The author route is sufficient to recover CEK, so it must be at least as strong +/// as every recipient route (`docs/format.md`, author-slot strength rule). +/// Ordering follows that rule, not KEM numbers: P-256 remains classical. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Strength { /// X25519, P-256, or RSA-OAEP: neither post-quantum protection nor a requirement on @@ -847,42 +796,11 @@ fn unsupported(tag: u16) -> FormatError { FormatError::UnknownCriticalField { tag } } -/// Character set permitted in a server address. -/// -/// Printable US-ASCII excluding space: `0x21..=0x7E`. Deliberately narrow, -/// for reasons unrelated to URL aesthetics. -/// -/// # Why restrict something we do not even parse -/// -/// Currently the product only PRINTS this field to a person -/// (`cc activate`, `cc inspect`) to compare author-specified addresses with -/// their intended destination. Human consent is the protection -/// mechanism, and comparison is meaningful only while the string cannot: -/// -/// * **move the cursor or color the screen**: `ESC` (0x1B) starts a control -/// sequence, allowing an address printed below to erase a line -/// printed above. "Going to this address" ceases to be true while remaining -/// on screen; -/// * **reverse character order**: U+202E RIGHT-TO-LEFT OVERRIDE -/// displays `moc.dab` as `bad.com`; -/// * **masquerade as another letter**: Cyrillic `а` (U+0430) and Latin `a` -/// look identical in any font. +/// Require printable ASCII without spaces: `0x21..=0x7E`. /// -/// All three concern DISPLAY; all are blocked by one input restriction: -/// printable ASCII contains neither control bytes nor bidirectional marks nor -/// alternative forms of the same letter. -/// -/// # Why this does not exclude non-Latin names -/// -/// Because they have a wire representation: punycode (`xn--…`), required by -/// DNS itself. The restriction matches what is sent over the network; what it excludes -/// is precisely the U-form whose display constitutes the attack. -/// -/// # Why check when writing too -/// -/// For the same reason as zero keys: writing checks prevent issuance of such a container; -/// parsing checks prevent accepting one already issued. -/// A one-sided check would leave the other possibility open. +/// Addresses are displayed for human comparison. This excludes terminal controls, +/// bidirectional marks, and Unicode lookalikes; international hostnames can use +/// punycode. Both writing and parsing enforce the same display constraint. fn check_address_charset(url: &str) -> Result<(), FormatError> { for byte in url.as_bytes() { if !matches!(byte, 0x21..=0x7E) { @@ -1074,44 +992,12 @@ fn encode_known_slot(version: u16, slot: &KnownSlot) -> Result, FormatEr Ok(w.finish().to_vec()) } -/// A slot's field composition must match its kind. -/// -/// Necessary because otherwise fields become optional "on average": a slot -/// sealing a secret while also carrying a claim-code commitment -/// is meaningless yet structurally valid, and a reader -/// encountering it would silently choose one of two interpretations. -/// -/// **Every** distinction in the §2.0 table is checked, not merely commitment presence, -/// along with the claim-code slot mechanism (§2 item 5). +/// Validate the slot-kind field layout (`docs/format.md` §2.0). /// -/// A sealing slot must carry nonempty ciphertext: empty means no secret, -/// making the slot appear usable while yielding nothing. A claim-code slot -/// must carry empty ciphertext: nonempty `ct` there is data nobody is accountable -/// for, because the reader never opens it (the share is derived from the -/// code), and room for a covert channel inside an author-signed header. -/// -/// The remaining two columns were added by item R-4, using the same argument -/// by which the specification forbids nonempty `ct`. Only `ct` and -/// `claim_commit` were checked, allowing a claim-code slot to carry 32 bytes of `key_fpr` -/// and 56 arbitrary bytes in `enc` and `nonce`, pass both checks, and reside inside -/// an author-signed header without anyone reading them. Precisely the -/// "room for a covert channel" the table guards against: the specification required -/// the full table to be enforced, but code enforced two thirds. -/// -/// A claim-code slot forbids `key_fpr` because it has nothing to confirm: it -/// seals to no key, and the code identifies its recipient. Zeroed `enc` and -/// `nonce` are not structural placeholders but their only meaningful values; -/// anything else would imply something the reader does not know -/// about this slot. -/// -/// The `key_fpr` check is deliberately **one-way**. It is forbidden in claim-code slots, -/// but not required in sealing slots, because §2 (tag 6) calls the -/// field optional; requiring it would reject files that can -/// be opened. The table column means "when present, this field is the key -/// to which the slot was sealed", not "it must be present". The first check was -/// two-way and failed the format's own tests, where slots without `key_fpr` -/// are valid: precisely a case where test failure indicated an incorrect change, -/// not an obsolete test. +/// Sealing slots require ciphertext and forbid a claim commitment. Claim slots +/// require that commitment and forbid ciphertext, `key_fpr`, and nonzero `enc` or +/// `nonce`: those fields have no meaning without a recipient key. `key_fpr` +/// remains optional for sealing slots, as specified by tag 6. fn check_slot_shape( kind: SlotKind, kem: KemAlg, @@ -1178,22 +1064,11 @@ fn decode_slots(version: u16, bytes: &[u8]) -> Result, FormatError> Ok(slots) } -/// `enc` length this format version can validate, by mechanism. -/// -/// `None` means not "error" but "this version does not parse slots of this mechanism -/// and may not inspect their field lengths". -/// -/// Necessary because 32-byte `enc` is the **X25519** shape, not the universal slot -/// shape: a P-256 public key takes 33 or 65 bytes, RSA-OAEP encapsulation -/// hundreds, an ML-KEM hybrid over a thousand. While §3.3 unconditionally specified `bytes[32]`, -/// a slot using any other mechanism was not "unknown" but **impossible**: -/// the length check inside the field parsing loop propagated an error, rejecting -/// the whole container even alongside our own fully openable slot. -/// The §3.3 promise "an unknown `kem_id` slot is skipped rather than rejecting the file" -/// could not be satisfied at all. +/// Encapsulation length defined for a format-version/KEM pair. /// -/// `match` without `_`: adding a mechanism to [`KemAlg`] must break compilation here, -/// beside the length table, rather than silently accept it in another mechanism's shape. +/// `None` means this version does not define the mechanism's shape, so its slot +/// may be skipped rather than checked as X25519. The exhaustive match keeps new +/// mechanisms visible when the table changes. fn expected_enc_len(version: u16, kem: KemAlg) -> Option { match (version, kem) { (_, KemAlg::X25519HkdfSha256) => Some(X25519_PUBLIC_LEN), diff --git a/crates/oc-format/src/lib.rs b/crates/oc-format/src/lib.rs index b2af3b8..8728ddd 100644 --- a/crates/oc-format/src/lib.rs +++ b/crates/oc-format/src/lib.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Арифметика длин и смещений — это и есть границы формата, поэтому здесь она // поднята с `warn` (уровень workspace) до `deny`. Атрибутом крейта, а не строкой // в `Cargo.toml`: `[lints] workspace = true` не смешивается с локальными @@ -10,55 +11,14 @@ //! Parsing and assembling the `.cc` container. //! -//! The crate slices bytes and computes offsets. It has no **I/O, clocks, or -//! random number generator**, which is why it can be tested with pure -//! data and built for `wasm32`. +//! The host supplies bytes; this crate performs no I/O, clock reads, or random +//! generation. Authentication uses original byte slices, never reserialization +//! (`docs/format.md` §5). [`Prologue::split`] checks structural boundaries, and +//! [`verify::verify_and_parse`] verifies the header and reports signer trust. //! -//! The boundary is precise because the previous wording ("knows nothing of -//! cryptography...") contradicted `Cargo.toml`: the crate depends on `oc-crypto` and -//! `oc-policy` **for types**: algorithm identifiers, policy structure, and key -//! types. This dependency is deliberate: otherwise parsing would return raw `u8` values -//! instead of validated enums, and checking "can this build execute the declared -//! algorithm" would move to callers and be repeated in each. What -//! the crate truly does not do is cryptographic operations: it does not -//! encrypt, sign, or derive keys. -//! -//! Everything it returns consists of borrowed slices of the original buffer, because -//! signatures are verified over raw bytes rather than the result of -//! reserialization (see `docs/format.md`, section 5). -//! -//! The entry point is [`Prologue::split`]. It is total: every buffer either parses -//! or yields an error, but never panics. -//! -//! # What does NOT live here: the product protocol -//! -//! Documents exchanged between client, server, and witness: activation, -//! author orders, file status, access requests, leases, revocations, -//! attestation, journal, catalog, attribute rules, administrative operations, and -//! replication were moved to `oc-protocol` by decision R-2 (`docs/plan.md`, -//! "D-core-freeze"). This was a MOVE: wire bytes did not change. -//! -//! The boundary follows the rate of change, the only argument that -//! sustains it. The container format FREEZES: once the first file goes outside, -//! every header byte is promised forever and can change only through a new -//! format version and a recorded decision (I-14). The product protocol -//! GROWS with the server: a new request type appears when its mechanism -//! appears. While both kinds lived in one crate, they were indistinguishable -//! from outside, and the promise "this crate is frozen and open" also applied to -//! eight and a half thousand lines changing every week. -//! -//! There is one dependency direction: `oc-protocol` → `oc-format`. No reverse edge: -//! no container parser calls any protocol document. [`FormatError`] remains -//! shared: decision R-2 did not require splitting the error type, and -//! several variants (`UnsupportedLeaseVersion` and neighbors) now -//! represent purely protocol events. -//! -//! Splitting was evaluated on 2026-09-20 and REJECTED: figures and rationale are in the docs for -//! [`FormatError::BadHeaderSignature`]. Briefly, three variants are purely protocol-related: -//! `UnsupportedLeaseVersion`, `BadNoteChar`, and `NotUtf8`. None is returned -//! internally by `oc-format`, but a fourth, `BadHeaderSignature`, is -//! inherently shared and cannot move without changing the error type across -//! `oc-protocol` and affecting `cc-cli` denial messages and exit codes. +//! Client/server documents live in `oc-protocol`, which depends on this crate. +//! [`FormatError`] is shared with those codecs, so some variants describe protocol +//! errors rather than container errors. pub mod content; pub mod edit; @@ -247,32 +207,11 @@ pub enum FormatError { /// key holder, or moved from another file. Details are deliberately absent: /// the attacker has no need to know precisely which check failed. BadContentMac, - /// Header signature mismatch. - /// - /// Distinct from [`FormatError::BadContentMac`] although both mean - /// "authenticity not established": the header and mutable region are authenticated - /// by different keys and parties, and the user needs - /// different explanations. "The file was not signed by the expected signer" and "the file was edited - /// by someone other than the content key holder" are different events requiring different responses. - /// - /// # The name is BROADER than the meaning, deliberately acknowledged - /// - /// `oc-protocol` returns this same variant when a signature on ITS OWN - /// document fails: a lease, order, revocation, replication nudge, - /// or administrative operation, none of which has a header. The name dates from - /// when all these documents lived in `oc-format` alongside the header. + /// Header or signed-document signature mismatch. /// - /// Evaluated on 2026-09-20 and left unchanged. Introducing - /// `oc_protocol::ProtocolError::BadSignature` would change the error type - /// of 108 public `oc-protocol` functions (672 references to `FormatError` - /// within the crate) and rewrite error handling in every consumer currently - /// matching this variant: `cc_cli::lease` (→ `BadSignature`), - /// `cc_cli::binding` (→ `NotAnchored`), `cc_viewer::session` (→ session - /// erasure), and `cc_cli::exit`. The first three define denial TEXT for the user, - /// the fourth the exit CODE, and both are promised to stay unchanged. Cost: edits to hundreds - /// of places for naming precision; benefit: naming precision. A half-finished - /// split is worse than a shared enum, while completing it here costs more - /// than explicitly acknowledged imprecision. + /// Shared with protocol codecs using the same verification error. Mutable-region + /// MAC failures use [`FormatError::BadContentMac`] because they have a different + /// authentication key and recovery path. BadHeaderSignature, } diff --git a/crates/oc-format/src/policy_codec.rs b/crates/oc-format/src/policy_codec.rs index 5208310..998dd0a 100644 --- a/crates/oc-format/src/policy_codec.rs +++ b/crates/oc-format/src/policy_codec.rs @@ -1,29 +1,10 @@ -//! Policy encoding. +// SPDX-License-Identifier: MPL-2.0 +//! Policy encoding with deny-by-default semantics. //! -//! The rule governing this entire module: **a reader encountering something unknown -//! denies**. An action with an unknown tag is neither ignored nor allowed: -//! it enters [`Policy::unknown_actions`], making the file less accessible, not -//! more. Thus any action added in a future version -//! (`ai_ingest`, `ocr`, `forward`) is also denied by old clients, -//! without changing them at all. -//! -//! The opposite behavior, "unknown field means absent means allowed", is -//! the classic way to bypass policy simply by presenting it to an older client. -//! -//! The same rule requires [`encode`] to write unknown actions -//! back onto the wire: otherwise a client that does not understand them erases them -//! through its own re-encoding, and the next reader receives a policy appearing -//! fully understood. Then "present to an older client" becomes -//! "pass the file THROUGH an older client": the same bypass in two steps. -//! -//! The rule also applies to an ABSENT field, not just an unknown -//! tag (§4: "an absent field means denial"). The danger is that a -//! restriction's "zero" value usually means "no restriction": `false` for -//! an obligation removes it; `None` for a limit removes the limit. Therefore -//! an absent field is NEVER read as the default value from -//! `Default`: it either causes a parse failure or takes the strictest -//! meaningful value. Each field's choice and rationale are in the -//! documentation for [`decode`]. +//! Unknown actions enter [`Policy::unknown_actions`] and survive re-encoding; +//! otherwise passing a file through an older client could erase restrictions. +//! Missing fields either fail parsing or take a restrictive value, as documented +//! by [`decode`]. A general `Default` would silently remove some restrictions. use crate::tlv::{TlvReader, TlvWriter}; use crate::FormatError; @@ -119,38 +100,13 @@ pub const FIRST_ACTION_BINDING_VERSION: u16 = 4; pub fn encode(version: u16, policy: &Policy) -> Result, FormatError> { let mut w = TlvWriter::new(); - // Действия пишутся все шесть, включая запрещённые. Писать только разрешения - // было бы короче, но тогда «действие запрещено» и «поле потеряно при - // повреждении» стали бы неразличимы. - // - // Вместе с ними пишутся действия, которых ЭТОТ клиент не знает. Раньше их - // писать было нечем: [`decode`] честно складывал их теги в - // [`Policy::unknown_actions`], а сюда они не доезжали — и круг - // decode → encode стирал сам факт «правила поняты не целиком». Политика, - // прошедшая через клиента постарше, становилась слабее, чем её написал - // автор: тег исчезал, следующий читатель видел полностью понятую политику, - // и `oc_policy::evaluate` пропускал то, что обязан был запретить. Прогнать - // файл через старую версию — приём слишком дешёвый, чтобы оставлять его - // работающим. - // - // Теги внутри ACTIONS обязаны строго возрастать, а неизвестный тег может - // оказаться где угодно среди известных (тег 0 или 7 так же законен, как - // 999), поэтому известные и неизвестные сливаются в одну упорядоченную по - // тегу карту, а не дописываются в конец: дописав, мы получили бы - // `FieldsOutOfOrder` на первом же теге меньше шести. + // Записываем запреты и неизвестные действия, чтобы повторное кодирование + // старым клиентом не стирало ограничения. Общая упорядоченная карта сохраняет + // строгое возрастание тегов, даже если неизвестный тег меньше известного. let mut actions_by_tag: BTreeMap = BTreeMap::new(); - // Значением неизвестного действия пишется ЗАПРЕТ. Клиент не понял, что это - // за действие, и потому не вправе от имени автора утверждать, что тот его - // разрешил: пиши мы сюда `RULE_ALLOW`, противнику хватило бы прогнать - // запрещающую политику через старого клиента, чтобы получить разрешение — - // то есть та же брешь, только с другой стороны. Обратная потеря (автор - // разрешил, записан запрет) идёт в строгую сторону: доступа она не - // добавляет, а тег на месте, поэтому новый клиент видит, что действие - // упомянуто. Сохранить и байт разрешения сегодня негде — - // `Policy::unknown_actions` хранит только тег; расширять её ради - // послабления, которого никто не проверял, было бы обменом безопасности на - // удобство. + // Для неизвестного действия сохраняем тег и пишем запрет. Исходный байт + // правила не хранится в `unknown_actions`; восстановить разрешение нельзя. for unknown in &policy.unknown_actions { let _ = actions_by_tag.insert(*unknown, RULE_DENY); } @@ -271,56 +227,16 @@ fn encode_binding(binding: Binding) -> u8 { } } -/// Parse a policy. -/// -/// Total. An absent field is NEVER read as a default value: -/// silent substitution would turn a damaged or truncated file into one with -/// different and weaker rules, while §4 requires absence -/// to move toward denial. -/// -/// Each field's absence is analyzed separately: there is no universal -/// answer, and the "obvious" answer is wrong for half the fields. -/// -/// **Rule** fields. Four fields present since the format's first day; a file missing -/// any is not a strict file but one whose rules did not reach us. -/// Opening it as valid would hide corruption or tampering from the user, -/// so absence of any produces [`FormatError::MissingField`]: -/// -/// * `ACTIONS`: otherwise the action set would be empty, and the file would "open" without -/// allowing anything: the user would see a malfunction rather than an honest -/// rejection. (An INDIVIDUAL action missing INSIDE the field is different: -/// that means denial, based on `Policy::deny_all`.) -/// * `VALIDITY`: "no validity period" would become "forever", the weakest value. -/// * `NETWORK`: "no network requirement" would become "offline always permitted". -/// * `MIN_BINDING`: an absence would become `Software`, the weakest binding. -/// -/// **Modifier** fields applied over rules already read. Their absence does not prevent -/// understanding what the author allowed, so rejecting opening has a cost without benefit: -/// it would break every file written by a version predating the field, -/// for protection also achieved by strict interpretation. Here absence means -/// the strictest MEANINGFUL value: +/// Parse a policy without weakening missing rules (`docs/format.md` §4). /// -/// * `WATERMARK`: a watermark is required. This was the finding: default `false` silently -/// removed an author-imposed obligation merely by cutting the field out. -/// The reverse mistake is impossible: an extra watermark does not break the file. -/// * `MAX_OPENS`: `OPENS_WHEN_THE_FIELD_IS_ABSENT`, meaning one open. -/// This deserves explanation because the reasoning is not obvious. "The author set -/// no limit" is a legitimate policy state; the reader may not invent a number -/// for the author. In that sense, no limit is indeed not -/// a relaxation. But precisely for that reason, "no limit" must be WRITTEN: -/// [`encode`] always writes the field, encoding `None` as an empty value. That -/// leaves exactly two cases. Field present and empty: the author's intent; read `None`. -/// Field entirely absent: it was cut out, lost, or a third-party implementation -/// did not know how to write it. We know NOTHING of the author's intent, and reading -/// that ignorance as "open as often as you want" presents the weakest -/// possible value as author-signed. Zero ("never open") -/// is unsuitable: it turns a removed field into an unopenable file, -/// giving the attacker a cheap denial of service. One is the boundary between -/// these errors: the recipient can open the file once, and the difference from -/// the author's intent becomes immediately visible rather than silent. +/// Missing `ACTIONS`, `VALIDITY`, `NETWORK`, or `MIN_BINDING` returns +/// [`FormatError::MissingField`]. An individual missing action remains denied. +/// Missing `WATERMARK` requires a watermark. Missing `MAX_OPENS` permits one open; +/// an explicitly present empty field instead means the author set no limit. +/// [`encode`] always writes that field, preserving the distinction. /// -/// The test `no_absent_field_ever_makes_the_policy_weaker` verifies that -/// no combination of missing fields weakens the policy. +/// `no_absent_field_ever_makes_the_policy_weaker` exercises combinations of omitted +/// fields. These rules are field-specific; using `Default` would relax some of them. pub fn decode(version: u16, bytes: &[u8]) -> Result { let mut reader = TlvReader::new(bytes); let mut policy = Policy::deny_all(); diff --git a/crates/oc-format/src/text.rs b/crates/oc-format/src/text.rs index 43509b0..1d1b528 100644 --- a/crates/oc-format/src/text.rs +++ b/crates/oc-format/src/text.rs @@ -1,34 +1,11 @@ -//! Rules for displaying text chosen by AN OUTSIDER. +// SPDX-License-Identifier: MPL-2.0 +//! Shared checks for displaying untrusted text. //! -//! # Why a separate module -//! -//! The same rule existed in FOUR copies in the repository: filenames -//! (`cc-cli`, finding V-8), server denial text (`activate.rs`), the request queue -//! (`decide.rs`), and the note (`access.rs`); a fifth function of the same kind, -//! `printable`, had no list at all. The copies diverged just as copies always -//! do: nobody updated any of the nine-code-point lists, -//! and nobody updated `printable` because a nearby comment promised -//! that its input was already constrained. -//! -//! The rule lives here in the pure crate for two reasons: parsing must -//! REJECT such text, while display must REPLACE it with a dot, and both must -//! use the same list. The responses differ; the set is shared. -//! -//! # What is absent and why -//! -//! * **Restriction to printable ASCII.** Correct for server addresses, which -//! have a wire representation (non-Latin names use punycode), but incorrect for -//! anything a person writes: a document name may legitimately be Russian, -//! as may a note. CATEGORIES are excluded, not alphabets. -//! * **A ban on U+200C and U+200D** (ZWNJ and ZWJ). Invisible but orthographically -//! REQUIRED in Persian, Arabic, and Devanagari, they also join composite -//! emoji. Banning them would repeat the mistake of restricting to -//! ASCII, one level deeper. -//! * **A ban on combining marks.** "Zalgo" with a hundred diacritics breaks layout, -//! but combining marks belong to the writing systems of half the world. A -//! LENGTH limit applies here, rather than a ban on the category. -//! * **A ban on variation selectors** (U+FE00..U+FE0F): these change the appearance -//! of the preceding character, rather than the order or visibility of the line. +//! Parsers reject unsafe characters; display code replaces them. Both use this +//! set to avoid differences between validation and rendering. Controls and +//! bidirectional formatting are excluded without restricting text to ASCII. +//! ZWNJ, ZWJ, combining marks, and variation selectors remain valid because they +//! are used by ordinary writing systems and emoji. Callers enforce length limits. /// Code points that reorder displayed text. /// diff --git a/crates/oc-format/src/tlv.rs b/crates/oc-format/src/tlv.rs index b24f61c..f684443 100644 --- a/crates/oc-format/src/tlv.rs +++ b/crates/oc-format/src/tlv.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Header encoding: fields with numeric tags, lengths, and values. //! //! The format is closed and third-party developers do not need to read it, so CBOR diff --git a/crates/oc-format/src/verify.rs b/crates/oc-format/src/verify.rs index a846eaf..64c091f 100644 --- a/crates/oc-format/src/verify.rs +++ b/crates/oc-format/src/verify.rs @@ -1,37 +1,14 @@ -//! Container parsing entry point. +// SPDX-License-Identifier: MPL-2.0 +//! Container verification entry point. //! -//! The operation order is part of the security contract, not an implementation -//! detail: structural boundaries, then signature, and only then use of -//! fields. +//! [`Prologue::split`] checks structure, then the header decoder obtains the author +//! key and algorithm suite needed to verify the original header bytes. Decoding +//! therefore precedes signature verification, unlike the idealized order in §5.1. +//! The decoder must safely handle unauthenticated and attacker-signed input. //! -//! This **does not make the input trusted**. A hostile header may be -//! perfectly signed with an attacker's key, so the decoder must tolerate -//! all input; trust in the signer is a separate decision made above. -//! -//! # Departure from specification §5.1 -//! -//! The specification requires signature verification **before** decoding. Literally, -//! this is impossible: both the author's public key and the algorithm suite identifier -//! are **inside** the header; without them verification lacks its key and input. Thus -//! the actual order is: -//! -//! 1. structural splitting ([`Prologue::split`]), without cryptography; -//! 2. header decoding to obtain two values: the author key and suite; -//! 3. signature verification over the raw header bytes; -//! 4. everything else. -//! -//! The departure is safe precisely insofar as the decoder is total: it does not -//! panic, loop forever, or allocate based on an unchecked length. This -//! §5.2 requirement exists regardless of order: "anyone can sign a hostile -//! header". The decoder must therefore tolerate -//! unverified input anyway, and step 2 imposes no work it would not already -//! have to perform after step 3. -//! -//! The cost is explicit: a decoding error takes precedence over a signature -//! error. A file with an unknown critical field and a garbage signature yields -//! [`FormatError::UnknownCriticalField`] rather than a signature failure. This reveals -//! one bit ("the header parsed"), which the attacker can already obtain -//! by signing their own header with their own key. +//! After signature verification, version checks and signer trust are evaluated. +//! A valid signature proves possession of its key, not trust in the author. +//! Because decoding runs first, its errors take precedence over signature errors. use crate::header::{ MAX_READABLE_CONTAINER_VERSION, MIN_READABLE_CONTAINER_VERSION, ParsedHeader, @@ -233,43 +210,12 @@ pub fn verify_and_parse<'a>( }); } - // Версия самого формата проверяется отдельно от `min_reader_version`, и это - // не дублирование. - // - // `min_reader_version` — это ЗАЯВЛЕНИЕ ПИСАТЕЛЯ о том, что клиент такой-то - // версии его файл поймёт. Заявление подписано, но подписал его автор, а - // автором может быть противник: он вправе поставить `container_version: 999` - // и `min_reader_version: 1`, утверждая, что файл будущего формата - // читается по правилам нынешнего. Поверив, клиент применил бы правила - // версии 1 к семантике версии 999. - // - // Поэтому неизвестная версия формата — отказ. Продукт безопасности, увидев - // то, чего не понимает, обязан не открывать, а не «понять большинство - // полей». Прямая совместимость от этого не страдает: она обеспечена - // диапазоном необязательных тегов (§2), который позволяет добавлять поля - // БЕЗ смены версии формата, а смена версии как раз и означает, что - // изменилось нечто, чего старый клиент понять не может. - // Диапазон, а не только верхняя граница. - // - // Проверка одного лишь `>` пропускала версию 0 — формата, которого никогда - // не существовало, — и файл читался по правилам версии 1. Версия ниже - // читаемой опаснее версии выше: она не вызывает подозрений («это же старый - // файл»), хотя означает ровно то же самое — семантику, которой у нас нет. - // - // НИЖНЯЯ ГРАНИЦА — ПЯТЁРКА, А НЕ ЕДИНИЦА (решение Р-1, 2026-09-19). Версии - // 1–4 сожжены для чтения: ни один контейнер этих версий с magic `CLOSECR1` - // наружу не выходил, потому что переименование 2026-09-08 сменило magic и - // метки в тот же день, когда нарезалась пятая. Обещание «1–4 читаемы» - // относилось к пустому множеству и проверялось ровно здесь — перебором - // номера, а не открытием файла. Причина и правило Р-3 («до первого файла, - // ушедшего наружу, читатель держит только версию писателя») записаны у - // `MIN_READABLE_CONTAINER_VERSION`. + // `min_reader_version` — утверждение автора, а не описание формата. + // Проверяем саму версию независимо: подписанный файл с версией 999 и + // `min_reader_version = 1` не становится понятным этому читателю. + // Диапазон исключает и будущие версии, и снятые версии 1–4 (решение Р-1). let container_version = parsed.header.container_version; - // Диапазоном, а не двумя сравнениями: пока обе границы были единицами, форма - // записи не имела значения, а с их расхождением стало важно, что проверка - // одна и обе границы в ней названы вместе. Сегодня границы снова совпали, но - // диапазон остаётся: он разойдётся снова в день нарезки версии 6, и форма - // записи, меняющаяся туда-обратно, прячет то, что менялось по существу. + // Обе границы нужны: версия 0 и снятые версии тоже недопустимы. if !(MIN_READABLE_CONTAINER_VERSION..=MAX_READABLE_CONTAINER_VERSION) .contains(&container_version) { diff --git a/crates/oc-format/tests/edit.rs b/crates/oc-format/tests/edit.rs index 11d5856..e69ec25 100644 --- a/crates/oc-format/tests/edit.rs +++ b/crates/oc-format/tests/edit.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Editing at byte level: certificate, editor signature, session head, and //! the `verify_edition` decision (`docs/format.md`, "EDITING IS EXECUTABLE"). //! diff --git a/crates/oc-format/tests/kat_policy.rs b/crates/oc-format/tests/kat_policy.rs index 35d07d7..9dfb418 100644 --- a/crates/oc-format/tests/kat_policy.rs +++ b/crates/oc-format/tests/kat_policy.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Запрет `disallowed_methods` в `clippy.toml` адресован ПРОДУКТУ: ввод-вывод в // чистых крейтах жить не должен. Тест векторов читает файл по определению — векторы // затем и лежат отдельным файлом, чтобы вторая реализация могла свериться, не читая diff --git a/crates/oc-format/tests/parser_robustness.rs b/crates/oc-format/tests/parser_robustness.rs index eacb7d1..bc02970 100644 --- a/crates/oc-format/tests/parser_robustness.rs +++ b/crates/oc-format/tests/parser_robustness.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Parser robustness against hostile input. //! //! A real `cargo-fuzz` fuzzer requires nightly and separate installation and will diff --git a/crates/oc-format/tests/security_probe_hostile_input.rs b/crates/oc-format/tests/security_probe_hostile_input.rs index 7f62498..c1a6efb 100644 --- a/crates/oc-format/tests/security_probe_hostile_input.rs +++ b/crates/oc-format/tests/security_probe_hostile_input.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки безопасности. Часть тестов здесь КРАСНАЯ // НАМЕРЕННО: падение теста и есть доказательство находки. Линтерные запреты // рабочего кода к пробам не применяются — проба вправе делать то, чего продукт diff --git a/crates/oc-format/tests/security_probe_policy_enforcement.rs b/crates/oc-format/tests/security_probe_policy_enforcement.rs index 714c6e8..4f2e18e 100644 --- a/crates/oc-format/tests/security_probe_policy_enforcement.rs +++ b/crates/oc-format/tests/security_probe_policy_enforcement.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки безопасности. Тесты `probe*` писались // КРАСНЫМИ: падение и было доказательством находки. Все три находки закрыты в // `oc-policy`, поэтому сегодня файл зелёный целиком — и роль проб сменилась с diff --git a/crates/oc-format/tests/skeptic_recheck_slot_tags.rs b/crates/oc-format/tests/skeptic_recheck_slot_tags.rs index 460507d..6716688 100644 --- a/crates/oc-format/tests/skeptic_recheck_slot_tags.rs +++ b/crates/oc-format/tests/skeptic_recheck_slot_tags.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Проба скептика: перепроверка заявления «внутри слота ключа неизвестный тег из // критичного диапазона молча отбрасывается». Прежняя проба брала тег 7, который // у слота ЗАНЯТ (nonce), и получала отказ по длине значения — то есть проверяла diff --git a/crates/oc-policy/Cargo.toml b/crates/oc-policy/Cargo.toml index c07e80a..35ad8d0 100644 --- a/crates/oc-policy/Cargo.toml +++ b/crates/oc-policy/Cargo.toml @@ -7,8 +7,7 @@ readme = "README.md" version.workspace = true edition.workspace = true rust-version.workspace = true -# Source-available core; see the complete grant and earlier-version rights. -license-file = "../../LICENSE-OPENCRATE" +license = "MPL-2.0" publish.workspace = true [dependencies] diff --git a/crates/oc-policy/LICENSE b/crates/oc-policy/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/crates/oc-policy/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crates/oc-policy/README.md b/crates/oc-policy/README.md index bfdaad4..d3ded01 100644 --- a/crates/oc-policy/README.md +++ b/crates/oc-policy/README.md @@ -2,14 +2,14 @@ Access decisions from explicit policy and caller-supplied facts. -[Documentation](../../docs/index.md) · [Runnable example](examples/access-policy.rs) · [Source](src/lib.rs) +[Documentation](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/index.md) · [Runnable example](examples/access-policy.rs) · [Source](https://github.com/AlexiAxAxA/OpenCrate/blob/main/crates/oc-policy/src/lib.rs) The crate has no package dependencies. Its main entry point is `evaluate(policy, lease, action, context)`: either a denial reason or permission with obligations. Missing permissions default to denial, and server policy can only tighten the author's permissions. -![Three decisions from the access-policy example](../../docs/assets/access-decisions.svg) +![Three decisions from the access-policy example](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/assets/access-decisions.svg) Run from the repository root: @@ -17,10 +17,14 @@ Run from the repository root: cargo run --locked -p oc-policy --example access-policy ``` -The [guide](../../docs/getting-started.md) also runs this example in a separate +The [guide](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/getting-started.md) also runs this example in a separate consumer application. The example's facts are synthetic; production facts must come from authenticated evidence and protected application state. The application supplies time, device state and replay counters and enforces every returned obligation. The crate does not operate a printer, clipboard or document window. Build its reference with `cargo doc -p oc-policy --no-deps`. + +## License + +Licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). diff --git a/crates/oc-policy/examples/access-policy.rs b/crates/oc-policy/examples/access-policy.rs index b9613aa..a2b97ed 100644 --- a/crates/oc-policy/examples/access-policy.rs +++ b/crates/oc-policy/examples/access-policy.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Explore access decisions using synthetic facts, without a server or device. use oc_policy::{ diff --git a/crates/oc-policy/src/lib.rs b/crates/oc-policy/src/lib.rs index e329358..96eba34 100644 --- a/crates/oc-policy/src/lib.rs +++ b/crates/oc-policy/src/lib.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Здесь арифметика — это сроки, счётчики открытий и номера лизингов, то есть // ровно те величины, переполнение которых означает расширение доступа. Поднято // с `warn` до `deny`; причина выбора атрибута, а не строки в `Cargo.toml`, — @@ -17,21 +18,11 @@ use core::fmt; use std::collections::{BTreeMap, BTreeSet}; -/// Compare two 32-byte digests in constant time. +/// Compare fixed-size public digests without an explicit early exit. /// -/// A copy of `oc_crypto::digest_eq`, not a call to it: this crate **has and must have -/// no dependencies at all**, neither `subtle` nor `oc-crypto`. Having no -/// dependencies is a condition of verifiability, not hygiene: the pure access -/// decision core is the only part of the system amenable to exhaustive -/// model checking, and any external crate dependency destroys that possibility. -/// -/// The implementation does not rely on optimizer promises that do not exist: the accumulator -/// uses a `volatile`-like technique through `core::hint::black_box` so the -/// compiler does not transform the loop into an early exit on the first difference. -/// -/// Device fingerprints and policy hashes are public, but secrets, tags, and -/// commitments are compared uniformly throughout the repository, avoiding -/// a separate proof of early-exit safety at every site. +/// This dependency-free crate uses XOR accumulation rather than `subtle`. +/// `black_box` discourages optimization but does not provide a constant-time +/// guarantee; keep secrets and authentication tags in the crypto comparison path. #[must_use] pub fn digest_eq(a: &[u8; 32], b: &[u8; 32]) -> bool { let mut diff = 0u8; diff --git a/crates/oc-policy/tests/security_probe_skeptic.rs b/crates/oc-policy/tests/security_probe_skeptic.rs index 26dcb68..ca88620 100644 --- a/crates/oc-policy/tests/security_probe_skeptic.rs +++ b/crates/oc-policy/tests/security_probe_skeptic.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Файл-проба состязательной проверки безопасности. Часть тестов здесь КРАСНАЯ // НАМЕРЕННО: падение теста и есть доказательство находки. Линтерные запреты // рабочего кода к пробам не применяются — проба вправе делать то, чего продукт diff --git a/crates/oc-policy/tests/server_policy_composition.rs b/crates/oc-policy/tests/server_policy_composition.rs index 3259972..e08b163 100644 --- a/crates/oc-policy/tests/server_policy_composition.rs +++ b/crates/oc-policy/tests/server_policy_composition.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Проба: решатель складывает профиль сервера сам. Литы рабочего кода к пробам // не применяются — проба вправе паниковать, это её способ отчитаться. #![allow( diff --git a/crates/oc-protocol/Cargo.toml b/crates/oc-protocol/Cargo.toml index bc260ba..6084780 100644 --- a/crates/oc-protocol/Cargo.toml +++ b/crates/oc-protocol/Cargo.toml @@ -7,8 +7,7 @@ readme = "README.md" version.workspace = true edition.workspace = true rust-version.workspace = true -# Source-available core; see the complete grant and earlier-version rights. -license-file = "../../LICENSE-OPENCRATE" +license = "MPL-2.0" publish.workspace = true [dependencies] @@ -16,9 +15,9 @@ publish.workspace = true # кодек политики и таблица версий живут в `oc-format`. Обратной стрелки нет и # быть не может — ни один разборщик контейнера не зовёт ни один документ # протокола, и это проверено до переноса. -oc-format = { version = "=0.0.2", path = "../oc-format" } -oc-crypto = { version = "=0.0.2", path = "../oc-crypto" } -oc-policy = { version = "=0.0.2", path = "../oc-policy" } +oc-format = { version = "=0.0.3", path = "../oc-format" } +oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } +oc-policy = { version = "=0.0.3", path = "../oc-policy" } [lints] workspace = true diff --git a/crates/oc-protocol/LICENSE b/crates/oc-protocol/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/crates/oc-protocol/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crates/oc-protocol/README.md b/crates/oc-protocol/README.md index 4779f6d..6ae5ae1 100644 --- a/crates/oc-protocol/README.md +++ b/crates/oc-protocol/README.md @@ -2,11 +2,11 @@ Codecs and verification for signed protocol documents. -[Documentation](../../docs/index.md) · [Protocol specification](../../docs/protocol.md) · [Source](src/lib.rs) +[Documentation](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/index.md) · [Protocol specification](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/protocol.md) · [Source](https://github.com/AlexiAxAxA/OpenCrate/blob/main/crates/oc-protocol/src/lib.rs) Use this crate for the documents around a container: activation, leases, revocation, access requests, journal evidence and related control messages. -The [protocol specification](../../docs/protocol.md) defines their representation +The [protocol specification](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/protocol.md) defines their representation and the recipient's required checks. The crate operates on supplied bytes and keys. The application provides @@ -15,6 +15,10 @@ Receiving a decoded message does not establish that it belongs to the requested file, device or policy; enforce the relevant bindings in the integration. Container layout belongs to `oc-format`; the access decision belongs to -`oc-policy`. See the [dependency graph](../../docs/architecture.md). +`oc-policy`. See the [dependency graph](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/architecture.md). From the repository root, build the reference with `cargo doc -p oc-protocol --no-deps`. + +## License + +Licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). diff --git a/crates/oc-protocol/src/access.rs b/crates/oc-protocol/src/access.rs index 1c3355d..1ebe1c8 100644 --- a/crates/oc-protocol/src/access.rs +++ b/crates/oc-protocol/src/access.rs @@ -1,33 +1,13 @@ -//! ACCESS REQUEST documents: the recipient requests, the author approves. +// SPDX-License-Identifier: MPL-2.0 +//! Recipient access requests and author decisions. //! -//! # What this conversation is and how it differs from activation +//! The author can release the existing share B to a new device without changing +//! the container. The server queues and delivers the decision, but the share is +//! sealed to the device key and remains unavailable to that intermediary. //! -//! Activation asks "may THIS device open the file NOW?" -//! and the server decides under rules written by the author beforehand. An access -//! request asks a different question: "is this person among the intended -//! recipients at all?" No server rule can answer that: -//! the recipient was not named when the file was packed. -//! -//! The author decides. The server is a **blind intermediary**: it stores the queue, shows -//! it to the author and delivers the answer. It sees neither share B nor the contents: -//! the answer is sealed to the device key, not the server's. -//! -//! # Why this cost the format no bytes -//! -//! The `AuthorDevice` slot carries BOTH shares (`docs/format.md` §3.3), so the author -//! can give share B to anyone without changing the container or re-signing -//! the header. The issued share lives in a separate sealed block of the same shape -//! already returned by the server; the recipient stores it alongside the lease. -//! -//! # Who the "author" is from the server's perspective -//! -//! The server has no accounts, and none had to be added. The container header -//! carries `author_key` (tag 5), signed BY THAT SAME KEY with strict verification (I-6). Thus -//! the server learns the author's key on file registration, from the file itself, -//! and accepts approval only when signed by that key. -//! -//! The technique is the same as pinning the lease-signing key: authority to -//! decide comes from the signed header, not the peer's claims. +//! Decisions are authenticated with the author key registered from the container +//! header. They answer who may receive a share; lease issuance separately decides +//! whether that device may open the file now. use oc_format::tlv::{TlvReader, TlvWriter}; use oc_format::{FormatError, MAX_HEADER_LEN}; diff --git a/crates/oc-protocol/src/action.rs b/crates/oc-protocol/src/action.rs index ffef93e..40ee986 100644 --- a/crates/oc-protocol/src/action.rs +++ b/crates/oc-protocol/src/action.rs @@ -1,46 +1,14 @@ -//! ACTION DOOR documents: action grant, execution request and lease. +// SPDX-License-Identifier: MPL-2.0 +//! Action grants, execution requests, and one-time leases. //! -//! # What this conversation is and how it differs from an agent grant +//! An [`crate::action::ActionGrant`] constrains arguments rather than authorizing file reads. +//! The door and server both check them with [`crate::action::args_within`]. Grants are signed by +//! the author; [`crate::action::ActionLease`] is signed by the server under its own domain label. +//! Both verify raw bytes before parsing. //! -//! An agent grant ([`crate::agent::AgentGrant`]) says "this door, this -//! subtree, until this time", granting READ access. This asks something else: -//! "this door may push this branch to this remote". The latter cannot be reduced -//! to the former: reading concerns a file, whereas an action concerns ARGUMENTS that do -//! not yet exist when the grant is issued. Thus an action grant names not actions but -//! CONSTRAINTS on them; arguments are checked against those constraints -//! twice: by the door before networking ([`crate::action::args_within`]), and by the server before issuing a lease. -//! -//! # Why a capability rather than a rule -//! -//! The sandboxed agent has no means of acting: no network, key or write access to -//! the tree. Rejection relies not on someone saying "forbidden", but on -//! absence of the means: the model cannot issue itself a lease, and the server will not sign -//! arguments outside the constraints. Injection may persuade the model to ask; -//! a request is all it can achieve. -//! -//! # Three documents and three different trust relationships -//! -//! * [`crate::action::ActionGrant`] is signed by the AUTHOR key, the same key as the container header -//! and agent grant. Layout `signature(64) ‖ body`, `verify_strict` verification over -//! raw bytes BEFORE parsing (I-5, I-6). -//! * [`crate::action::ActionRequest`] is ENTIRELY UNSIGNED, like [`crate::access::AskAccess`]: the door -//! has a key-agreement key, which cannot sign. Requester identity is not trusted from -//! the document: the server compares `door_fpr` with the fingerprint proved in -//! the handshake, in constant time. -//! * [`crate::action::ActionLease`] is signed by the SERVER with its lease-signing key -//! (`authority.lease_verify_key` from the header, pinned by the author's signature). -//! It has its own label ([`oc_crypto::label::ACTION_LEASE`]): the key is shared, so without -//! separate domains permission to open a file could serve as permission to push -//! a branch. -//! -//! # Why `force` is unrepresentable rather than forbidden -//! -//! [`crate::action::Args::GitPush`] has no field for it, required or optional. -//! A prohibition written as a rule needs an enforcement point, and we have -//! two (door and server): one path gets fixed, its neighbor forgotten. -//! An absent field is enforced by parsing: an argument with an extra critical tag -//! is rejected during parsing; an optional tag never reaches the executor, which -//! reads the structure rather than the bytes. +//! [`crate::action::ActionRequest`] is unsigned. The server must bind its `door_fpr` to the +//! identity proved by the handshake. `GitPush` has no force option: the executor +//! uses parsed arguments, so ignored optional fields cannot introduce one. use oc_format::FormatError; use oc_format::tlv::{TlvReader, TlvWriter}; diff --git a/crates/oc-protocol/src/activation.rs b/crates/oc-protocol/src/activation.rs index 3d8b2bc..7a9bcfd 100644 --- a/crates/oc-protocol/src/activation.rs +++ b/crates/oc-protocol/src/activation.rs @@ -1,48 +1,13 @@ -//! Activation documents: device request, server grant, refusal. +// SPDX-License-Identifier: MPL-2.0 +//! Activation documents and the shared request/response envelope. //! -//! # Why separate the document from its delivery mechanism +//! Device keys perform agreement, not signing. Authentication and proof of +//! possession belong to the handshake (`docs/protocol.md` §9.4), not to a device +//! signature in these documents. A claimed fingerprint alone proves no identity. //! -//! Because these are different things, and confusing them is costly. A document is a commitment; -//! a socket carries bytes. The lease already works this way (`signature ‖ body`), so it -//! survives any transport change: it does not know whether it arrived by network, file, -//! or USB drive. -//! -//! Had we done the reverse — wire first, document second — the request's shape would -//! have been dictated by convenient transmission, and would have to change with -//! the transport. The decision is recorded in `docs/protocol.md` §9. -//! -//! # What these documents do NOT carry -//! -//! **A device signature.** There is none to obtain: the device key is for agreement, -//! cannot sign, and `cc-keystore` has no signing function. -//! Proof of possession uses sealing: the server sends a challenge, -//! the device returns an echo (`docs/protocol.md` §9.4) — a SEPARATE step, -//! not yet present here. -//! -//! The direct consequence must be stated explicitly: **the activation -//! request is currently authenticated by nobody.** While a human brings it, -//! that does not matter. Once it arrives from a socket, lack of proof of -//! possession lets someone consume another's limits merely by presenting their -//! fingerprint — so network activation is exposed ONLY together with §9.4. -//! -//! # The same parsing rules as the rest of the format -//! -//! Tags strictly increase (I-7), lengths must exactly match their types -//! (I-8), and trailing bytes after the parsed document are rejected. This parses hostile -//! input: messages come from another party, and leniency -//! would mean the parties interpret the layout differently. -//! -//! # The envelope also carries ACCESS REQUEST documents -//! -//! ALL message kinds live here — both activation and access requests -//! (`crate::access`). One socket must have one kind registry: if we introduced -//! another envelope, the same first byte would mean different things depending -//! on whose parser read it — yet this very byte chooses the parser, meaning -//! the decision precedes knowing whose document -//! has arrived. -//! -//! Access-request documents themselves remain in `crate::access`: the envelope knows -//! their names, not their layouts. +//! The envelope's kind registry also covers access requests; their layouts live +//! in [`crate::access`]. Codecs enforce field order, exact lengths, and complete +//! consumption of the input independently of transport. use oc_format::tlv::{TlvReader, TlvWriter}; use oc_format::{FormatError, MAX_HEADER_LEN}; diff --git a/crates/oc-protocol/src/agent.rs b/crates/oc-protocol/src/agent.rs index 5e47142..edf5503 100644 --- a/crates/oc-protocol/src/agent.rs +++ b/crates/oc-protocol/src/agent.rs @@ -1,39 +1,14 @@ -//! AGENT PROTOCOL documents: agent grant and delegation to a descendant. +// SPDX-License-Identifier: MPL-2.0 +//! Agent grants and delegation chains. //! -//! # What this conversation is and how it differs from approval +//! A grant binds a door, subtree, expiry, and delegation depth. The door uses an +//! agreement key for sealed shares and a separate Ed25519 key for delegations; +//! the author's grant authenticates the latter as `door_verify`. //! -//! Approval ([`crate::access::Decision`]) asks "is this person among -//! the intended recipients of THIS file?": one `file_id`, one signature, no -//! mention of expiration. An agent grant asks instead "this door, this SUBTREE, -//! until this time". It cannot be reduced to a batch of approvals: that would be N -//! human decisions, N signatures and nowhere for the door signing key, -//! expiration or delegation depth. -//! -//! # Why the door has TWO key pairs -//! -//! A device in the protocol is a key-agreement public key (`activation.rs`), which -//! cannot sign. A door must sign delegations to descendants, -//! otherwise there is nothing to authenticate them with. Thus it has a second pair, Ed25519; -//! the author names its public half in the grant as `door_verify`. This key is covered -//! by the author's signature, so an intermediary cannot replace it without breaking -//! the signature. -//! -//! # What is signed -//! -//! **Raw body bytes, not a reconstructed structure**; the signature PRECEDES -//! the body, matching the layout of leases, revocations and orders -//! (`signature(64) ‖ body`). It is checked BEFORE TLV parsing (I-5, I-6): parsing -//! unauthenticated bytes is itself an oracle, yielding distinguishable error codes about -//! material nobody signed. -//! -//! # Whose key verifies the grant -//! -//! The CALLER obtains `author_key` from the container header or the server's file -//! record, not the submitted document. The embedded key -//! is compared with the supplied key in constant time and serves just one purpose: -//! making "to whom this grant is addressed" part of the signed body. The same technique -//! pins the lease-signing key: authority to decide -//! comes from the signed header, not the peer's claims. +//! Signatures cover `signature(64) ‖ body` layouts and verify raw body bytes before +//! TLV parsing. The caller supplies the trusted author key from the container or +//! registered file record; a key embedded in the submitted grant cannot establish +//! its own authority. use oc_format::FormatError; use oc_format::tlv::{TlvReader, TlvWriter}; diff --git a/crates/oc-protocol/src/attestation.rs b/crates/oc-protocol/src/attestation.rs index 4ff9832..09e5884 100644 --- a/crates/oc-protocol/src/attestation.rs +++ b/crates/oc-protocol/src/attestation.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Device key attestation documents on the wire (B6b, `docs/protocol.md` //! §9.11.1). //! diff --git a/crates/oc-protocol/src/attribute_rule.rs b/crates/oc-protocol/src/attribute_rule.rs index 68415dc..d45ccb5 100644 --- a/crates/oc-protocol/src/attribute_rule.rs +++ b/crates/oc-protocol/src/attribute_rule.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! File rule based on holder attributes: bytes (F-27, B4b). //! //! # Why the rule lives here rather than only on the server diff --git a/crates/oc-protocol/src/control.rs b/crates/oc-protocol/src/control.rs index 85b39f6..5525b04 100644 --- a/crates/oc-protocol/src/control.rs +++ b/crates/oc-protocol/src/control.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Authority control documents (E2, B2; `docs/protocol.md` §9.16). //! //! Three documents, one codec: diff --git a/crates/oc-protocol/src/directory.rs b/crates/oc-protocol/src/directory.rs index 458d621..cb5864d 100644 --- a/crates/oc-protocol/src/directory.rs +++ b/crates/oc-protocol/src/directory.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Organization key directory: a record, its leaf and proof //! (`docs/protocol.md` §9.14, D4). //! diff --git a/crates/oc-protocol/src/lease.rs b/crates/oc-protocol/src/lease.rs index 4dff37f..aafc26e 100644 --- a/crates/oc-protocol/src/lease.rs +++ b/crates/oc-protocol/src/lease.rs @@ -1,32 +1,10 @@ -//! Lease: server-signed permission to open one file on one device. +// SPDX-License-Identifier: MPL-2.0 +//! Server-signed permission to open one file on one device. //! -//! # Why a separate document rather than a container field -//! -//! The container is signed by the author and does not change after release. A lease has its own -//! lifecycle: issuance, expiration, renewal, revocation, many times for the -//! same file. Placing it inside would require rewriting the container -//! on every renewal, meaning a new author signature for a file the author had not touched. -//! -//! A useful practical consequence: **the container format version does not change**. -//! Server fields in the header are already allocated and signed (`authority.urls`, -//! `authority.sealing_kid`, `authority.lease_verify_key`), while the lease has -//! its own version and lifecycle. -//! -//! # What makes a lease trustworthy -//! -//! A signature under `authority.lease_verify_key`, the very key that **the author -//! pinned in the header under their own signature**. A fake server cannot substitute its key: -//! replacing the verification key means replacing the author-signed header. -//! -//! This is the only point where the client trusts an external party; the trust chain -//! is short precisely because the author established it personally. -//! -//! # What is signed -//! -//! **Raw body bytes, not a reconstructed structure.** The same rule as for the -//! header (§5) and mutable area (I-5), for the same reason: reconstruction -//! before verification reproduces the entire family of canonicalization errors known from -//! JWS and XML-DSig. When parsed, verify what was on the wire. +//! A lease is separate from the author-signed container so it can expire, renew, +//! or be revoked without rewriting the header. Verification uses the server key +//! pinned by the author in `authority.lease_verify_key`. +//! Signatures cover raw body bytes and are checked before parsing. use oc_crypto::CryptoError; use oc_policy::{Attestation, LeaseFacts, Timestamp, TpmClock}; diff --git a/crates/oc-protocol/src/lib.rs b/crates/oc-protocol/src/lib.rs index 941feb8..d061e8e 100644 --- a/crates/oc-protocol/src/lib.rs +++ b/crates/oc-protocol/src/lib.rs @@ -1,95 +1,23 @@ -//! Product protocol documents: everything exchanged between client, server and -//! witness that does not reside inside the `.cc` container. +// SPDX-License-Identifier: MPL-2.0 +//! Client, server, and witness documents outside the `.cc` container. //! -//! # Why this is a separate crate rather than modules in `oc-format` +//! Container parsing lives in [`oc_format`]. This crate depends on it for TLV and +//! [`oc_format::FormatError`]; the dependency does not run in the other direction. +//! Protocol documents have their own versions and lifecycles. The crate performs +//! no I/O and reads neither clocks nor randomness from the environment. //! -//! The boundary follows the RATE OF CHANGE, not directory -//! neighbors. The container format freezes: after the first file leaves the system, -//! every header byte is promised forever, and changing it requires a new -//! format version together with a recorded decision (I-14). The product protocol -//! grows with the server: a new request kind, a new field in the file standing, -//! a new rejection reason appear exactly when the mechanism appears, with -//! no promise of permanence. +//! # Unknown fields //! -//! While both kinds lived in one crate, they were indistinguishable externally: the crate -//! promised to be frozen and opened contained eight and a half thousand -//! lines changing every week. Mixing them meant either freezing what -//! must change or exposing the freeze to things that change independently. Decision -//! R-2 (`docs/plan.md`, «D-core-freeze») separated them into crates; now -//! the difference is VISIBLE in a `use` statement, before any gate runs. +//! Unknown critical tags are rejected; unknown optional tags are skipped through +//! `oc_format::tlv::unknown_tag_action`. Known field lengths, ordering, duplicate +//! rejection, enum values, and message kinds remain strict. //! -//! # What lives where +//! Signed documents verify raw body bytes before parsing. Skipping an optional +//! field therefore cannot hide a change to the signed body. Two layouts differ: //! -//! [`oc_format`] contains the container: splitting the prologue, the header and its slots, -//! the mutable area, chunk frames, the footer, the policy codec, the TLV parser, -//! signature verification and the shared error type [`oc_format::FormatError`]. This crate holds -//! what arrives from or goes onto the wire: activation, author orders, -//! file standing on the server, access requests and decisions, leases, revocations, -//! device key attestation, the witness journal, the recipient directory, attribute -//! rules, control operations and replicas. -//! -//! There is exactly one dependency arrow: protocol depends on format. The reverse does not -//! and cannot exist: no container parser calls any protocol document; -//! this was verified in the code before the move, not merely promised. -//! -//! The lease ([`lease`]) lives here although it is cached alongside the container and -//! verified by the reader: it is a SEPARATE document with its own version -//! ([`lease::LEASE_VERSION`]) and lifecycle, issued by the server, -//! and changes at the server's pace rather than the format's pace. -//! -//! # Purity -//! -//! The crate passes through the same gate as `oc-format`, `oc-crypto`, `oc-policy` and -//! `oc-engine`: no I/O, clocks or random number generator. -//! The verification check is a `wasm32-unknown-unknown` build. This is not a -//! style concern: tests for time travel and revocation must be DATA, -//! not a replacement of the system clock. -//! -//! # Unknown tags -//! -//! **The same rule as the container (I-7), since 2026-09-21.** A tag ≤ -//! `oc_format::tlv::CRIT_TAG_MAX` is critical: an unknown such tag is rejected with -//! [`oc_format::FormatError::UnknownCriticalField`], the same error -//! variant as before the decision. Higher tags are optional: their values are skipped. The decision -//! is made by `oc_format::tlv::unknown_tag_action`, the same function used by -//! the container; the protocol has no separate one. -//! -//! Before that date, protocol parsers rejected EVERY unknown tag, justified -//! by clients and servers updating together. That argument ceased to hold -//! even before the first release: after release, servers and clients -//! update at different times: a solo operator runs their server, while recipients -//! use unmanaged machines. Under the old rule, the very first -//! new field after release would break every released peer. -//! Before release, the change is free: there are no external clients, and client and server ship -//! as one package. After release, it would require a wire version. -//! -//! # The optional range does NOT enable forgery -//! -//! Verification order has not changed: signatures and MACs are verified BEFORE body parsing -//! (I-5), and unknown tags are skipped AFTERWARD, within already authenticated -//! bytes. Signatures cover the RAW body bytes: `lease`, `revocation`, `order`, -//! `control` and `replica` sign exactly the slice they subsequently parse, -//! so a third party cannot append a tag in transit: the signature would no longer -//! verify. Unauthenticated documents are discussed below, each with its own rationale. -//! -//! There are two exceptions, both documented locally: -//! -//! * [`access::decode_decision`] remains STRICT. Its signature is verified not over -//! raw bytes but over a body REBUILT from the parsed structure -//! ([`access::decision_body`]); a skipped tag disappears from that reconstruction. -//! Thus an optional range there would enable exactly what this paragraph -//! promises to prevent: a third party could append a tag to a signed decision, -//! and its signature would verify. It would provide no extension capability either: a field added -//! to `decision_body` by a new build would still fail signature verification in an old build. -//! * [`witness`] does not follow this rule because it contains no TLV: heads, views and -//! cosigned heads have exact-length layouts. -//! -//! What has NOT changed: request and response kinds (`KIND_*`) are not tags; an unknown -//! kind is still rejected. Enumerations WITHIN values (lease state, operation -//! outcome, inheritance mode) are not tags; an unknown value of a known field -//! is still rejected. Strict tag ordering, duplicate rejection and exact lengths -//! of known fields (I-7, I-8) remain unchanged, and ordering is checked even for -//! skipped tags. +//! * [`access::decode_decision`] rejects every unknown tag because verification +//! reconstructs the body; skipping a field would remove it from the transcript. +//! * [`witness`] uses fixed-length layouts rather than TLV. /// Access request documents: the recipient requests, the author approves. pub mod access; diff --git a/crates/oc-protocol/src/order.rs b/crates/oc-protocol/src/order.rs index 693288a..5717481 100644 --- a/crates/oc-protocol/src/order.rs +++ b/crates/oc-protocol/src/order.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! An author's order to the server: what to do with a file. //! //! Before 2026-09-03, registration and revocation were operator `cca` commands on diff --git a/crates/oc-protocol/src/replica.rs b/crates/oc-protocol/src/replica.rs index 85fb531..1a12c88 100644 --- a/crates/oc-protocol/src/replica.rs +++ b/crates/oc-protocol/src/replica.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Server state replication: push and acknowledgment (E2, B4; //! `docs/protocol.md` §9.17). //! diff --git a/crates/oc-protocol/src/revocation.rs b/crates/oc-protocol/src/revocation.rs index 6b6e7b0..5af25fc 100644 --- a/crates/oc-protocol/src/revocation.rs +++ b/crates/oc-protocol/src/revocation.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Revocation: a server-signed "file revoked" document. //! //! # Why a document when revocation already works diff --git a/crates/oc-protocol/src/standing.rs b/crates/oc-protocol/src/standing.rs index c5b224f..a5d0e2a 100644 --- a/crates/oc-protocol/src/standing.rs +++ b/crates/oc-protocol/src/standing.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! File standing on the server: what the author sees when asking "what is set there?". //! //! # Why a separate document diff --git a/crates/oc-protocol/src/unknown.rs b/crates/oc-protocol/src/unknown.rs index 853ac51..fac6a44 100644 --- a/crates/oc-protocol/src/unknown.rs +++ b/crates/oc-protocol/src/unknown.rs @@ -1,31 +1,10 @@ -//! The crate's shared rule for unknown tags (I-7, decision of 2026-09-21). +// SPDX-License-Identifier: MPL-2.0 +//! Shared unknown-tag handling for protocol codecs (I-7). //! -//! Tag decisions are made by `oc_format::tlv::unknown_tag_action`, the same -//! function the container uses. There is deliberately no second implementation here: two places -//! computing the critical-range boundary would silently diverge, and do so -//! in exactly the direction of "skipped what should have been rejected". -//! -//! # Why a helper rather than a line in every parser -//! -//! The crate has fourteen document parsers; three `match` lines repeated verbatim -//! fourteen times are a known repository problem: one path -//! gets fixed while its neighbor is forgotten. Here there is nothing to forget: one rule in -//! one place. -//! -//! # Why the second helper REMEMBERS skipped entries -//! -//! Some documents check themselves: the parsed structure -//! is re-encoded and must reproduce the ORIGINAL bytes (`control::Binding`, -//! `ControlRequest`, `Receipt`, `Transfer`, `directory::Record`). This check -//! is deliberate: it establishes canonicality. A body our encoder does not -//! reproduce lacks a unique representation, yet its exact bytes determine -//! the signature, countersignature and directory journal leaf. -//! -//! A skipped optional tag breaks this comparison: re-encoding loses it. -//! Thus the comparison uses the body WITHOUT skipped entries rather than the entire body: -//! canonicality of KNOWN fields remains guarded, while unknown optional fields -//! bypass that check. Entire entries (tag, length, value) are removed using the same -//! technique as I-3. +//! `oc_format::tlv::unknown_tag_action` decides whether to reject or skip a field. +//! Codecs that check canonical re-encoding must omit skipped entries from that +//! comparison, since their encoder cannot reproduce unknown fields. Signatures +//! still authenticate the original body, including those entries. use core::ops::Range; use oc_format::FormatError; diff --git a/crates/oc-protocol/src/witness.rs b/crates/oc-protocol/src/witness.rs index 84735a6..a77af6a 100644 --- a/crates/oc-protocol/src/witness.rs +++ b/crates/oc-protocol/src/witness.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Witnessing a server journal head (`docs/protocol.md` §9.13, D3). //! //! # What this contains and why diff --git a/crates/oc-protocol/tests/action_vectors.rs b/crates/oc-protocol/tests/action_vectors.rs index 9d399c7..4bd60eb 100644 --- a/crates/oc-protocol/tests/action_vectors.rs +++ b/crates/oc-protocol/tests/action_vectors.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Индексирование, срезы и чтение файла разрешены ЗДЕСЬ и только здесь, тем же // порядком, что в `agent_vectors.rs`: вектор лежит вне кода намеренно, чтобы // вторая реализация могла свериться, не читая наш Rust. Запрет часов, файлов и diff --git a/crates/oc-protocol/tests/agent_vectors.rs b/crates/oc-protocol/tests/agent_vectors.rs index 9467b73..a78317d 100644 --- a/crates/oc-protocol/tests/agent_vectors.rs +++ b/crates/oc-protocol/tests/agent_vectors.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 // Индексирование, срезы и чтение файла разрешены ЗДЕСЬ и только здесь, тем же // порядком, что в `lease_signature.rs`: вектор лежит вне кода намеренно, чтобы // вторая реализация могла свериться, не читая наш Rust. Запрет часов, файлов и diff --git a/crates/oc-protocol/tests/control_vectors.rs b/crates/oc-protocol/tests/control_vectors.rs index 7e953b4..49af321 100644 --- a/crates/oc-protocol/tests/control_vectors.rs +++ b/crates/oc-protocol/tests/control_vectors.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Frozen control and replica document vectors (E2, B2/B4/B7). //! //! These documents arrive ENTIRELY FROM THE WIRE and must not change silently: diff --git a/crates/oc-protocol/tests/kat_operation.rs b/crates/oc-protocol/tests/kat_operation.rs index c135442..c793c4e 100644 --- a/crates/oc-protocol/tests/kat_operation.rs +++ b/crates/oc-protocol/tests/kat_operation.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Operation identity bytes on the wire versus the frozen vector //! `tests/kat/operation_id.kat` (`docs/protocol.md` §9.10). //! diff --git a/crates/oc-protocol/tests/lease_attestation.rs b/crates/oc-protocol/tests/lease_attestation.rs index 360889b..66500f8 100644 --- a/crates/oc-protocol/tests/lease_attestation.rs +++ b/crates/oc-protocol/tests/lease_attestation.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Lease version 3: device key attestation flag under the server signature (B6b). //! //! The flag raises the evaluator's binding level to `HardwareAttested`, so diff --git a/crates/oc-protocol/tests/lease_server_policy.rs b/crates/oc-protocol/tests/lease_server_policy.rs index 2cdc1e8..19ed603 100644 --- a/crates/oc-protocol/tests/lease_server_policy.rs +++ b/crates/oc-protocol/tests/lease_server_policy.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Lease version 2: the server sends its OWN profile to the recipient under its signature. //! //! Checks three properties motivating the field, not merely "the field survived": diff --git a/crates/oc-protocol/tests/lease_signature.rs b/crates/oc-protocol/tests/lease_signature.rs index 44a3e92..07d07a0 100644 --- a/crates/oc-protocol/tests/lease_signature.rs +++ b/crates/oc-protocol/tests/lease_signature.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Lease signature: round trip from issuance to verification, and every way to reject it. //! //! A separate file rather than a module: these tests need a real signing key, diff --git a/crates/oc-protocol/tests/session_mac_table.rs b/crates/oc-protocol/tests/session_mac_table.rs index 79703cb..623b16e 100644 --- a/crates/oc-protocol/tests/session_mac_table.rs +++ b/crates/oc-protocol/tests/session_mac_table.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Session MAC: authenticated-kind table and trailer separation, in the owning crate. //! //! Why a separate file when six tests in `cc-authority/tests/session_mac.rs` diff --git a/crates/oc-protocol/tests/unknown_tags.rs b/crates/oc-protocol/tests/unknown_tags.rs index f51b7c4..bbf0979 100644 --- a/crates/oc-protocol/tests/unknown_tags.rs +++ b/crates/oc-protocol/tests/unknown_tags.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Tag-range rule for PROTOCOL DOCUMENTS (I-7, decision of 2026-09-21). //! //! # What is tested here diff --git a/crates/opencrate/Cargo.toml b/crates/opencrate/Cargo.toml index cbbb433..0ea5400 100644 --- a/crates/opencrate/Cargo.toml +++ b/crates/opencrate/Cargo.toml @@ -4,22 +4,22 @@ description = "One-dependency entry point for the Open Crate Rust core" repository = "https://github.com/AlexiAxAxA/OpenCrate" homepage = "https://closecrate.com/" readme = "README.md" -version = "0.0.2" +version = "0.0.3" edition.workspace = true rust-version.workspace = true -license-file = "../../LICENSE-OPENCRATE" +license = "MPL-2.0" publish.workspace = true [features] app-data = ["dep:opencrate-sdk"] [dependencies] -oc-policy = { version = "=0.0.2", path = "../oc-policy" } -oc-crypto = { version = "=0.0.2", path = "../oc-crypto" } -oc-format = { version = "=0.0.2", path = "../oc-format" } -oc-protocol = { version = "=0.0.2", path = "../oc-protocol" } -oc-engine = { version = "=0.0.2", path = "../oc-engine" } -opencrate-sdk = { version = "=0.0.2", optional = true } +oc-policy = { version = "=0.0.3", path = "../oc-policy" } +oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } +oc-format = { version = "=0.0.3", path = "../oc-format" } +oc-protocol = { version = "=0.0.3", path = "../oc-protocol" } +oc-engine = { version = "=0.0.3", path = "../oc-engine" } +opencrate-sdk = { version = "=0.0.3", optional = true } [[example]] name = "seal-file" diff --git a/crates/opencrate/LICENSE b/crates/opencrate/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/crates/opencrate/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crates/opencrate/README.md b/crates/opencrate/README.md index e29d9d6..28893f7 100644 --- a/crates/opencrate/README.md +++ b/crates/opencrate/README.md @@ -1,31 +1,94 @@ -# Open Crate Rust core +

+ OpenCrate +

-`opencrate` is the one-dependency entry point for the Open Crate core. -It re-exports the five independent libraries under `crypto`, `engine`, -`format`, `policy`, and `protocol`. You may depend on an individual -`oc-*` package instead when you need only that part. +# OpenCrate + +Rust libraries for encrypted document containers and sealed application bytes. +`opencrate` brings the five core libraries into one dependency: +`crypto`, `format`, `protocol`, `policy`, and `engine`. + +[![Core CI](https://github.com/AlexiAxAxA/OpenCrate/actions/workflows/ci.yml/badge.svg)](https://github.com/AlexiAxAxA/OpenCrate/actions/workflows/ci.yml) +[![MPL-2.0](https://img.shields.io/badge/license-MPL--2.0-527fba)](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE) + +## Encrypt application bytes + +Enable `app-data` for the small API from the separate +[OpenCrateSDK](https://github.com/AlexiAxAxA/OpenCrateSDK). It handles JSON, +messages, or the bytes of any file up to 16 MiB, for one X25519 recipient. ```toml [dependencies] -opencrate = "=0.0.2" +opencrate = { version = "=0.0.3", features = ["app-data"] } ``` -For the bytes of a file of any extension, JSON, or messages, enable the -optional native-host SDK bridge: +```rust +use opencrate::app_data::{generate_recipient, open_bytes, recipient_public, seal_bytes}; + +fn main() -> Result<(), Box> { + let secret = generate_recipient()?; + let public = recipient_public(&secret); + let purpose = "com.example.invoice.v1"; + let context = b"tenant-7:invoice-42"; + + let envelope = seal_bytes(&public, purpose, context, br#"{"total":42}"#)?; + let plaintext = open_bytes(&secret, purpose, context, &envelope)?; + assert_eq!(&*plaintext, br#"{"total":42}"#); + Ok(()) +} +``` + +Keep the recipient key, authenticate its public key before sending, and save +the envelope. Opening requires the same `purpose` and `context`; neither is +stored in the envelope. The returned plaintext buffer wipes itself on drop, +but your application owns any copies it makes. + +The SDK's `OCSB1` envelope has no author signature, access policy, lease, or +revocation. It is a preview without an independent security audit or a stable +compatibility promise. Read the +[integration guide](https://github.com/AlexiAxAxA/OpenCrateSDK/blob/main/docs/usage-guide.md) +before choosing it for data you need to keep. + +## Use the document core + +For `.cc` containers or individual building blocks, use the default facade: ```toml [dependencies] -opencrate = { version = "=0.0.2", features = ["app-data"] } +opencrate = "=0.0.3" ``` -`opencrate::app_data` re-exports [`opencrate-sdk`](https://crates.io/crates/opencrate-sdk). -It seals at most 16 MiB for one recipient into an `OCSB1` envelope. The app -protects the secret key, authenticates the public key, and stores the envelope. -`OCSB1` is not a `.cc` container and provides no lease, policy enforcement or -revocation. The default core stays independent of OS randomness and keeps its -WASM build; `app-data` requires Rust 1.96 and a host OS random source. - -This is a pre-release library core, not the Close Crate viewer or server. -The [Open Crate Community License](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE-OPENCRATE) applies. -See the [repository](https://github.com/AlexiAxAxA/OpenCrate) for the -format specification, usage examples, and security policy. +| Module | Responsibility | +| --- | --- | +| `crypto` | AEAD, key agreement, signatures, key derivation, and integrity trees | +| `format` | Container layout, TLV parsing, and header verification | +| `protocol` | Client/server documents and signed leases | +| `policy` | Access decisions from supplied rules and context | +| `engine` | Packing keys, recipient slots, and headers | + +The default core performs no I/O and obtains neither time nor randomness from +the environment. It builds for `wasm32-unknown-unknown` with Rust 1.90+. +`app-data` requires Rust 1.96+ and an OS random source. Your application supplies +storage, key management, trusted time, transport, and enforcement of decisions. +These libraries do not install a viewer or run an authority server. + +## Read and run the code + +- [Getting started](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/getting-started.md) +- [Container format](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/format.md) +- [Runnable examples](https://github.com/AlexiAxAxA/OpenCrate/blob/main/examples/README.md) +- [Security policy](https://github.com/AlexiAxAxA/OpenCrate/blob/main/SECURITY.md) + +The repository examples verify a frozen container header and evaluate an access +policy. Clone the repository to run them; the first Cargo build may take time. + +![Example output](https://raw.githubusercontent.com/AlexiAxAxA/OpenCrate/main/docs/assets/quick-start.gif) + +## License + +Version 0.0.3 is licensed under +[MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). +Commercial use is allowed. Distributed modifications to covered files remain +under MPL-2.0; separate application files can use other terms. +Older registry archives keep their original Community License. Dependencies +retain their own terms. diff --git a/crates/opencrate/examples/seal-file.rs b/crates/opencrate/examples/seal-file.rs index 9a70d85..cc0f185 100644 --- a/crates/opencrate/examples/seal-file.rs +++ b/crates/opencrate/examples/seal-file.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! Read any small file as bytes and round-trip it through the optional SDK. //! This demonstration keeps a temporary key in memory and writes no envelope. // The example is the host boundary: unlike the core libraries it accepts a path diff --git a/crates/opencrate/src/lib.rs b/crates/opencrate/src/lib.rs index dad856b..a885098 100644 --- a/crates/opencrate/src/lib.rs +++ b/crates/opencrate/src/lib.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MPL-2.0 //! One-dependency entry point for the five Open Crate core libraries. //! The modules re-export the same APIs as the individual `oc-*` crates. //! Enable `app-data` to access the separate, host-side SDK for small arbitrary diff --git a/deny.toml b/deny.toml index 1da5294..3f5c89c 100644 --- a/deny.toml +++ b/deny.toml @@ -2,7 +2,7 @@ targets = ["x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [licenses] version = 2 -allow = ["MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "Unicode-3.0", "CC0-1.0"] +allow = ["MIT", "Apache-2.0", "MPL-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "Unicode-3.0", "CC0-1.0"] confidence-threshold = 0.9 [bans] multiple-versions = "deny" @@ -12,68 +12,3 @@ yanked = "deny" [sources] unknown-registry = "deny" unknown-git = "deny" - -# First-party custom license, tied to its reviewed text hash. - -[[licenses.exceptions]] -crate = "oc-crypto" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "oc-crypto" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "../../LICENSE-OPENCRATE", hash = 0xd44aae0f }] - -[[licenses.exceptions]] -crate = "oc-format" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "oc-format" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "../../LICENSE-OPENCRATE", hash = 0xd44aae0f }] - -[[licenses.exceptions]] -crate = "oc-protocol" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "oc-protocol" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "../../LICENSE-OPENCRATE", hash = 0xd44aae0f }] - -[[licenses.exceptions]] -crate = "oc-policy" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "oc-policy" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "../../LICENSE-OPENCRATE", hash = 0xd44aae0f }] - -[[licenses.exceptions]] -crate = "oc-engine" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "oc-engine" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "../../LICENSE-OPENCRATE", hash = 0xd44aae0f }] - -[[licenses.exceptions]] -crate = "opencrate" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "opencrate" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "../../LICENSE-OPENCRATE", hash = 0xd44aae0f }] - -[[licenses.exceptions]] -crate = "opencrate-sdk" -allow = ["LicenseRef-OpenCrate-Community-1.0"] - -[[licenses.clarify]] -crate = "opencrate-sdk" -expression = "LicenseRef-OpenCrate-Community-1.0" -license-files = [{ path = "LICENSE-OPENCRATE", hash = 0xd44aae0f }] diff --git a/docs/getting-started.md b/docs/getting-started.md index 9fb78aa..fccc72c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -102,11 +102,11 @@ reproducible builds. ## Choose the libraries for your integration The five libraries and the `opencrate` facade are published to crates.io at -`0.0.2`. To use the full core, add one dependency: +`0.0.3`. To use the full core, add one dependency: ```toml [dependencies] -opencrate = "=0.0.2" +opencrate = "=0.0.3" ``` The facade re-exports the libraries as `opencrate::{crypto, engine, format, @@ -115,11 +115,11 @@ individually instead: ```toml [dependencies] -oc-format = "=0.0.2" -oc-crypto = "=0.0.2" -oc-protocol = "=0.0.2" -oc-policy = "=0.0.2" -oc-engine = "=0.0.2" +oc-format = "=0.0.3" +oc-crypto = "=0.0.3" +oc-protocol = "=0.0.3" +oc-policy = "=0.0.3" +oc-engine = "=0.0.3" ``` To seal arbitrary application bytes or a small file without a `.cc` container, @@ -130,7 +130,7 @@ extension up to 16 MiB; it does not save a key or envelope for later use. ```toml [dependencies] -opencrate = { version = "=0.0.2", features = ["app-data"] } +opencrate = { version = "=0.0.3", features = ["app-data"] } ``` For persistent application data, follow the diff --git a/docs/licensing.md b/docs/licensing.md index 5b2278f..f064330 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -1,70 +1,20 @@ # Licensing -Open Crate uses the [Open Crate Community License 1.0](../LICENSE). -It is source-available, not OSI-approved open source. The complete license is -authoritative; this page is an explanation. - -| Use | Terms | -| --- | --- | -| Personal, non-commercial projects and learning | Free | -| Non-commercial academic teaching and research | Free | -| Company with **both** less than $1M annual group revenue and fewer than 25 people | Free, including commercial projects | -| Company reaching either threshold | Commercial agreement required; an existing qualifying user gets a 90-day transition | -| Evaluation and non-production security auditing | Free regardless of company size | - -Thresholds apply to the entire corporate group, not one developer team. Revenue -means gross revenue from all activities over the preceding twelve months, not -profit or revenue from one project. Investment and loan proceeds are excluded. -Employees and individual contractors count toward headcount. An independent -commercial developer qualifies under the same business rules; working alone -does not create an unlimited revenue exemption. - -No activation, compulsory telemetry, paid account or paperwork is needed to -exercise the free grant. There are no fees for past use that was permitted. -Redistribution must retain the license, and each recipient must independently -qualify. Creating a document does not license that document under these terms. - -## Commercial agreements - -Write to **god@closecrate.com** (the contact route on -[closecrate.com](https://closecrate.com/) named in section 5 of the license), -or open the **Licensing enquiry** issue template in this repository, which is -the licensing issue category named in the same section. E-mail is private; -issues are public, so in an issue state only the kind of use and a private -contact, and keep revenue figures, headcount, contracts and customer names out -of it. - -An enquiry proceeds in writing, in this order: - -1. The maintainer confirms which use is being licensed and which group is - covered, and moves the conversation to the private contact you gave. -2. You receive a written offer that states the price, the covered group, the - term, what is and is not included, the payment method and the payee. - Nothing is owed and nothing is granted before that offer is accepted. -3. Both sides sign a commercial license agreement; the free-grant conditions - of sections 3 and 4 of the Community License stop applying to the covered - group for the agreed term. -4. Payment is made as the agreement says. A payment made without a signed - agreement does not create a license. - -What the agreement covers is fixed by the license itself: fee, scope, term, -payment method, support and any warranty are agreed separately in writing. -Expect an annual company license for production use of the five core crates, -with integration help and support priced separately. No per-document or -per-user metering is planned for the core. Close Crate hosting and application -services are separate purchases. - -Until the first agreement is signed, there is no published price list. Do not -infer a price from any third-party licensing model mentioned in this -repository's history or documentation. - -## Third-party code - -This is the first public Open Crate release candidate. The core is offered under -the Community License, without an alternative MIT/Apache grant. Dependencies -retain their own licenses. Inspect their inventory with `cargo metadata --locked` -and `cargo deny check licenses`. - -This custom license has not received an independent legal review. A commercial -contract and working sales channel are still needed to turn inquiries into -payments. +OpenCrate 0.0.3 and later are licensed under [MPL-2.0](../LICENSE): the five +core libraries and the `opencrate` facade. The separate +[OpenCrateSDK](https://github.com/AlexiAxAxA/OpenCrateSDK) applies the same license +to SDK 0.0.3 and its experimental FFI and language wrappers. + +There are no revenue or headcount limits and no separate commercial agreement +for these versions. MPL-2.0 allows commercial use and applies copyleft at the +file level: when you distribute modified covered files, their source remains +available under MPL-2.0. A larger application can use other terms for its +separate files. See the [Mozilla FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). +Third-party dependencies retain their own licenses. + +The 0.0.1 and 0.0.2 registry archives retain their original Community License. +Publishing 0.0.3 does not rewrite those archives or their metadata. This +repository's current code is available under MPL-2.0. + +Close Crate's viewer, server, and other product code are outside this repository +and this grant. Opening the core does not license the separate product. diff --git a/docs/review-2026-09-26.md b/docs/review-2026-09-26.md new file mode 100644 index 0000000..219ad5f --- /dev/null +++ b/docs/review-2026-09-26.md @@ -0,0 +1,47 @@ +# 0.0.3 review + +This release applies MPL-2.0 to the core and facade. The SDK uses the same +license. Older registry archives retain their original terms. + +## Readability + +Shortened repeated explanations and change histories in library comments. +Kept wire formulas, verification order, compatibility boundaries and safety +contracts. Corrected outdated hybrid support and claim-derivation documentation. +Public policy digest comparison is no longer described as providing a general +constant-time guarantee for secret inputs. + +## Reproduced defects and fixes + +- `mlkem_p256::Keypair` derived `Debug` printed the 64-byte ML-KEM seed through + `Zeroizing`. Its manual implementation now redacts that field. +- `seal_p256` accepted a compressed recipient point, but the recipient derived + its KDF input from an uncompressed point. It now requires the 65-byte + uncompressed representation returned by `P256Agreement::public_key`. +- `seal` accepted X25519 coordinate aliases with identical DH results and + different KDF bytes. It now requires a coordinate below `2^255 - 19`. + Low-level X25519 agreement still accepts RFC 7748 encodings. + +All three regression tests failed before the fixes. Each includes a valid +round-trip or a check of the exposed seed; they pass after the fixes. Rejecting +previously unopenable recipient encodings changes input validation, not KDF, +nonce or authenticated wire formulas. Existing canonical recipients retain +their representation and RNG consumption order. + +## Checks and limits + +The review used separate public source checkouts. Native tests include existing +KAT and golden vectors, malformed signed headers, low-order keys and parser +mutation tests. Clippy, Rustdoc with warnings denied, a pure-core WASM build, +and dependency license/advisory/source checks passed locally. + +SDK checks cover 10,928 envelope bit mutations, 1,366 truncations, purpose and +context binding, low-order keys, the 16 MiB boundary, and simulated entropy +failure on each of the two RNG draws. Eight Python ABI tests use a real native +library and valid caller-owned buffers, including rejection without output +writes and allowed input/output overlap after copying. + +These are implementation checks, not an independent cryptographic audit. +An additional audit reproduced the defects above, but its extended fuzzing +continuation was interrupted by a platform filter. No completed instrumented +fuzz campaign or timing/side-channel proof is claimed here. From 92b0bfba9c695773ed73ea7332faa506db805a3e Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 10:55:11 +0300 Subject: [PATCH 02/10] Align license enquiry and documentation index with MPL release --- .github/ISSUE_TEMPLATE/config.yml | 4 +-- .github/ISSUE_TEMPLATE/licensing-enquiry.yml | 34 ++++++-------------- EXPORT-INVENTORY.json | 6 ++-- docs/index.md | 3 ++ 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ac2ab51..489f273 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,6 @@ contact_links: - name: Report a security vulnerability (private) url: https://github.com/AlexiAxAxA/OpenCrate/security/advisories/new about: Suspected vulnerabilities go through GitHub private vulnerability reporting, never through a public issue. See SECURITY.md. - - name: Website and commercial contact + - name: Project website url: https://closecrate.com/ - about: Commercial licensing and anything business-sensitive goes privately to god@closecrate.com. + about: Open Crate core and the separate Close Crate service. diff --git a/.github/ISSUE_TEMPLATE/licensing-enquiry.yml b/.github/ISSUE_TEMPLATE/licensing-enquiry.yml index 453d6fa..7e71704 100644 --- a/.github/ISSUE_TEMPLATE/licensing-enquiry.yml +++ b/.github/ISSUE_TEMPLATE/licensing-enquiry.yml @@ -1,42 +1,28 @@ name: Licensing enquiry -description: Ask about the Open Crate Community License or start a commercial licensing conversation. +description: Ask about MPL-2.0, redistribution or contributions. title: "[Licensing] " labels: ["licensing"] body: - type: markdown attributes: value: | - This is the licensing issue category named in section 5 of the - [Open Crate Community License 1.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE-OPENCRATE). - **Issues are public.** Do not post revenue figures, headcount, contracts, - customer names or anything else you consider business-sensitive. For a - private conversation write to god@closecrate.com instead, or leave a way - to reach you here and the maintainer will continue by e-mail. + Open Crate 0.0.3 uses + [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). + Commercial use does not require a separate agreement. + **Issues are public.** Keep private business details out of the issue. The [licensing guide](https://github.com/AlexiAxAxA/OpenCrate/blob/main/docs/licensing.md) answers the common - questions about the free thresholds. + questions about covered files and earlier releases. - type: dropdown id: kind attributes: label: What is this about? options: - - Question about whether my use is covered by the free grant - - Commercial license for a business at or above a threshold + - MPL-2.0 requirements for modified covered files + - Contributing code - Redistribution, OEM or embedding in a product for third parties - Other validations: required: true - - type: dropdown - id: eligibility - attributes: - label: Group size (self-assessed, no figures needed) - description: Both thresholds apply to the whole corporate group. Pick the bracket only. - options: - - Below both thresholds (under USD 1M revenue and under 25 people) - - At or above at least one threshold - - Not sure how to assess it - - Personal or academic non-commercial use - validations: - required: true - type: textarea id: use attributes: @@ -48,7 +34,7 @@ body: id: contact attributes: label: Private contact - description: An e-mail address or a note such as "reach me through the profile on this account". Required for commercial conversations. + description: Optional contact if the question needs private follow-up. validations: required: false - type: checkboxes @@ -58,5 +44,3 @@ body: options: - label: I have not included business-sensitive figures or documents in this public issue. required: true - - label: I understand that no price, discount or right is granted by opening this issue; terms are agreed in writing. - required: true diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 42c5bc3..eb299e4 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -135,8 +135,8 @@ ".gitattributes": "7bede99e1c6abeccdfd26bb94379dec310db319cd773e0a37fc3f2c98c5a32c2", ".github/FUNDING.yml": "72b0bf6e1bcc31165ea34935fdf148c671538c9e010233fb8f5fe670f4b7f909", ".github/ISSUE_TEMPLATE/bug-report.yml": "132a4fc8d7cfc655485dd70dbbd2042fcf49636818329e9df5ab14fbe1ca38f0", - ".github/ISSUE_TEMPLATE/config.yml": "e309cd574df26e9b0eea44083905d3785da1ed9761ce2d9a8f020fded3cb097a", - ".github/ISSUE_TEMPLATE/licensing-enquiry.yml": "c4686b43c2b39718ab9b86713461ad919a10415c6b6a1b5ecdc4183b449e2524", + ".github/ISSUE_TEMPLATE/config.yml": "37956cfb281e88e84ab2eb18c997edb62c105fdaedb6b7e977b5344a95408edb", + ".github/ISSUE_TEMPLATE/licensing-enquiry.yml": "f4d190349b5958103c40221b08df62e63cc1de94eeff34ffcb68d636a87d3f5d", ".github/workflows/ci.yml": "37f74c0187b1130489f60dc325d10f3d5c1f47e58df507e470cc5edb53542a33", ".gitignore": "ac05572173ab2ab7437511f230c35c4d767ebb9611496581f7bc9e54c57bc24a", "CONTRIBUTING.md": "ffa723ec1487243ea5fdde2ffdd36d00c59ea2c076de5baf08a1bf1b67c1a098", @@ -256,7 +256,7 @@ "docs/format.md": "b4e98a8bd6ccbe09e81f10637068a9d878f996387e6d36096d8dde21534ca1c5", "docs/getting-started.md": "9a5236295d05cd8c8b3e415babd6182c0bb5cfb4250dbd8105342b8868c12051", "docs/how-it-works.md": "5a15ef7455a46a9b550a85359fcf0b8e8dd85bcbc45e2ded612676b1cc8826e7", - "docs/index.md": "328e5e1d6ea45daae7ef1651459a83c759f4b698b5d81a571f5eb1f9a3c985fa", + "docs/index.md": "c715872e7db63f889d7462931060bec8a34f23c80dfc26118d929263a7a209ad", "docs/licensing.md": "ef99fc145a6d57dbd823c664c460affaa181340ea0e230527a2eb786c49c5d2a", "docs/protocol.md": "ec582526e61914ee6251ba2188d6680d3aa88aacaf2056d49ee6a463ce1e6990", "docs/review-2026-09-26.md": "1515c513e2e070ac39f97d1861b4b38322f131d0db0d08b72fd3d7b955471910", diff --git a/docs/index.md b/docs/index.md index 452055a..eb95452 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,3 +41,6 @@ key protection and enforcement. See [Translation notes](translation-notes.md) for preserved literal examples and how to read the dated specification history. + +See [the 0.0.3 review](review-2026-09-26.md) for reproduced defects, checks and +the scope of the readability changes. From bf33f7fbbbeda13cb9cce96e50883dbd1e8eab6d Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 10:58:23 +0300 Subject: [PATCH 03/10] Resolve published SDK in core workspace lockfile --- Cargo.lock | 8 ++------ EXPORT-INVENTORY.json | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 859687c..b38501d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,12 +12,6 @@ dependencies = [ "inout", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.8" @@ -503,6 +497,8 @@ dependencies = [ [[package]] name = "opencrate-sdk" version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30355e3c57f1f5cf6c35164228feea1e8d74507a91040bd137208fe94e69af0c" dependencies = [ "getrandom", "oc-crypto", diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index eb299e4..156f292 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -140,7 +140,7 @@ ".github/workflows/ci.yml": "37f74c0187b1130489f60dc325d10f3d5c1f47e58df507e470cc5edb53542a33", ".gitignore": "ac05572173ab2ab7437511f230c35c4d767ebb9611496581f7bc9e54c57bc24a", "CONTRIBUTING.md": "ffa723ec1487243ea5fdde2ffdd36d00c59ea2c076de5baf08a1bf1b67c1a098", - "Cargo.lock": "9c3d00291575bdb2daea76987ec12614a759b1f29002eb8e020f7c0f6a39ff18", + "Cargo.lock": "e7485c98fde71f0ff04bb3b29c2b0b3e3cb223a604f278de16144a8ca4aa1cd3", "Cargo.toml": "04d40e283aae2e3dc6facffcb4b0cf84f3d5caac3f483acde09f9e92bd954ff0", "LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "README.md": "7628c9b38b4910d357569535374f1f9520e4822dfb18f8f0d32b1537e3d2ae73", From 6836a7e91fb4d07e8eb0c0f33dca1cdd5720fae5 Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 10:59:12 +0300 Subject: [PATCH 04/10] Keep transitive arrayref required by the tested blake3 version --- Cargo.lock | 6 ++++++ EXPORT-INVENTORY.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b38501d..1510618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,12 @@ dependencies = [ "inout", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.8" diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 156f292..1718e07 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -140,7 +140,7 @@ ".github/workflows/ci.yml": "37f74c0187b1130489f60dc325d10f3d5c1f47e58df507e470cc5edb53542a33", ".gitignore": "ac05572173ab2ab7437511f230c35c4d767ebb9611496581f7bc9e54c57bc24a", "CONTRIBUTING.md": "ffa723ec1487243ea5fdde2ffdd36d00c59ea2c076de5baf08a1bf1b67c1a098", - "Cargo.lock": "e7485c98fde71f0ff04bb3b29c2b0b3e3cb223a604f278de16144a8ca4aa1cd3", + "Cargo.lock": "8254f0756886f2092a8ad7e71fb4f3f2c9c8c04a27f3439d592e9a03f40e1cd4", "Cargo.toml": "04d40e283aae2e3dc6facffcb4b0cf84f3d5caac3f483acde09f9e92bd954ff0", "LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "README.md": "7628c9b38b4910d357569535374f1f9520e4822dfb18f8f0d32b1537e3d2ae73", From 600cbc4b2b0cba36ad00a7af5d69736eb51e93ae Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 11:05:05 +0300 Subject: [PATCH 05/10] Validate DH provider and ephemeral encodings before agreement --- EXPORT-INVENTORY.json | 8 +- crates/oc-crypto/src/mlkem_p256.rs | 2 +- crates/oc-crypto/src/seal.rs | 59 +++-- crates/oc-crypto/tests/review_regressions.rs | 211 ++++++++++++++++-- crates/oc-crypto/tests/security_probe_seal.rs | 4 +- 5 files changed, 248 insertions(+), 36 deletions(-) diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 1718e07..c1db2e1 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -155,10 +155,10 @@ "crates/oc-crypto/src/lib.rs": "a79823c2e5a733606a8416848a803bcfd17c6fb9457b485fd885ab3bfc0c1bbf", "crates/oc-crypto/src/mac.rs": "75d6636bfbcbe9d7f9cd307ce5101d9d15e2023553038f42fe90a08efeb08edb", "crates/oc-crypto/src/merkle.rs": "63e117f274790f36943e7d0e69897241049ab102c14bcae958d1f80941fe93d2", - "crates/oc-crypto/src/mlkem_p256.rs": "cd83d36aba74857fac878c66f7a4d5c3624bf8b9a053541deb1d22bec258a823", + "crates/oc-crypto/src/mlkem_p256.rs": "f5a77672884ab20e2278d255915cba99e827e9bb5b5f20dca5e84b778513a1f6", "crates/oc-crypto/src/rsa.rs": "0a36fc1b91c322ac0a09fdb9c5b47db79b15b2c4a27129af83ab75a1d0ddc8f4", "crates/oc-crypto/src/rsa_test_signer.rs": "c34082bb4d6cd0df456a31eb4af37ca9721b8dc56162a9d6821cf3fcb6fd7f93", - "crates/oc-crypto/src/seal.rs": "8abd16b224ea7c96d45ad5df647c59cbff51f5ad8d93fea81302779de7ccf1eb", + "crates/oc-crypto/src/seal.rs": "f1405d1295693bfb172dda7d4b954b3f41f269c6c9e6cca516a93a1011b05fa8", "crates/oc-crypto/src/secret.rs": "55bff2d293719d34fcb2de7780a4dce7d81c0c0420b5c5da3b6b2dda83bcb9dd", "crates/oc-crypto/src/sign.rs": "e231b7a66a86039c1a4e9830bbb3470250a23f046dccc70f1a6c260fc1ef54db", "crates/oc-crypto/src/stream.rs": "6269ea48bdd1d9e494c4a8fa91bfcd73d6b84bd32d01223f686816972638a38f", @@ -168,9 +168,9 @@ "crates/oc-crypto/src/xwing.rs": "139884797ecbdbe12d22425812e775a182ccf7b1d7d12f488982137f3539c449", "crates/oc-crypto/tests/kat.rs": "0dc188ad01491e554049b078cfb3647b4b51de5e57715949d178bd10ff562b2a", "crates/oc-crypto/tests/measure_packing_path.rs": "085b9a74b7931fe970b30263851026c2cbbf8495898638a0861d4c3d07d99a18", - "crates/oc-crypto/tests/review_regressions.rs": "a19dbf804026c3007fba261670f8218c7ad19520bd1490ba240181ae096a5cbb", + "crates/oc-crypto/tests/review_regressions.rs": "84880dde33ab776afbe214bbb48db00898ce0c30aae5c0dc10feb8ce99e969d5", "crates/oc-crypto/tests/security_probe_keyschedule.rs": "1eb129e068b2138b2a0fd0465a62866f66e842a2bb4440ea0fd1c7b469e3d859", - "crates/oc-crypto/tests/security_probe_seal.rs": "2315397bfeaf3b2c81a17d934081ae72d966b84d5cf044c351f59e2b13aa196c", + "crates/oc-crypto/tests/security_probe_seal.rs": "31380327c7d0af0f9133c1ec264422ca5c5d123167fb87b2661b029e21e799bd", "crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs": "7d619a7845d35b33426cbbc70441c1f4622742073c4a0bafdde27c2f50ac520e", "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "44ee682d809c6211311e89c6193652adccb432b5e2cc2feabd5f94c1377c09ca", "crates/oc-crypto/tests/spec_labels.rs": "c5a15f4792ffa62c1f49d7b08aa8ef95c13d28b0a795575155e98f6af65022e4", diff --git a/crates/oc-crypto/src/mlkem_p256.rs b/crates/oc-crypto/src/mlkem_p256.rs index 4f21e6c..0591123 100644 --- a/crates/oc-crypto/src/mlkem_p256.rs +++ b/crates/oc-crypto/src/mlkem_p256.rs @@ -105,7 +105,7 @@ impl core::fmt::Debug for Keypair { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Keypair") .field("ml_kem_seed", &"") - .field("classical", &self.classical) + .field("classical", &"") .field("public_key", &self.public_key) .finish() } diff --git a/crates/oc-crypto/src/seal.rs b/crates/oc-crypto/src/seal.rs index e663b5a..0d2428f 100644 --- a/crates/oc-crypto/src/seal.rs +++ b/crates/oc-crypto/src/seal.rs @@ -268,9 +268,7 @@ pub fn seal_p256( rng: &mut R, ) -> Result { // K10 связывает байты; public_key() получателя возвращает несжатую точку. - if recipient_public.len() != 65 || recipient_public.first() != Some(&0x04) { - return Err(CryptoError::BadLength); - } + validate_dh_public(recipient_public, 65)?; use crate::agreement::KeyAgreement as _; let eph = crate::agreement::P256Agreement::generate(rng); @@ -332,6 +330,26 @@ fn canonical_x25519_public(public: &[u8; PUBLIC_KEY_LEN]) -> bool { public.iter().rev().cmp(modulus.iter().rev()).is_lt() } +// Проверяем представление до DH/KDF: K10 связывает именно байты ключа. +fn validate_dh_public(public: &[u8], expected_len: usize) -> Result<(), CryptoError> { + match expected_len { + PUBLIC_KEY_LEN => { + let public = public.try_into().map_err(|_| CryptoError::BadLength)?; + if !canonical_x25519_public(public) { + return Err(CryptoError::BadKey); + } + } + 65 => { + if public.len() != 65 || public.first() != Some(&0x04) { + return Err(CryptoError::BadLength); + } + p256::PublicKey::from_sec1_bytes(public).map_err(|_| CryptoError::BadKey)?; + } + _ => return Err(CryptoError::BadLength), + } + Ok(()) +} + /// Seal a slot using the X-Wing HYBRID, `kem_id = 4`. /// /// A separate function beside [`seal`] and [`seal_p256`] for the same reason @@ -479,6 +497,7 @@ pub fn open( // который знает версию контейнера; здесь ответ один — не наш вход. let enc: [u8; PUBLIC_KEY_LEN] = blob.enc.as_slice().try_into().map_err(|_| CryptoError::BadLength)?; + validate_dh_public(&enc, PUBLIC_KEY_LEN)?; let sk = StaticSecret::from(*recipient_secret.expose()); // Свой публичный ключ пересчитывается, а не принимается снаружи: он входит в @@ -491,15 +510,9 @@ pub fn open( open_core(&key, blob, aad) } -/// Open a blob using an AGREEMENT PARTY, whatever its implementation. -/// -/// This is the extension point for which the trait exists: the private key may reside -/// in a TPM without leaving it. A hardware implementation replaces -/// the software implementation here without changing one byte of key derivation, -/// which uses the shared secret rather than the key. -/// -/// Our public key comes from the party itself, not an argument: it enters -/// `ikm`, so accepting it externally would make key derivation externally controllable. +/// Open a DH seal through a software or hardware agreement provider. +/// Both public keys must use canonical X25519 or uncompressed P-256 encoding. +/// The provider supplies its own public key for K10; callers cannot override it. pub fn open_with( agreement: &dyn crate::agreement::KeyAgreement, blob: &SealedBlob, @@ -510,10 +523,11 @@ pub fn open_with( return Err(CryptoError::BadLength); } - // Согласование первым: оно же проверяет чужую точку. Длину `enc` проверяет - // реализация трейта — своей мерой для своего механизма. - let shared = agreement.agree(&blob.enc)?; + // Проверяем обе записи до согласования, включая аппаратного провайдера. let own_pk = agreement.public_key(); + validate_dh_public(&own_pk, own_pk.len())?; + validate_dh_public(&blob.enc, own_pk.len())?; + let shared = agreement.agree(&blob.enc)?; let key = derive_key(&shared, &blob.enc, &own_pk, info)?; open_core(&key, blob, aad) } @@ -817,6 +831,21 @@ mod kem_binding_tests { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::panic)] mod tests { + #[test] + fn canonical_x25519_coordinates_are_strictly_below_the_modulus() { + let mut modulus = [0xff; PUBLIC_KEY_LEN]; + *modulus.first_mut().unwrap() = 0xed; + *modulus.last_mut().unwrap() = 0x7f; + let mut below = modulus; + *below.first_mut().unwrap() = 0xec; + assert!(canonical_x25519_public(&below)); + assert!(canonical_x25519_public(&[0; PUBLIC_KEY_LEN])); + assert!(!canonical_x25519_public(&modulus)); + assert!(!canonical_x25519_public(&[0xff; PUBLIC_KEY_LEN])); + let mut high_bit = [0; PUBLIC_KEY_LEN]; + *high_bit.last_mut().unwrap() = 0x80; + assert!(!canonical_x25519_public(&high_bit)); + } /// HARDWARE HYBRID: SEALED IN SOFTWARE, OPENED WITH BOTH HALVES. /// /// The classical half is substituted BEHIND THE TRAIT; this probe uses diff --git a/crates/oc-crypto/tests/review_regressions.rs b/crates/oc-crypto/tests/review_regressions.rs index 89dd40b..84369c3 100644 --- a/crates/oc-crypto/tests/review_regressions.rs +++ b/crates/oc-crypto/tests/review_regressions.rs @@ -3,13 +3,21 @@ use oc_crypto::agreement::{KeyAgreement, P256Agreement}; use oc_crypto::{CryptoError, mlkem_p256, seal, secret::X25519Secret}; -// Repeatable RNG for format and rejection checks only. -struct Fixed; +// Повторяемый RNG только для проб; счётчик подтверждает отказ до расхода энтропии. +#[derive(Default)] +struct Fixed { + fills: usize, +} impl rand_core::TryRng for Fixed { type Error = core::convert::Infallible; - fn try_next_u32(&mut self) -> Result { Ok(0x42424242) } - fn try_next_u64(&mut self) -> Result { Ok(0x4242424242424242) } + fn try_next_u32(&mut self) -> Result { + Ok(0x42424242) + } + fn try_next_u64(&mut self) -> Result { + Ok(0x4242424242424242) + } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { + self.fills = self.fills.saturating_add(1); dest.fill(0x42); Ok(()) } @@ -18,43 +26,218 @@ impl rand_core::TryCryptoRng for Fixed {} #[test] fn hybrid_pair_debug_redacts_the_mlkem_seed() { - let pair = mlkem_p256::keypair_from_seed(&[0x31; 32]).unwrap(); + let mut pair = mlkem_p256::keypair_from_seed(&[0x31; 32]).unwrap(); let secret_rendering = format!("{:?}", pair.ml_kem_seed.as_slice()); - assert!(!format!("{pair:?}").contains(&secret_rendering), - "Keypair Debug exposes the ML-KEM seed"); - assert!(format!("{pair:?}").contains("")); + let shown = format!("{pair:?}"); + let pretty = format!("{pair:#?}"); + assert!(!shown.contains(&secret_rendering), "Keypair Debug exposes the ML-KEM seed"); + assert!(shown.contains("")); + pair.ml_kem_seed.fill(0x7b); + pair.classical = P256Agreement::from_be_bytes(&[0x12; 32]).unwrap(); + // Не печатаем значения в assert: старый Debug сам содержит секрет. + assert!(shown == format!("{pair:?}"), "secret fields affect Debug"); + assert!(pretty == format!("{pair:#?}"), "secret fields affect pretty Debug"); } #[test] fn compressed_p256_recipient_is_refused_before_sealing() { let recipient = P256Agreement::from_be_bytes(&[0x11; 32]).unwrap(); let public = recipient.public_key(); - let valid = seal::seal_p256(&public, b"info", b"aad", b"secret", &mut Fixed).unwrap(); + let valid = + seal::seal_p256(&public, b"info", b"aad", b"secret", &mut Fixed::default()).unwrap(); assert_eq!(&*seal::open_with(&recipient, &valid, b"info", b"aad").unwrap(), b"secret"); let mut compressed = vec![0x02 | (public[64] & 1)]; compressed.extend_from_slice(&public[1..33]); - // The point and DH result match; only the transcript encoding differs. + // Точка и DH совпадают; различаются только байты контекста KDF. assert!(recipient.agree(&compressed).unwrap().ct_eq(&recipient.agree(&public).unwrap())); - let result = seal::seal_p256(&compressed, b"info", b"aad", b"secret", &mut Fixed); + let mut rng = Fixed::default(); + let result = seal::seal_p256(&compressed, b"info", b"aad", b"secret", &mut rng); if let Ok(ref unusable) = result { assert!(seal::open_with(&recipient, unusable, b"info", b"aad").is_err()); } assert!(matches!(result, Err(CryptoError::BadLength))); + assert_eq!(rng.fills, 0); } #[test] fn noncanonical_x25519_recipient_is_refused_before_sealing() { let recipient = X25519Secret::from_bytes([0x11; 32]); let public = seal::x25519_public(&recipient); - let valid = seal::seal(&public, b"info", b"aad", b"secret", &mut Fixed).unwrap(); + let valid = seal::seal(&public, b"info", b"aad", b"secret", &mut Fixed::default()).unwrap(); assert_eq!(&*seal::open(&recipient, &valid, b"info", b"aad").unwrap(), b"secret"); + let agreement = oc_crypto::agreement::X25519Agreement::new(&recipient); + assert_eq!(&*seal::open_with(&agreement, &valid, b"info", b"aad").unwrap(), b"secret"); let mut alias = public; alias[31] |= 0x80; - let agreement = oc_crypto::agreement::X25519Agreement::new(&recipient); assert!(agreement.agree(&alias).unwrap().ct_eq(&agreement.agree(&public).unwrap())); - let result = seal::seal(&alias, b"info", b"aad", b"secret", &mut Fixed); + let mut rng = Fixed::default(); + let result = seal::seal(&alias, b"info", b"aad", b"secret", &mut rng); if let Ok(ref unusable) = result { assert!(seal::open(&recipient, unusable, b"info", b"aad").is_err()); } assert!(matches!(result, Err(CryptoError::BadKey))); + assert_eq!(rng.fills, 0); +} + +fn compressed_p256(public: &[u8]) -> Vec { + let mut compressed = vec![0x02 | (public[64] & 1)]; + compressed.extend_from_slice(&public[1..33]); + compressed +} + +#[test] +fn unsupported_p256_encodings_are_refused_before_entropy() { + let pair = P256Agreement::from_be_bytes(&[0x11; 32]).unwrap(); + let public = pair.public_key(); + let mut hybrid = public.clone(); + hybrid[0] = 0x06 | (public[64] & 1); + let mut wrong_prefix = public.clone(); + wrong_prefix[0] = 0x05; + let mut overlong = public.clone(); + overlong.push(0); + for key in [vec![], vec![0], public[..64].to_vec(), overlong, hybrid, wrong_prefix] { + let mut rng = Fixed::default(); + assert!(matches!( + seal::seal_p256(&key, b"info", b"aad", b"secret", &mut rng), + Err(CryptoError::BadLength) + )); + assert_eq!(rng.fills, 0); + } +} + +#[test] +fn off_curve_uncompressed_p256_is_refused_before_entropy() { + let mut invalid = [0; 65]; + invalid[0] = 0x04; + let mut rng = Fixed::default(); + assert!(matches!( + seal::seal_p256(&invalid, b"info", b"aad", b"secret", &mut rng), + Err(CryptoError::BadKey) + )); + assert_eq!(rng.fills, 0); +} + +#[test] +fn reduced_x25519_coordinate_alias_is_refused() { + let secret = X25519Secret::from_bytes([0x11; 32]); + let agreement = oc_crypto::agreement::X25519Agreement::new(&secret); + let mut basepoint = [0; 32]; + basepoint[0] = 9; + let mut alias = [0xff; 32]; + alias[0] = 0xf6; // p + 9, little-endian. + alias[31] = 0x7f; + assert!(agreement.agree(&basepoint).unwrap().ct_eq(&agreement.agree(&alias).unwrap())); + assert!(seal::seal(&basepoint, b"info", b"aad", b"secret", &mut Fixed::default()).is_ok()); + let mut rng = Fixed::default(); + assert!(matches!( + seal::seal(&alias, b"info", b"aad", b"secret", &mut rng), + Err(CryptoError::BadKey) + )); + assert_eq!(rng.fills, 0); +} + +#[test] +fn all_x25519_coordinates_from_modulus_to_high_bit_are_refused_before_entropy() { + // Все 19 записей p..2^255-1 должны отвергаться, даже если DH ненулевой. + for first in 0xed..=0xff { + let mut public = [0xff; 32]; + public[0] = first; + public[31] = 0x7f; + let mut rng = Fixed::default(); + assert!(matches!( + seal::seal(&public, b"info", b"aad", b"secret", &mut rng), + Err(CryptoError::BadKey) + )); + assert_eq!(rng.fills, 0); + } +} + +#[test] +fn x25519_opening_refuses_noncanonical_ephemeral_keys() { + let secret = X25519Secret::from_bytes([0x11; 32]); + let agreement = oc_crypto::agreement::X25519Agreement::new(&secret); + let valid = seal::seal( + &seal::x25519_public(&secret), + b"info", + b"aad", + b"secret", + &mut Fixed::default(), + ) + .unwrap(); + let mut high_bit = valid.enc.clone(); + high_bit[31] |= 0x80; + let mut reduced = vec![0xff; 32]; + reduced[0] = 0xf6; + reduced[31] = 0x7f; + for enc in [high_bit, reduced] { + let blob = seal::SealedBlob { enc, ..valid.clone() }; + assert!(matches!(seal::open(&secret, &blob, b"info", b"aad"), Err(CryptoError::BadKey))); + assert!(matches!( + seal::open_with(&agreement, &blob, b"info", b"aad"), + Err(CryptoError::BadKey) + )); + } +} + +#[test] +fn p256_opening_refuses_compressed_ephemeral_keys() { + let recipient = P256Agreement::from_be_bytes(&[0x11; 32]).unwrap(); + let mut blob = + seal::seal_p256(&recipient.public_key(), b"info", b"aad", b"secret", &mut Fixed::default()) + .unwrap(); + blob.enc = compressed_p256(&blob.enc); + assert!(matches!( + seal::open_with(&recipient, &blob, b"info", b"aad"), + Err(CryptoError::BadLength) + )); +} + +struct PublicOverride<'a> { + inner: &'a dyn KeyAgreement, + public: Vec, + calls: core::cell::Cell, +} + +impl KeyAgreement for PublicOverride<'_> { + fn public_key(&self) -> Vec { + self.public.clone() + } + fn agree(&self, peer: &[u8]) -> Result { + self.calls.set(self.calls.get().saturating_add(1)); + self.inner.agree(peer) + } +} + +#[test] +fn opening_refuses_noncanonical_provider_keys_before_agreement() { + let p256 = P256Agreement::from_be_bytes(&[0x11; 32]).unwrap(); + let blob = + seal::seal_p256(&p256.public_key(), b"info", b"aad", b"secret", &mut Fixed::default()) + .unwrap(); + let provider = PublicOverride { + inner: &p256, + public: compressed_p256(&p256.public_key()), + calls: core::cell::Cell::new(0), + }; + assert!(matches!( + seal::open_with(&provider, &blob, b"info", b"aad"), + Err(CryptoError::BadLength) + )); + assert_eq!(provider.calls.get(), 0); + + let secret = X25519Secret::from_bytes([0x11; 32]); + let x25519 = oc_crypto::agreement::X25519Agreement::new(&secret); + let mut public = x25519.public_key(); + let blob = seal::seal( + &seal::x25519_public(&secret), + b"info", + b"aad", + b"secret", + &mut Fixed::default(), + ) + .unwrap(); + public[31] |= 0x80; + let provider = PublicOverride { inner: &x25519, public, calls: core::cell::Cell::new(0) }; + assert!(matches!(seal::open_with(&provider, &blob, b"info", b"aad"), Err(CryptoError::BadKey))); + assert_eq!(provider.calls.get(), 0); } diff --git a/crates/oc-crypto/tests/security_probe_seal.rs b/crates/oc-crypto/tests/security_probe_seal.rs index 6f1ced0..4193c1e 100644 --- a/crates/oc-crypto/tests/security_probe_seal.rs +++ b/crates/oc-crypto/tests/security_probe_seal.rs @@ -218,7 +218,7 @@ fn no_info_can_make_two_slot_purposes_derive_the_same_key() { fn a_non_canonical_enc_that_yields_the_same_dh_still_fails_to_open() { // Старший бит `u` игнорируется реализацией X25519, поэтому `enc` и // `enc | 0x80` дают один и тот же общий секрет. Блоб не должен быть - // податлив: enc входит в ikm, и подмена обязана ломать тег. + // податлив: неканоническая форма отвергается до DH и вывода ключа. let mut rng = SeedRng::seeded(21); let sk = recipient(7); let mut blob = seal(&x25519_public(&sk), INFO, AAD, PT, &mut rng).unwrap(); @@ -227,7 +227,7 @@ fn a_non_canonical_enc_that_yields_the_same_dh_still_fails_to_open() { blob.enc[31] |= 0x80; assert_eq!( open(&sk, &blob, INFO, AAD).err(), - Some(CryptoError::Authentication), + Some(CryptoError::BadKey), "неканоническая перекодировка enc открыла блоб: конструкция податлива" ); } From bf2776e7de128fafbe9fe577bce0216b17acdd9c Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 11:42:12 +0300 Subject: [PATCH 06/10] Coordinate 0.0.4 release and document strict DH invariants --- .github/ISSUE_TEMPLATE/licensing-enquiry.yml | 2 +- Cargo.lock | 16 +++++------ Cargo.toml | 2 +- EXPORT-INVENTORY.json | 30 ++++++++++---------- README.md | 6 ++-- crates/oc-engine/Cargo.toml | 6 ++-- crates/oc-format/Cargo.toml | 6 ++-- crates/oc-protocol/Cargo.toml | 6 ++-- crates/opencrate/Cargo.toml | 14 ++++----- crates/opencrate/README.md | 8 +++--- docs/format.md | 11 +++++++ docs/getting-started.md | 16 +++++------ docs/index.md | 2 +- docs/licensing.md | 8 ++++-- docs/review-2026-09-26.md | 26 +++++++++++++---- 15 files changed, 93 insertions(+), 66 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/licensing-enquiry.yml b/.github/ISSUE_TEMPLATE/licensing-enquiry.yml index 7e71704..7cfb960 100644 --- a/.github/ISSUE_TEMPLATE/licensing-enquiry.yml +++ b/.github/ISSUE_TEMPLATE/licensing-enquiry.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - Open Crate 0.0.3 uses + Open Crate 0.0.4 uses [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). Commercial use does not require a separate agreement. **Issues are public.** Keep private business details out of the issue. diff --git a/Cargo.lock b/Cargo.lock index 1510618..91d854d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "oc-crypto" -version = "0.0.3" +version = "0.0.4" dependencies = [ "blake3", "chacha20poly1305", @@ -456,7 +456,7 @@ dependencies = [ [[package]] name = "oc-engine" -version = "0.0.3" +version = "0.0.4" dependencies = [ "oc-crypto", "oc-format", @@ -467,7 +467,7 @@ dependencies = [ [[package]] name = "oc-format" -version = "0.0.3" +version = "0.0.4" dependencies = [ "oc-crypto", "oc-policy", @@ -477,11 +477,11 @@ dependencies = [ [[package]] name = "oc-policy" -version = "0.0.3" +version = "0.0.4" [[package]] name = "oc-protocol" -version = "0.0.3" +version = "0.0.4" dependencies = [ "oc-crypto", "oc-format", @@ -490,7 +490,7 @@ dependencies = [ [[package]] name = "opencrate" -version = "0.0.3" +version = "0.0.4" dependencies = [ "oc-crypto", "oc-engine", @@ -502,9 +502,9 @@ dependencies = [ [[package]] name = "opencrate-sdk" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30355e3c57f1f5cf6c35164228feea1e8d74507a91040bd137208fe94e69af0c" +checksum = "ed0d2e1df1749daffc2f718ebf97a0c654580f0d3ef110d4d004d5ecde29738f" dependencies = [ "getrandom", "oc-crypto", diff --git a/Cargo.toml b/Cargo.toml index c0f18a3..5603752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = ["crates/oc-crypto", "crates/oc-format", "crates/oc-protocol", "crates oc-crypto = { path = "crates/oc-crypto" } [workspace.package] -version = "0.0.3" +version = "0.0.4" edition = "2024" rust-version = "1.90" license = "MPL-2.0" diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index c1db2e1..63c124e 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -136,14 +136,14 @@ ".github/FUNDING.yml": "72b0bf6e1bcc31165ea34935fdf148c671538c9e010233fb8f5fe670f4b7f909", ".github/ISSUE_TEMPLATE/bug-report.yml": "132a4fc8d7cfc655485dd70dbbd2042fcf49636818329e9df5ab14fbe1ca38f0", ".github/ISSUE_TEMPLATE/config.yml": "37956cfb281e88e84ab2eb18c997edb62c105fdaedb6b7e977b5344a95408edb", - ".github/ISSUE_TEMPLATE/licensing-enquiry.yml": "f4d190349b5958103c40221b08df62e63cc1de94eeff34ffcb68d636a87d3f5d", + ".github/ISSUE_TEMPLATE/licensing-enquiry.yml": "9b3e01229f8df8c3b8dd769362940eb80a7939f814386c6755abb56404b7a3dd", ".github/workflows/ci.yml": "37f74c0187b1130489f60dc325d10f3d5c1f47e58df507e470cc5edb53542a33", ".gitignore": "ac05572173ab2ab7437511f230c35c4d767ebb9611496581f7bc9e54c57bc24a", "CONTRIBUTING.md": "ffa723ec1487243ea5fdde2ffdd36d00c59ea2c076de5baf08a1bf1b67c1a098", - "Cargo.lock": "8254f0756886f2092a8ad7e71fb4f3f2c9c8c04a27f3439d592e9a03f40e1cd4", - "Cargo.toml": "04d40e283aae2e3dc6facffcb4b0cf84f3d5caac3f483acde09f9e92bd954ff0", + "Cargo.lock": "3697060ceeff838b0d4fb770263278b669382bfe80b6b0b677f40d4f8bb76392", + "Cargo.toml": "b936807a4ea732d45403727986d27e23ab83823b488f6fe81d03044db5b201c6", "LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", - "README.md": "7628c9b38b4910d357569535374f1f9520e4822dfb18f8f0d32b1537e3d2ae73", + "README.md": "7310d9533445b67186bdad672e88109fab01c91619940f27cb3a0388bd5bce2d", "SECURITY.md": "839c254e62ea1cfa4628520ca6d1e145c5fc1f6b109ab4f2f9c711b3db26d930", "clippy.toml": "447c1c2ae9dc0dfef9bae35ca82cb04f08c09bf7943d3835823138b546c03621", "crates/oc-crypto/Cargo.toml": "63a4268cf85daca35746b15080aee8b51fd703e59ae3a08ed76cf479f7970804", @@ -175,7 +175,7 @@ "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "44ee682d809c6211311e89c6193652adccb432b5e2cc2feabd5f94c1377c09ca", "crates/oc-crypto/tests/spec_labels.rs": "c5a15f4792ffa62c1f49d7b08aa8ef95c13d28b0a795575155e98f6af65022e4", "crates/oc-crypto/tests/zeroize_on_drop.rs": "6049affea1696a0235a3f47685d7126c9e022ca61ca3000d4abcffa6e1c4dc9e", - "crates/oc-engine/Cargo.toml": "00a95e126327629404f697eb2131735159eb09d2a6994849e63fa7faae17f336", + "crates/oc-engine/Cargo.toml": "f08036694fe9d89898403e2681e789a4f970d89ab5d8708560f675669a0b25c4", "crates/oc-engine/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-engine/README.md": "1eece36c00aecf16e31c12ce8c7cdbaa1f7b8299b435879e73f6cfec86bc42b2", "crates/oc-engine/src/edit.rs": "ea729a45d50b190badd275549eef70f2f84a94be1bcf5acd0bc5e5a7192b5316", @@ -184,7 +184,7 @@ "crates/oc-engine/tests/edit_writer.rs": "8d8d77313772bf184677c79757322073ab29bcca203154050dd0a856dd055c1c", "crates/oc-engine/tests/hardware_hybrid_golden.rs": "cec004a062ab838a2acfbe634f17c54cda1053a14337b57c51d2711e4b2033b7", "crates/oc-engine/tests/p256_author_golden.rs": "6f20067edbc1259cd981e10347c4e1df4f80af6935456b01f42a7e246451f947", - "crates/oc-format/Cargo.toml": "d79856d1eeac0cb15f7064c2b9313e9b8871fd26daa9d449464e5c28dfcaa4eb", + "crates/oc-format/Cargo.toml": "8a06ce853ae2063af066d6ee0416700efb820e8c4c1907011c0eb50bcfa9f09e", "crates/oc-format/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-format/README.md": "54e78a93669649329fa9b68022c84bd69df7751fca007941b60f2ea0e1b16b2c", "crates/oc-format/examples/verify-header.rs": "5c9ca98e85e1aade70875f9a935ae34618f94737e1b14336dba756d8fd62d59c", @@ -212,7 +212,7 @@ "crates/oc-policy/src/lib.rs": "743eaf0550615071ef98ffab4b42c271981332fe19ad2290e20f1e21dea0ade4", "crates/oc-policy/tests/security_probe_skeptic.rs": "c60f720854250afc356466c6a3d5227ee80e50a41d28d0e3c53a580082d6f769", "crates/oc-policy/tests/server_policy_composition.rs": "c7069b4151bb0660e03050b249fe0f9dcafab44ed3bb3db70ad05977308bb87d", - "crates/oc-protocol/Cargo.toml": "a051746d07d110c0329846bb39b92b37be5ea7a207ae945fd0f1549882e50f07", + "crates/oc-protocol/Cargo.toml": "6fa1277b4b24a9d321faafeac346810d858824217c10583466f1a607d181c047", "crates/oc-protocol/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-protocol/README.md": "42ca1744fee5ac93b7bbdfcafcd016cf19c427ca208ae2491e3351d65a107c42", "crates/oc-protocol/src/access.rs": "5a846d5285cd6beb48b5bb753a5ae836912580c8d91dc35f7a5f8e03324c37eb", @@ -240,9 +240,9 @@ "crates/oc-protocol/tests/lease_signature.rs": "f8f9de0fd92d546f34cdbf3dd036e664e682d4f40aa7f6f907e680a6d81ea37a", "crates/oc-protocol/tests/session_mac_table.rs": "67d28231e53eb1bcde0e75b3703a62715aab24f9870aa0bcfe7eaad5a7103296", "crates/oc-protocol/tests/unknown_tags.rs": "ddb5efc2c168b863c8b176bb327098bc657c9324ffb92700d98df1854c85d85f", - "crates/opencrate/Cargo.toml": "0cc071d2772fc611d9ced97e6331222bdb5d3f55e678275838df953444753bb7", + "crates/opencrate/Cargo.toml": "033d6fd548324ebff26a78d8890762cec4a4e6cf9c9c22fa6667db6893e66a2e", "crates/opencrate/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", - "crates/opencrate/README.md": "99049cb0966af57619ace7151dabd7f3ecfe97ddb292f28ca6f8dbb588fee8a2", + "crates/opencrate/README.md": "1979dd4d5cd02bebb4ad54f418084c14711f94d6572bf35aad5417b1e46c70fe", "crates/opencrate/examples/seal-file.rs": "802e2d887f54e60386e02c500d12667468b8acb5a89654d4cfc5aa7edaa22709", "crates/opencrate/src/lib.rs": "8a6effdca9c9709505ea59bca87879d432be8ed70808ef4d67c18dcb05612555", "deny.toml": "e06d8f3d9ac96158cc073b84076ca98a9efedb5e5e9e460c60715506cf5c6daa", @@ -253,13 +253,13 @@ "docs/assets/open-crate-banner.svg": "9e169d913482e4eb5c01094b20b6870c4b3c2931514dc0b8b0f37c489c170b9c", "docs/assets/open-crate-mark.svg": "f3039e6557a4a5cac7aff44ec4457119efc1fa543caf17320fafc15489ed0dc0", "docs/assets/quick-start.gif": "4f0232560ff9bd8e1e7a1c34c4c04b5aa94deeb134e22ea722307e0b3aa5bee1", - "docs/format.md": "b4e98a8bd6ccbe09e81f10637068a9d878f996387e6d36096d8dde21534ca1c5", - "docs/getting-started.md": "9a5236295d05cd8c8b3e415babd6182c0bb5cfb4250dbd8105342b8868c12051", + "docs/format.md": "e33752fd977afd6077b5bd8d2c804464fbcb291f0fa4430e6495b9142252712a", + "docs/getting-started.md": "6a5ed33429c4b51a690dd7ee253e44a1d63dbf4516eb1c59787493c09aa2716f", "docs/how-it-works.md": "5a15ef7455a46a9b550a85359fcf0b8e8dd85bcbc45e2ded612676b1cc8826e7", - "docs/index.md": "c715872e7db63f889d7462931060bec8a34f23c80dfc26118d929263a7a209ad", - "docs/licensing.md": "ef99fc145a6d57dbd823c664c460affaa181340ea0e230527a2eb786c49c5d2a", + "docs/index.md": "a6d5883cfd5f88cfc7b8a2c260442aae073fcc0888fdc934c4ea457076d30420", + "docs/licensing.md": "82d00091b4c173fe02e506fc67821dd6fea96324bf408e6f6a7f8d93243b3ea3", "docs/protocol.md": "ec582526e61914ee6251ba2188d6680d3aa88aacaf2056d49ee6a463ce1e6990", - "docs/review-2026-09-26.md": "1515c513e2e070ac39f97d1861b4b38322f131d0db0d08b72fd3d7b955471910", + "docs/review-2026-09-26.md": "6bb457d9249e494ce2bb5660ccb3830d38f1722fc0abc5c45970de3c0f836faa", "docs/translation-notes.md": "c290d69d83f42cd1f9d9311e32b8ad2b08391a94856e68a0b4993dfa1d103065", "examples/README.md": "7a95174a360ec6527fcb86cef5a78c011e4281010bde5ddfac8fc280f7194738", "rust-toolchain.toml": "41308dbe82a83679b7b2081be4d5ae7f9aa52f642bcc3eab104ac77501560e79", @@ -313,6 +313,6 @@ "registry_upload": false }, "reviewed_public_baseline": "7e607dfcc6e3cc948e9ee7de9c1da56a76d8242e", - "release_candidate": "0.0.3", + "release_candidate": "0.0.4", "license": "MPL-2.0" } diff --git a/README.md b/README.md index 42d44ba..c7c472f 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ decisions. It is the five-library core behind Close Crate. For a Rust project, add the `opencrate` facade to use all five libraries, or depend on the individual `oc-*` crates you need. The facade is version -`0.0.3`; the five core libraries are also `0.0.3` on crates.io: +`0.0.4`; the five core libraries are also `0.0.4` on crates.io: ```toml [dependencies] -opencrate = "=0.0.3" +opencrate = "=0.0.4" ``` Your application supplies storage, transport, keys, secure randomness and time. @@ -63,7 +63,7 @@ extension, or JSON and messages, for one recipient. It re-exports the separate ```toml [dependencies] -opencrate = { version = "=0.0.3", features = ["app-data"] } +opencrate = { version = "=0.0.4", features = ["app-data"] } ``` ```sh diff --git a/crates/oc-engine/Cargo.toml b/crates/oc-engine/Cargo.toml index d54590d..4e794d4 100644 --- a/crates/oc-engine/Cargo.toml +++ b/crates/oc-engine/Cargo.toml @@ -11,9 +11,9 @@ license = "MPL-2.0" publish.workspace = true [dependencies] -oc-format = { version = "=0.0.3", path = "../oc-format" } -oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } -oc-policy = { version = "=0.0.3", path = "../oc-policy" } +oc-format = { version = "=0.0.4", path = "../oc-format" } +oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } +oc-policy = { version = "=0.0.4", path = "../oc-policy" } # Те же версии, что у `oc-crypto` и `cc-cli`: новых крейтов в дереве ноль. rand_core = { version = "0.10.1", default-features = false } zeroize = { version = "1.9.0", features = ["derive"] } diff --git a/crates/oc-format/Cargo.toml b/crates/oc-format/Cargo.toml index 29eb40d..32c7f2f 100644 --- a/crates/oc-format/Cargo.toml +++ b/crates/oc-format/Cargo.toml @@ -12,15 +12,15 @@ publish.workspace = true [dependencies] sha2 = "0.11.0" -oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } -oc-policy = { version = "=0.0.3", path = "../oc-policy" } +oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } +oc-policy = { version = "=0.0.4", path = "../oc-policy" } zeroize = "1.9.0" # Программный подписант RSA-PSS — только пробам правки (`tests/edit.rs`); # продуктовая сборка признака не видит (resolver 2 не сливает признаки # dev-зависимостей в обычную сборку). [dev-dependencies] -oc-crypto = { version = "=0.0.3", path = "../oc-crypto", features = ["test-signer"] } +oc-crypto = { version = "=0.0.4", path = "../oc-crypto", features = ["test-signer"] } [lints] workspace = true diff --git a/crates/oc-protocol/Cargo.toml b/crates/oc-protocol/Cargo.toml index 6084780..4f90dea 100644 --- a/crates/oc-protocol/Cargo.toml +++ b/crates/oc-protocol/Cargo.toml @@ -15,9 +15,9 @@ publish.workspace = true # кодек политики и таблица версий живут в `oc-format`. Обратной стрелки нет и # быть не может — ни один разборщик контейнера не зовёт ни один документ # протокола, и это проверено до переноса. -oc-format = { version = "=0.0.3", path = "../oc-format" } -oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } -oc-policy = { version = "=0.0.3", path = "../oc-policy" } +oc-format = { version = "=0.0.4", path = "../oc-format" } +oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } +oc-policy = { version = "=0.0.4", path = "../oc-policy" } [lints] workspace = true diff --git a/crates/opencrate/Cargo.toml b/crates/opencrate/Cargo.toml index 0ea5400..ea49f61 100644 --- a/crates/opencrate/Cargo.toml +++ b/crates/opencrate/Cargo.toml @@ -4,7 +4,7 @@ description = "One-dependency entry point for the Open Crate Rust core" repository = "https://github.com/AlexiAxAxA/OpenCrate" homepage = "https://closecrate.com/" readme = "README.md" -version = "0.0.3" +version = "0.0.4" edition.workspace = true rust-version.workspace = true license = "MPL-2.0" @@ -14,12 +14,12 @@ publish.workspace = true app-data = ["dep:opencrate-sdk"] [dependencies] -oc-policy = { version = "=0.0.3", path = "../oc-policy" } -oc-crypto = { version = "=0.0.3", path = "../oc-crypto" } -oc-format = { version = "=0.0.3", path = "../oc-format" } -oc-protocol = { version = "=0.0.3", path = "../oc-protocol" } -oc-engine = { version = "=0.0.3", path = "../oc-engine" } -opencrate-sdk = { version = "=0.0.3", optional = true } +oc-policy = { version = "=0.0.4", path = "../oc-policy" } +oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } +oc-format = { version = "=0.0.4", path = "../oc-format" } +oc-protocol = { version = "=0.0.4", path = "../oc-protocol" } +oc-engine = { version = "=0.0.4", path = "../oc-engine" } +opencrate-sdk = { version = "=0.0.4", optional = true } [[example]] name = "seal-file" diff --git a/crates/opencrate/README.md b/crates/opencrate/README.md index 28893f7..50477c8 100644 --- a/crates/opencrate/README.md +++ b/crates/opencrate/README.md @@ -19,7 +19,7 @@ messages, or the bytes of any file up to 16 MiB, for one X25519 recipient. ```toml [dependencies] -opencrate = { version = "=0.0.3", features = ["app-data"] } +opencrate = { version = "=0.0.4", features = ["app-data"] } ``` ```rust @@ -55,7 +55,7 @@ For `.cc` containers or individual building blocks, use the default facade: ```toml [dependencies] -opencrate = "=0.0.3" +opencrate = "=0.0.4" ``` | Module | Responsibility | @@ -86,9 +86,9 @@ policy. Clone the repository to run them; the first Cargo build may take time. ## License -Version 0.0.3 is licensed under +Version 0.0.4 is licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). Commercial use is allowed. Distributed modifications to covered files remain under MPL-2.0; separate application files can use other terms. -Older registry archives keep their original Community License. Dependencies +Registry archives 0.0.1 and 0.0.2 keep their original Community License. Dependencies retain their own terms. diff --git a/docs/format.md b/docs/format.md index 732222f..3bef515 100644 --- a/docs/format.md +++ b/docs/format.md @@ -1260,6 +1260,17 @@ The commitment lets the recipient discover a mistyped code **before** expensive No domain label is used twice anywhere in the system. +**Canonical DH keys in K10 — clarification, 2026-09-26.** Seal binds the raw +`enc` and recipient-key bytes. For `kem_id = 1`, it accepts only a 32-byte +little-endian X25519 coordinate in `0 <= u < 2^255 - 19`, with the high bit zero. +Aliases reduced or masked by RFC 7748 are rejected. For `kem_id = 2`, it accepts +only a valid uncompressed SEC1 point `0x04 ‖ X(32) ‖ Y(32)`; compressed, hybrid, +infinite and invalid points are rejected. Both the recipient key and `enc` are +checked before DH/KDF. A zero shared secret remains invalid. Low-level +`KeyAgreement` retains its library's accepted forms and does not itself define +the Seal context. K10 formulas, valid output and frozen KAT/golden artifacts +remain unchanged. + | # | Purpose | Function | salt | ikm | info | bytes | |---|---|---|---|---|---|---| | K1 | file KEK | HKDF-SHA256 | `file_id` | `secret_A‖secret_B` | `"CC/v1/kek"‖org_id‖file_id` | 32 | diff --git a/docs/getting-started.md b/docs/getting-started.md index fccc72c..b65b0cf 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -102,11 +102,11 @@ reproducible builds. ## Choose the libraries for your integration The five libraries and the `opencrate` facade are published to crates.io at -`0.0.3`. To use the full core, add one dependency: +`0.0.4`. To use the full core, add one dependency: ```toml [dependencies] -opencrate = "=0.0.3" +opencrate = "=0.0.4" ``` The facade re-exports the libraries as `opencrate::{crypto, engine, format, @@ -115,11 +115,11 @@ individually instead: ```toml [dependencies] -oc-format = "=0.0.3" -oc-crypto = "=0.0.3" -oc-protocol = "=0.0.3" -oc-policy = "=0.0.3" -oc-engine = "=0.0.3" +oc-format = "=0.0.4" +oc-crypto = "=0.0.4" +oc-protocol = "=0.0.4" +oc-policy = "=0.0.4" +oc-engine = "=0.0.4" ``` To seal arbitrary application bytes or a small file without a `.cc` container, @@ -130,7 +130,7 @@ extension up to 16 MiB; it does not save a key or envelope for later use. ```toml [dependencies] -opencrate = { version = "=0.0.3", features = ["app-data"] } +opencrate = { version = "=0.0.4", features = ["app-data"] } ``` For persistent application data, follow the diff --git a/docs/index.md b/docs/index.md index eb95452..b207d4c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,5 +42,5 @@ key protection and enforcement. See [Translation notes](translation-notes.md) for preserved literal examples and how to read the dated specification history. -See [the 0.0.3 review](review-2026-09-26.md) for reproduced defects, checks and +See [the 0.0.4 review](review-2026-09-26.md) for reproduced defects, checks and the scope of the readability changes. diff --git a/docs/licensing.md b/docs/licensing.md index f064330..75d6c78 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -1,9 +1,9 @@ # Licensing -OpenCrate 0.0.3 and later are licensed under [MPL-2.0](../LICENSE): the five +OpenCrate 0.0.4 is licensed under [MPL-2.0](../LICENSE): the five core libraries and the `opencrate` facade. The separate [OpenCrateSDK](https://github.com/AlexiAxAxA/OpenCrateSDK) applies the same license -to SDK 0.0.3 and its experimental FFI and language wrappers. +to SDK 0.0.4 and its experimental FFI and language wrappers. There are no revenue or headcount limits and no separate commercial agreement for these versions. MPL-2.0 allows commercial use and applies copyleft at the @@ -13,7 +13,9 @@ separate files. See the [Mozilla FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/ Third-party dependencies retain their own licenses. The 0.0.1 and 0.0.2 registry archives retain their original Community License. -Publishing 0.0.3 does not rewrite those archives or their metadata. This +The intermediate 0.0.3 releases of `oc-policy`, `oc-crypto`, `oc-format` and +`opencrate-sdk` also use MPL-2.0; the other core packages skip that version. +Publishing a new version does not rewrite older archives or metadata. This repository's current code is available under MPL-2.0. Close Crate's viewer, server, and other product code are outside this repository diff --git a/docs/review-2026-09-26.md b/docs/review-2026-09-26.md index 219ad5f..cf1dff1 100644 --- a/docs/review-2026-09-26.md +++ b/docs/review-2026-09-26.md @@ -1,4 +1,4 @@ -# 0.0.3 review +# 0.0.4 review This release applies MPL-2.0 to the core and facade. The SDK uses the same license. Older registry archives retain their original terms. @@ -14,16 +14,21 @@ constant-time guarantee for secret inputs. ## Reproduced defects and fixes - `mlkem_p256::Keypair` derived `Debug` printed the 64-byte ML-KEM seed through - `Zeroizing`. Its manual implementation now redacts that field. + `Zeroizing`. Its manual implementation explicitly redacts both private fields + in normal and pretty output. - `seal_p256` accepted a compressed recipient point, but the recipient derived its KDF input from an uncompressed point. It now requires the 65-byte uncompressed representation returned by `P256Agreement::public_key`. -- `seal` accepted X25519 coordinate aliases with identical DH results and +- DH Seal accepted X25519 coordinate aliases with identical DH results and different KDF bytes. It now requires a coordinate below `2^255 - 19`. + Recipient and ephemeral encodings are now checked before DH/KDF, including + keys supplied by a `KeyAgreement` provider. P-256 points must also be valid + points on the curve. Invalid recipient encodings are rejected before RNG use. Low-level X25519 agreement still accepts RFC 7748 encodings. -All three regression tests failed before the fixes. Each includes a valid -round-trip or a check of the exposed seed; they pass after the fixes. Rejecting +All ten regression tests failed against the original public implementation and +pass after the fixes. They check both Debug formats, modulus and high-bit aliases, +invalid P-256 points, provider calls and RNG consumption. Rejecting previously unopenable recipient encodings changes input validation, not KDF, nonce or authenticated wire formulas. Existing canonical recipients retain their representation and RNG consumption order. @@ -33,7 +38,16 @@ their representation and RNG consumption order. The review used separate public source checkouts. Native tests include existing KAT and golden vectors, malformed signed headers, low-order keys and parser mutation tests. Clippy, Rustdoc with warnings denied, a pure-core WASM build, -and dependency license/advisory/source checks passed locally. +and dependency license/advisory/source checks passed locally. The full 0.0.4 +workspace release-mode run passed 681 tests, with 15 existing ignored tests. + +A separate bounded property harness passed 13 tests. It checks K10 and nonce +derivation against separately composed primitives, commitment checks, domain/AAD +binding, rollback behavior, failure cleanup, signed headers, and P-256 forms. +It verified 33,153 Merkle leaf proofs across 257 tree shapes and 5,000 stateful +tree operations. Deterministic mutation loops covered 10,000 format inputs, +10,000 crypto inputs and 10,000 SDK inputs; a hybrid mutation test covered +2,273 bytes. These counts describe test inputs, not a security guarantee. SDK checks cover 10,928 envelope bit mutations, 1,366 truncations, purpose and context binding, low-order keys, the 16 MiB boundary, and simulated entropy From 0c32b3f0d1655697a353ec1a7ad7a46b77e1ad1a Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 12:34:02 +0300 Subject: [PATCH 07/10] Fix secret diagnostics, OAEP residue and encryption plan consistency --- EXPORT-INVENTORY.json | 19 ++- crates/oc-crypto/src/rsa.rs | 48 +++++- crates/oc-crypto/src/seal.rs | 8 + crates/oc-crypto/src/stream.rs | 4 + crates/oc-crypto/tests/crypto_continuation.rs | 68 +++++++++ crates/oc-engine/src/edit.rs | 13 +- crates/oc-engine/src/lib.rs | 49 ++++++- crates/oc-engine/src/wire.rs | 13 +- crates/oc-engine/tests/crypto_continuation.rs | 138 ++++++++++++++++++ crates/oc-format/src/tlv.rs | 27 +++- crates/oc-format/tests/crypto_continuation.rs | 30 ++++ docs/review-2026-09-26.md | 22 ++- 12 files changed, 420 insertions(+), 19 deletions(-) create mode 100644 crates/oc-crypto/tests/crypto_continuation.rs create mode 100644 crates/oc-engine/tests/crypto_continuation.rs create mode 100644 crates/oc-format/tests/crypto_continuation.rs diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 63c124e..1565b20 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -156,16 +156,17 @@ "crates/oc-crypto/src/mac.rs": "75d6636bfbcbe9d7f9cd307ce5101d9d15e2023553038f42fe90a08efeb08edb", "crates/oc-crypto/src/merkle.rs": "63e117f274790f36943e7d0e69897241049ab102c14bcae958d1f80941fe93d2", "crates/oc-crypto/src/mlkem_p256.rs": "f5a77672884ab20e2278d255915cba99e827e9bb5b5f20dca5e84b778513a1f6", - "crates/oc-crypto/src/rsa.rs": "0a36fc1b91c322ac0a09fdb9c5b47db79b15b2c4a27129af83ab75a1d0ddc8f4", + "crates/oc-crypto/src/rsa.rs": "220290a344521af93ccb6306997a998a44e97296dc3fc84140fcad22365ff0a2", "crates/oc-crypto/src/rsa_test_signer.rs": "c34082bb4d6cd0df456a31eb4af37ca9721b8dc56162a9d6821cf3fcb6fd7f93", - "crates/oc-crypto/src/seal.rs": "f1405d1295693bfb172dda7d4b954b3f41f269c6c9e6cca516a93a1011b05fa8", + "crates/oc-crypto/src/seal.rs": "765dc5710e9c5330884468ef5499ffaeab5b72eddf17fa5ad9cfacbbbb720371", "crates/oc-crypto/src/secret.rs": "55bff2d293719d34fcb2de7780a4dce7d81c0c0420b5c5da3b6b2dda83bcb9dd", "crates/oc-crypto/src/sign.rs": "e231b7a66a86039c1a4e9830bbb3470250a23f046dccc70f1a6c260fc1ef54db", - "crates/oc-crypto/src/stream.rs": "6269ea48bdd1d9e494c4a8fa91bfcd73d6b84bd32d01223f686816972638a38f", + "crates/oc-crypto/src/stream.rs": "26ea721afd4165e2ff994d90b2896e24bc89463bb463cdb655da5ccc13ac75a3", "crates/oc-crypto/src/tpm.rs": "73a0c2aa437bf92b67d4967fcb4ff463b77801b8690db9a47b19fc0bef8022b1", "crates/oc-crypto/src/transcript.rs": "54ed996ac650c3b5a4aa0e804508603981ef3a596b2e9fd52d24953657a733af", "crates/oc-crypto/src/wrap.rs": "99138975a39b1f6c671a88a1756defe9edf8a17b6f601d2d3ee00b8dbdaf06f2", "crates/oc-crypto/src/xwing.rs": "139884797ecbdbe12d22425812e775a182ccf7b1d7d12f488982137f3539c449", + "crates/oc-crypto/tests/crypto_continuation.rs": "52f67862995e54fb56900f5df8c9186c912679e8ff66638535b24234dad18649", "crates/oc-crypto/tests/kat.rs": "0dc188ad01491e554049b078cfb3647b4b51de5e57715949d178bd10ff562b2a", "crates/oc-crypto/tests/measure_packing_path.rs": "085b9a74b7931fe970b30263851026c2cbbf8495898638a0861d4c3d07d99a18", "crates/oc-crypto/tests/review_regressions.rs": "84880dde33ab776afbe214bbb48db00898ce0c30aae5c0dc10feb8ce99e969d5", @@ -178,9 +179,10 @@ "crates/oc-engine/Cargo.toml": "f08036694fe9d89898403e2681e789a4f970d89ab5d8708560f675669a0b25c4", "crates/oc-engine/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-engine/README.md": "1eece36c00aecf16e31c12ce8c7cdbaa1f7b8299b435879e73f6cfec86bc42b2", - "crates/oc-engine/src/edit.rs": "ea729a45d50b190badd275549eef70f2f84a94be1bcf5acd0bc5e5a7192b5316", - "crates/oc-engine/src/lib.rs": "1456716cab8831eaf2737f2959fa33718779739e479aafc59753997a52a64bd6", - "crates/oc-engine/src/wire.rs": "a3cfdefb386786f97a9359b75383c9bc218b8d0b1982cf0dd2f1618c7b5d2922", + "crates/oc-engine/src/edit.rs": "150bb013fc322a0d422f00f41067d37e9a1c3a3ec2282819a531be1daf4db1ba", + "crates/oc-engine/src/lib.rs": "344ecc1714d74fb37e62d04b5c4f14f5d81ed70aaaae20bbb89adedda5245b0b", + "crates/oc-engine/src/wire.rs": "a04810360f419dcff5441dbf38ed0bd52d4bd0feff8d5539af4793c88d847cbb", + "crates/oc-engine/tests/crypto_continuation.rs": "c2ead30163765b5991d1b5bbc445e771d2529788f7ab988ef7a9a3b77573d914", "crates/oc-engine/tests/edit_writer.rs": "8d8d77313772bf184677c79757322073ab29bcca203154050dd0a856dd055c1c", "crates/oc-engine/tests/hardware_hybrid_golden.rs": "cec004a062ab838a2acfbe634f17c54cda1053a14337b57c51d2711e4b2033b7", "crates/oc-engine/tests/p256_author_golden.rs": "6f20067edbc1259cd981e10347c4e1df4f80af6935456b01f42a7e246451f947", @@ -197,8 +199,9 @@ "crates/oc-format/src/lib.rs": "2eb89b68917cbdf605f6d0d3252dedf82ae2eeb2f45cc8eda4d26c8d83cd28db", "crates/oc-format/src/policy_codec.rs": "ad498c2c177ac76d64818e94c1e0eb708947400273c4d2f3f7f35158ced3a25b", "crates/oc-format/src/text.rs": "2c84dc2ec2a0e9d57ee52f1478401753fc3798a5d6ff15ccaacaf53d0bc27d2a", - "crates/oc-format/src/tlv.rs": "8565cf55f7a91ea7f2c23d45e0f4144aff3a6f1bf515830a7ba1ea2acb4d50c5", + "crates/oc-format/src/tlv.rs": "39a033337bafaedd69961254d4dbe4642656201aed85e69855c3e8663ad169dc", "crates/oc-format/src/verify.rs": "48fb0398d98131167192eeb0b0edbde55eaaeab9fb603030aeda9854a4da08a5", + "crates/oc-format/tests/crypto_continuation.rs": "8183106d9f0a0e9bf5345171cd4ac63e09bdc768dbf6923c08a1d02cae213b26", "crates/oc-format/tests/edit.rs": "2e840455d87c97a1dc90145a048711a656211a4baef30b8e883ac6a96b642b2b", "crates/oc-format/tests/kat_policy.rs": "bb04d93c27a5c86f7fa533dbf224b801378229875d5fcc147cffd1c049a6a5c4", "crates/oc-format/tests/parser_robustness.rs": "5c53c6053bd3322fa11d6c184305907cc03f9b23ef463dac842b55a96b9f1ecf", @@ -259,7 +262,7 @@ "docs/index.md": "a6d5883cfd5f88cfc7b8a2c260442aae073fcc0888fdc934c4ea457076d30420", "docs/licensing.md": "82d00091b4c173fe02e506fc67821dd6fea96324bf408e6f6a7f8d93243b3ea3", "docs/protocol.md": "ec582526e61914ee6251ba2188d6680d3aa88aacaf2056d49ee6a463ce1e6990", - "docs/review-2026-09-26.md": "6bb457d9249e494ce2bb5660ccb3830d38f1722fc0abc5c45970de3c0f836faa", + "docs/review-2026-09-26.md": "4f017558ea668b2f3236d0bf54977df0772c54d03448b8ce66e0df4e7fd43375", "docs/translation-notes.md": "c290d69d83f42cd1f9d9311e32b8ad2b08391a94856e68a0b4993dfa1d103065", "examples/README.md": "7a95174a360ec6527fcb86cef5a78c011e4281010bde5ddfac8fc280f7194738", "rust-toolchain.toml": "41308dbe82a83679b7b2081be4d5ae7f9aa52f642bcc3eab104ac77501560e79", diff --git a/crates/oc-crypto/src/rsa.rs b/crates/oc-crypto/src/rsa.rs index 8f099d7..5de1564 100644 --- a/crates/oc-crypto/src/rsa.rs +++ b/crates/oc-crypto/src/rsa.rs @@ -208,6 +208,30 @@ fn public_op(modulus: &[u8], input: &[u8]) -> Result> Ok(zeroize::Zeroizing::new(out.to_vec())) } +fn oaep_public_op(modulus: &[u8], input: &[u8]) -> Result, CryptoError> { + use crypto_bigint::{Uint, modular::{FixedMontyForm, FixedMontyParams}}; + use zeroize::Zeroizing; + + if modulus.len() != Uint::::BYTES || input.len() != Uint::::BYTES { + return Err(CryptoError::BadLength); + } + if modulus.first().is_none_or(|byte| byte & 0x80 == 0) { + return Err(CryptoError::BadLength); + } + let n = Uint::::from_be_slice(modulus); + let x = Zeroizing::new(Uint::::from_be_slice(input)); + if *x >= n { + return Err(CryptoError::BadSignature); + } + let odd = Odd::new(n).into_option().ok_or(CryptoError::BadLength)?; + let params = FixedMontyParams::new(odd); + // Fixed-width arithmetic keeps OAEP intermediates out of freed heap blocks. + // Wipe the named secret values; compiler-created stack copies are not covered. + let base = Zeroizing::new(FixedMontyForm::new(&x, ¶ms)); + let encrypted = base.pow(&Uint::<1>::from_u32(65537)); + Ok(encrypted.retrieve().to_be_bytes().as_ref().to_vec()) +} + /// Verify RSASSA-PKCS1-v1_5 with SHA-256 and exponent 65537. /// /// The encoding is CONSTRUCTED from the message and compared with the recovered value in full, @@ -316,12 +340,34 @@ pub fn encrypt_oaep_sha256( seed_slot.copy_from_slice(masked_seed.as_slice()); masked_db.copy_from_slice(&db); - Ok(public_op(modulus, &em)?.to_vec()) + match k { + 256 => oaep_public_op::<{ crypto_bigint::U2048::LIMBS }>(modulus, &em), + 384 => oaep_public_op::<{ crypto_bigint::U3072::LIMBS }>(modulus, &em), + 512 => oaep_public_op::<{ crypto_bigint::U4096::LIMBS }>(modulus, &em), + _ => Err(CryptoError::BadLength), + } } #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)] mod tests { + #[test] + fn oaep_fixed_arithmetic_matches_public_operation_for_supported_widths() { + for size in [256, 384, 512] { + let modulus = vec![0xff; size]; + let mut encoded = zeroize::Zeroizing::new(vec![0x51; size]); + *encoded.first_mut().unwrap() = 0; + let expected = super::public_op(&modulus, &encoded).unwrap(); + let actual = match size { + 256 => super::oaep_public_op::<{ crypto_bigint::U2048::LIMBS }>(&modulus, &encoded), + 384 => super::oaep_public_op::<{ crypto_bigint::U3072::LIMBS }>(&modulus, &encoded), + 512 => super::oaep_public_op::<{ crypto_bigint::U4096::LIMBS }>(&modulus, &encoded), + _ => unreachable!(), + }.unwrap(); + assert_eq!(actual, *expected); + } + } + use super::*; /// Derived lengths must match the PSS layout rather than being diff --git a/crates/oc-crypto/src/seal.rs b/crates/oc-crypto/src/seal.rs index 0d2428f..291ba06 100644 --- a/crates/oc-crypto/src/seal.rs +++ b/crates/oc-crypto/src/seal.rs @@ -372,6 +372,14 @@ pub fn seal_xwing( plaintext: &[u8], rng: &mut R, ) -> Result { + if recipient_public.len() != crate::xwing::PUBLIC_KEY_LEN { + return Err(CryptoError::BadLength); + } + let (_, classical) = recipient_public + .split_last_chunk::() + .ok_or(CryptoError::BadLength)?; + // The recipient reconstructs a canonical public key for the combiner and K10. + validate_dh_public(classical, PUBLIC_KEY_LEN)?; let (shared, ciphertext) = crate::xwing::encapsulate(recipient_public, rng)?; let mut nonce_seed = Zeroizing::new([0u8; NONCE_LEN]); diff --git a/crates/oc-crypto/src/stream.rs b/crates/oc-crypto/src/stream.rs index 900cf4d..3187631 100644 --- a/crates/oc-crypto/src/stream.rs +++ b/crates/oc-crypto/src/stream.rs @@ -77,6 +77,10 @@ where G: rand_core::CryptoRng + ?Sized, { let capacity = usize::try_from(chunk_size).map_err(|_| StreamError::TooLarge)?; + // Zero capacity skips the source and would seal a nonempty file as empty. + if capacity == 0 { + return Err(CryptoError::BadLength.into()); + } // Затирающий буфер фиксированной ёмкости: обычный вектор уносил бы каждый // прочитанный кусок исходного файла в кучу — и при уничтожении, и при росте // (И-11). diff --git a/crates/oc-crypto/tests/crypto_continuation.rs b/crates/oc-crypto/tests/crypto_continuation.rs new file mode 100644 index 0000000..d965daf --- /dev/null +++ b/crates/oc-crypto/tests/crypto_continuation.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MPL-2.0 +#![allow(clippy::unwrap_used)] + +use core::cell::Cell; +use oc_crypto::{AeadAlg, CryptoError, PayloadKey, stream::{StreamError, seal_chunks}}; + +#[derive(Default)] +struct CountingRng { draws: usize } + +impl rand_core::TryRng for CountingRng { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + self.draws = self.draws.saturating_add(1); + Ok(0x42424242) + } + + fn try_next_u64(&mut self) -> Result { + self.draws = self.draws.saturating_add(1); + Ok(0x4242424242424242) + } + + fn try_fill_bytes(&mut self, bytes: &mut [u8]) -> Result<(), Self::Error> { + self.draws = self.draws.saturating_add(1); + bytes.fill(0x42); + Ok(()) + } +} + +impl rand_core::TryCryptoRng for CountingRng {} + +#[test] +fn xwing_seal_rejects_noncanonical_classical_key_before_rng() { + let secret = [0x31; oc_crypto::xwing::SECRET_LEN]; + let public = oc_crypto::xwing::public_key(&secret).unwrap(); + let canonical = oc_crypto::seal::seal_xwing( + &public, b"context", b"aad", b"secret", &mut CountingRng::default(), + ).unwrap(); + let opened = oc_crypto::seal::open_xwing(&secret, &canonical, b"context", b"aad") + .unwrap(); + assert_eq!(opened.as_slice(), b"secret"); + let mut alias = public; + *alias.last_mut().unwrap() |= 0x80; + let mut rng = CountingRng::default(); + let result = oc_crypto::seal::seal_xwing(&alias, b"context", b"aad", b"secret", &mut rng); + if let Ok(blob) = &result { + assert!(oc_crypto::seal::open_xwing(&secret, blob, b"context", b"aad").is_err(), "control did not reproduce mismatch"); + } + assert!(matches!(result, Err(CryptoError::BadKey)), "noncanonical X-Wing key accepted"); + assert_eq!(rng.draws, 0); +} + +#[test] +fn zero_chunk_size_is_rejected_before_callbacks_or_rng() { + let reads = Cell::new(0_usize); + let writes = Cell::new(0_usize); + let mut rng = CountingRng::default(); + let result = seal_chunks( + &PayloadKey::from_bytes([0x31; 32]), + AeadAlg::XChaCha20Poly1305, &[0x42; 16], 0, &mut rng, + |_| { reads.set(reads.get().saturating_add(1)); Ok::<_, core::convert::Infallible>(0) }, + |_| { writes.set(writes.get().saturating_add(1)); Ok(()) }, + ); + assert!(matches!(result, Err(StreamError::Crypto(CryptoError::BadLength))), "zero capacity accepted"); + assert_eq!(reads.get(), 0); + assert_eq!(writes.get(), 0); + assert_eq!(rng.draws, 0); +} diff --git a/crates/oc-engine/src/edit.rs b/crates/oc-engine/src/edit.rs index 2729bff..4898c52 100644 --- a/crates/oc-engine/src/edit.rs +++ b/crates/oc-engine/src/edit.rs @@ -31,7 +31,6 @@ use oc_format::edit::{ }; /// A piece of the new revision. -#[derive(Debug)] pub enum Piece<'a> { /// An old-file frame with THIS SAME index: `nonce ‖ ct ‖ tag`. Keep(&'a [u8]), @@ -39,6 +38,18 @@ pub enum Piece<'a> { Seal { plaintext: &'a [u8], seed: [u8; NONCE_LEN] }, } +impl core::fmt::Debug for Piece<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Keep(frame) => f.debug_tuple("Keep").field(&frame.len()).finish(), + Self::Seal { plaintext, .. } => f.debug_struct("Seal") + .field("plaintext_len", &plaintext.len()) + .field("seed", &"") + .finish(), + } + } +} + /// Inputs needed to assemble a revision. #[derive(Debug)] pub struct EditPlan<'a> { diff --git a/crates/oc-engine/src/lib.rs b/crates/oc-engine/src/lib.rs index dbe6238..7fba0b7 100644 --- a/crates/oc-engine/src/lib.rs +++ b/crates/oc-engine/src/lib.rs @@ -50,6 +50,8 @@ pub enum EngineError { Crypto(CryptoError), /// The assembled header is internally inconsistent. CoreHashMismatch, + /// Assembly changed the planned chunk size or recipient. + PlanMismatch, /// Hybrid recipient, but no author hybrid half was supplied. /// /// A distinct error, not a downgrade to a classical slot: downgrading @@ -75,6 +77,7 @@ impl core::fmt::Display for EngineError { match self { Self::Format(e) => write!(f, "{e}"), Self::Crypto(e) => write!(f, "{e}"), + Self::PlanMismatch => f.write_str("assembly request differs from the encryption plan"), Self::CoreHashMismatch => { write!(f, "хеш ядра заголовка изменился после добавления слотов") } @@ -147,6 +150,25 @@ enum RecipientPlan { Claim([u8; 32]), } +impl RecipientPlan { + fn matches(&self, file_id: &[u8; 16], requested: &Recipient) -> bool { + match (self, requested) { + (Self::None, Recipient::None) => true, + (Self::Identity(planned), Recipient::Identity { public_key }) => + oc_crypto::public_key_eq(planned, public_key), + (Self::Hybrid(planned), Recipient::Hybrid { public_key }) => + oc_crypto::public_key_eq(planned.as_slice(), public_key.as_slice()), + (Self::HardwareHybrid(planned), Recipient::HardwareHybrid { public_key }) => + oc_crypto::public_key_eq(planned.as_slice(), public_key.as_slice()), + (Self::Claim(planned), Recipient::Claim { secret }) => { + let (_, commitment) = kdf::secret_b_from_claim(file_id, secret); + oc_crypto::digest_eq(planned, &commitment) + } + _ => false, + } + } +} + /// Public keys the engine places in the header and slots. /// /// PUBLIC only. No device secret enters here, enforced @@ -181,7 +203,6 @@ pub struct PublicKeys<'a> { } /// What to pack and under which rules. -#[derive(Debug)] pub struct PackRequest<'a> { pub original_name: &'a str, pub policy: Policy, @@ -217,6 +238,20 @@ pub struct SealedInfo { pub tree_root: [u8; 32], } +impl core::fmt::Debug for PackRequest<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PackRequest") + .field("original_name", &"") + .field("policy", &self.policy) + .field("chunk_size", &self.chunk_size) + .field("org_id", &self.org_id) + .field("authority_urls", &self.authority_urls) + .field("recipient", &self.recipient) + .field("coauthors", &self.coauthors) + .finish() + } +} + /// What the engine returns after assembly. /// /// # NO signing transcript here, deliberately @@ -246,6 +281,7 @@ pub struct Assembled { pub struct Session { file_id: [u8; 16], header_salt: [u8; 32], + chunk_size: u32, cek: Cek, secret_a: SecretA, secret_b: SecretB, @@ -314,7 +350,7 @@ pub fn plan(request: &PackRequest<'_>, rng: &mut G) -> (S kdf::derive_payload_key(&cek, &header_salt, &file_id, request.chunk_size, suite().aead); ( - Session { file_id, header_salt, cek, secret_a, secret_b, plan }, + Session { file_id, header_salt, chunk_size: request.chunk_size, cek, secret_a, secret_b, plan }, Plan { file_id, payload_key, chunk_size: request.chunk_size, aead: suite().aead }, ) } @@ -329,7 +365,8 @@ impl Session { /// /// # Errors /// Returns [`EngineError`] on encoding or sealing failure, or a - /// recomputed core-hash mismatch. + /// recomputed core-hash mismatch. The chunk size and recipient must match + /// the request used by [`plan`], otherwise [`EngineError::PlanMismatch`]. pub fn assemble( self, request: &PackRequest<'_>, @@ -337,7 +374,11 @@ impl Session { sealed: SealedInfo, rng: &mut G, ) -> Result { - let Session { file_id, header_salt, cek, secret_a, secret_b, plan } = self; + let Session { file_id, header_salt, chunk_size, cek, secret_a, secret_b, plan } = self; + // Keep the K3 chunk size and recipient selected during planning. + if request.chunk_size != chunk_size || !plan.matches(&file_id, &request.recipient) { + return Err(EngineError::PlanMismatch); + } let private_meta = seal_private_meta(&cek, &header_salt, &file_id, request, sealed.total_len, rng)?; diff --git a/crates/oc-engine/src/wire.rs b/crates/oc-engine/src/wire.rs index f4c6cfa..b52eb3c 100644 --- a/crates/oc-engine/src/wire.rs +++ b/crates/oc-engine/src/wire.rs @@ -92,7 +92,7 @@ impl std::error::Error for WireError {} /// is impossible. The methods below turn it into a "request plus public keys" pair, /// preventing a second set of engine fields that would diverge from /// the first. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct PlanArgs { pub original_name: String, pub policy: Policy, @@ -115,6 +115,17 @@ pub struct PlanArgs { pub coauthors: Option, } +impl core::fmt::Debug for PlanArgs { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PlanArgs") + .field("original_name", &"") + .field("chunk_size", &self.chunk_size) + .field("recipient", &self.recipient) + .field("author", &self.author) + .finish_non_exhaustive() + } +} + impl PlanArgs { /// Construct an owned request from borrowed halves. /// diff --git a/crates/oc-engine/tests/crypto_continuation.rs b/crates/oc-engine/tests/crypto_continuation.rs new file mode 100644 index 0000000..c1dcfd4 --- /dev/null +++ b/crates/oc-engine/tests/crypto_continuation.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MPL-2.0 +#![allow(clippy::unwrap_used, clippy::panic)] + +use oc_crypto::{secret::X25519Secret, seal}; +use oc_engine::{PackRequest, PublicKeys, Recipient, SealedInfo}; +use oc_policy::Policy; + +struct Fixed; +impl rand_core::TryRng for Fixed { + type Error = core::convert::Infallible; + fn try_next_u32(&mut self) -> Result { Ok(0x42424242) } + fn try_next_u64(&mut self) -> Result { Ok(0x4242424242424242) } + fn try_fill_bytes(&mut self, bytes: &mut [u8]) -> Result<(), Self::Error> { + bytes.fill(0x42); Ok(()) + } +} +impl rand_core::TryCryptoRng for Fixed {} + +fn request() -> PackRequest<'static> { + PackRequest { original_name: "private.txt", policy: Policy::deny_all(), + chunk_size: 4096, org_id: b"org".to_vec(), authority_urls: vec![], + recipient: Recipient::None, coauthors: None } +} + +fn keys() -> PublicKeys<'static> { + let public = seal::x25519_public(&X25519Secret::from_bytes([0x31; 32])); + PublicKeys { author: [0x42; 32], authority_sealing: public, + authority_lease_verify: [0x53; 32], device: public, + device_hardware_hybrid: None, device_hybrid: None, device_tpm: None } +} + +#[test] +fn changed_chunk_size_cannot_assemble_a_header_with_a_different_payload_key() { + let mut request = request(); + let (session, plan) = oc_engine::plan(&request, &mut Fixed); + request.chunk_size = 8192; + let result = session.assemble(&request, &keys(), SealedInfo { + total_len: 6, chunk_count: 1, tree_root: [0x64; 32], + }, &mut Fixed); + if let Ok(assembled) = &result { + // The recipient recovers CEK but derives a different payload key from the header. + let parsed = oc_format::header::Header::decode(&assembled.header).unwrap(); + let header = &parsed.header; + let core = oc_format::header::Header::core_hash(&assembled.header, &parsed.spans).unwrap(); + let policy = oc_format::header::Header::policy_hash(&assembled.header, &parsed.spans).unwrap(); + let slot = header.key_slots.iter().find_map(|s| match s { + oc_format::header::KeySlot::Known(s) if s.kind == oc_format::header::SlotKind::AuthorDevice => Some(s), + _ => None, + }).unwrap(); + let secret = X25519Secret::from_bytes([0x31; 32]); + let info = seal::slot_info(oc_crypto::label::SLOT_AUTHOR_DEVICE, slot.kem, &header.file_id); + let opened = seal::open(&secret, &seal::SealedBlob { + enc: slot.enc.clone(), nonce: slot.nonce, ct: slot.ct.clone(), + }, &info, &policy).unwrap(); + let (a, b) = opened.split_at(32); + let kek = oc_crypto::kdf::derive_kek(&header.file_id, &header.org_id, + &oc_crypto::SecretA::from_bytes(a.try_into().unwrap()), + &oc_crypto::SecretB::from_bytes(b.try_into().unwrap())); + let cek = oc_crypto::wrap::unwrap_cek(&kek, &header.wrapped_cek, &slot.commitment, &core).unwrap(); + let reader_key = oc_crypto::kdf::derive_payload_key(&cek, &header.header_salt, + &header.file_id, header.chunk_size, header.suite.aead); + assert!(reader_key.expose() != plan.payload_key.expose(), "probe did not reproduce key mismatch"); + } + assert!(result.is_err(), "assemble accepts metadata inconsistent with the payload KDF"); +} + +#[test] +fn pack_request_debug_redacts_private_filename() { + let first = request(); + let mut other = request(); + other.original_name = "another.txt"; + assert!(format!("{first:?}") == format!("{other:?}"), "private filename affects Debug"); + assert!(format!("{first:#?}") == format!("{other:#?}"), "private filename affects pretty Debug"); +} + +#[test] +fn editing_piece_debug_redacts_plaintext_and_nonce_seed() { + let first = oc_engine::edit::Piece::Seal { plaintext: &[0x51; 32], seed: [0x62; 24] }; + let second = oc_engine::edit::Piece::Seal { plaintext: &[0xa7; 32], seed: [0xb8; 24] }; + assert!(format!("{first:?}") == format!("{second:?}"), "plaintext or seed affects Debug"); + assert!(format!("{first:#?}") == format!("{second:#?}"), "plaintext or seed affects pretty Debug"); +} + +#[test] +fn wire_plan_debug_redacts_private_filename() { + let first = oc_engine::wire::PlanArgs::from_parts(&request(), &keys()); + let mut second = first.clone(); + second.original_name = "another.txt".into(); + assert!(format!("{first:?}") == format!("{second:?}"), "filename affects wire Debug"); + assert!(format!("{first:#?}") == format!("{second:#?}"), "filename affects wire pretty Debug"); +} + +#[test] +fn changed_recipient_cannot_silently_seal_to_the_previous_recipient() { + let old = X25519Secret::from_bytes([0x64; 32]); + let intended = X25519Secret::from_bytes([0x75; 32]); + let mut request = request(); + request.recipient = Recipient::Identity { public_key: seal::x25519_public(&old) }; + let (session, _) = oc_engine::plan(&request, &mut Fixed); + request.recipient = Recipient::Identity { public_key: seal::x25519_public(&intended) }; + let result = session.assemble( + &request, + &keys(), + SealedInfo { total_len: 6, chunk_count: 1, tree_root: [0x64; 32] }, + &mut Fixed, + ); + if let Ok(assembled) = &result { + let parsed = oc_format::header::Header::decode(&assembled.header).unwrap(); + let header = &parsed.header; + let policy = + oc_format::header::Header::policy_hash(&assembled.header, &parsed.spans).unwrap(); + let slot = header + .key_slots + .iter() + .find_map(|s| match s { + oc_format::header::KeySlot::Known(s) + if s.kind == oc_format::header::SlotKind::RecipientIdentity => + { + Some(s) + } + _ => None, + }) + .unwrap(); + let info = seal::slot_info(oc_crypto::label::SLOT_RECIPIENT, slot.kem, &header.file_id); + let blob = + seal::SealedBlob { enc: slot.enc.clone(), nonce: slot.nonce, ct: slot.ct.clone() }; + assert!( + seal::open(&old, &blob, &info, &policy).is_ok(), + "probe did not reproduce old recipient access" + ); + assert!( + seal::open(&intended, &blob, &info, &policy).is_err(), + "probe did not reproduce intended recipient failure" + ); + } + assert!(result.is_err(), "assemble silently uses the earlier recipient"); +} + diff --git a/crates/oc-format/src/tlv.rs b/crates/oc-format/src/tlv.rs index f684443..6496acf 100644 --- a/crates/oc-format/src/tlv.rs +++ b/crates/oc-format/src/tlv.rs @@ -36,7 +36,7 @@ pub const FIELD_PREFIX_LEN: usize = 6; /// The range is essential, not a convenience: policy and header core hashes use /// original bytes rather than a re-encoding of the parsed structure, avoiding /// the whole family of canonicalization bugs known from JWS and XML-DSig. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct Field<'a> { pub tag: u16, pub value: &'a [u8], @@ -44,6 +44,13 @@ pub struct Field<'a> { pub span: Range, } +impl core::fmt::Debug for Field<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Field").field("tag", &self.tag) + .field("value_len", &self.value.len()).field("span", &self.span).finish() + } +} + impl<'a> Field<'a> { /// Value as exactly one byte. pub fn u8(&self) -> Result { @@ -80,13 +87,19 @@ impl<'a> Field<'a> { } /// Sequential field reading with increasing-tag validation. -#[derive(Debug)] pub struct TlvReader<'a> { buf: &'a [u8], pos: usize, last_tag: Option, } +impl core::fmt::Debug for TlvReader<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TlvReader").field("len", &self.buf.len()) + .field("pos", &self.pos).field("last_tag", &self.last_tag).finish() + } +} + impl<'a> TlvReader<'a> { /// Start reading. Parses nothing: parsing happens in [`TlvReader::next_field`]. pub fn new(buf: &'a [u8]) -> Self { @@ -162,12 +175,20 @@ impl<'a> TlvReader<'a> { /// then protect the file on disk but not the process that assembled it. /// Zeroizing does not harm public header fields: the cost is one memset /// when the writer is destroyed. -#[derive(Debug, Default)] +#[derive(Default)] pub struct TlvWriter { buf: Zeroizing>, last_tag: Option, } +impl core::fmt::Debug for TlvWriter { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // TLV also carries key seeds and private metadata. + f.debug_struct("TlvWriter").field("len", &self.buf.len()) + .field("last_tag", &self.last_tag).finish() + } +} + impl TlvWriter { pub fn new() -> Self { Self::default() diff --git a/crates/oc-format/tests/crypto_continuation.rs b/crates/oc-format/tests/crypto_continuation.rs new file mode 100644 index 0000000..04c9c57 --- /dev/null +++ b/crates/oc-format/tests/crypto_continuation.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 +#![allow(clippy::unwrap_used)] + +use oc_format::tlv::{Field, TlvReader, TlvWriter}; + +#[test] +fn writer_debug_omits_field_contents() { + let mut first = TlvWriter::new(); + first.put(1, &[0x51; 32]).unwrap(); + let mut second = TlvWriter::new(); + second.put(1, &[0xa7; 32]).unwrap(); + assert!(format!("{first:?}") == format!("{second:?}"), "value affects Debug"); + assert!(format!("{first:#?}") == format!("{second:#?}"), "value affects pretty Debug"); +} + +#[test] +fn reader_debug_omits_input_contents() { + let first = TlvReader::new(&[0x51; 32]); + let second = TlvReader::new(&[0xa7; 32]); + assert!(format!("{first:?}") == format!("{second:?}"), "input affects Debug"); + assert!(format!("{first:#?}") == format!("{second:#?}"), "input affects pretty Debug"); +} + +#[test] +fn field_debug_omits_value_contents() { + let first = Field { tag: 1, value: &[0x51; 32], span: 6..38 }; + let second = Field { tag: 1, value: &[0xa7; 32], span: 6..38 }; + assert!(format!("{first:?}") == format!("{second:?}"), "value affects Debug"); + assert!(format!("{first:#?}") == format!("{second:#?}"), "value affects pretty Debug"); +} diff --git a/docs/review-2026-09-26.md b/docs/review-2026-09-26.md index cf1dff1..eb441da 100644 --- a/docs/review-2026-09-26.md +++ b/docs/review-2026-09-26.md @@ -1,4 +1,4 @@ -# 0.0.4 review +# MPL release and cryptographic review This release applies MPL-2.0 to the core and facade. The SDK uses the same license. Older registry archives retain their original terms. @@ -35,6 +35,26 @@ their representation and RNG consumption order. ## Checks and limits +The follow-up for 0.0.5 also fixes: + +- TLV fields/readers/writers, editing pieces and packing requests no longer print + their private contents through `Debug`, including owned wire requests. +- RSA-OAEP uses fixed-width arithmetic for its secret encoded message. Named + secret values are wiped, avoiding recoverable OAEP data in freed heap limbs. + This does not prove erasure of compiler-created stack copies. +- Streaming encryption rejects zero chunk capacity before callbacks or RNG use. +- Header assembly rejects a chunk size or recipient different from the plan; + otherwise it can derive a different payload key or seal to the old recipient. +- X-Wing Seal requires a canonical classical recipient coordinate before RNG + use. The raw X-Wing primitive and its frozen combiner remain unchanged. + +Ten new regressions fail against the published 0.0.4 source and pass after +these fixes. Two private allocator checks reproduce OAEP residue with a +synthetic modulus and a real RSA key in registry 0.0.4, and pass after the fix. +An arithmetic compatibility test covers RSA-2048, RSA-3072 and RSA-4096. +The updated workspace passes 692 tests, with 15 existing ignored tests; +Clippy, Rustdoc and the pure-core WASM build pass. Frozen artifacts are unchanged. + The review used separate public source checkouts. Native tests include existing KAT and golden vectors, malformed signed headers, low-order keys and parser mutation tests. Clippy, Rustdoc with warnings denied, a pure-core WASM build, From 0fe5629bbeabbcc48cba0532c29b827f19515a74 Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 12:51:25 +0300 Subject: [PATCH 08/10] Prepare coordinated 0.0.5 release with registry SDK and audit regressions --- Cargo.lock | 16 +- Cargo.toml | 2 +- EXPORT-INVENTORY.json | 30 ++-- README.md | 6 +- crates/oc-crypto/tests/crypto_continuation.rs | 46 ++++-- crates/oc-engine/Cargo.toml | 6 +- crates/oc-engine/tests/crypto_continuation.rs | 139 ++++++++++++++---- crates/oc-format/Cargo.toml | 6 +- crates/oc-protocol/Cargo.toml | 6 +- crates/opencrate/Cargo.toml | 14 +- crates/opencrate/README.md | 6 +- docs/format.md | 6 + docs/getting-started.md | 16 +- docs/index.md | 2 +- docs/review-2026-09-26.md | 5 +- 15 files changed, 209 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91d854d..4645f87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "oc-crypto" -version = "0.0.4" +version = "0.0.5" dependencies = [ "blake3", "chacha20poly1305", @@ -456,7 +456,7 @@ dependencies = [ [[package]] name = "oc-engine" -version = "0.0.4" +version = "0.0.5" dependencies = [ "oc-crypto", "oc-format", @@ -467,7 +467,7 @@ dependencies = [ [[package]] name = "oc-format" -version = "0.0.4" +version = "0.0.5" dependencies = [ "oc-crypto", "oc-policy", @@ -477,11 +477,11 @@ dependencies = [ [[package]] name = "oc-policy" -version = "0.0.4" +version = "0.0.5" [[package]] name = "oc-protocol" -version = "0.0.4" +version = "0.0.5" dependencies = [ "oc-crypto", "oc-format", @@ -490,7 +490,7 @@ dependencies = [ [[package]] name = "opencrate" -version = "0.0.4" +version = "0.0.5" dependencies = [ "oc-crypto", "oc-engine", @@ -502,9 +502,9 @@ dependencies = [ [[package]] name = "opencrate-sdk" -version = "0.0.4" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0d2e1df1749daffc2f718ebf97a0c654580f0d3ef110d4d004d5ecde29738f" +checksum = "5a249ea77288ea231ea87d649415404804c485396fc6e8bace20b774fb5492c1" dependencies = [ "getrandom", "oc-crypto", diff --git a/Cargo.toml b/Cargo.toml index 5603752..ea3a65a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = ["crates/oc-crypto", "crates/oc-format", "crates/oc-protocol", "crates oc-crypto = { path = "crates/oc-crypto" } [workspace.package] -version = "0.0.4" +version = "0.0.5" edition = "2024" rust-version = "1.90" license = "MPL-2.0" diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 1565b20..8b0cb75 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -140,10 +140,10 @@ ".github/workflows/ci.yml": "37f74c0187b1130489f60dc325d10f3d5c1f47e58df507e470cc5edb53542a33", ".gitignore": "ac05572173ab2ab7437511f230c35c4d767ebb9611496581f7bc9e54c57bc24a", "CONTRIBUTING.md": "ffa723ec1487243ea5fdde2ffdd36d00c59ea2c076de5baf08a1bf1b67c1a098", - "Cargo.lock": "3697060ceeff838b0d4fb770263278b669382bfe80b6b0b677f40d4f8bb76392", - "Cargo.toml": "b936807a4ea732d45403727986d27e23ab83823b488f6fe81d03044db5b201c6", + "Cargo.lock": "899d292b33c8813943adad12b7f70daae6daf0a5f802fadb78ae836776b40b1c", + "Cargo.toml": "713c746480cbb9c1e3ef502a42c190f96769e5eb56d256b190854b6649039779", "LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", - "README.md": "7310d9533445b67186bdad672e88109fab01c91619940f27cb3a0388bd5bce2d", + "README.md": "2c77e6daec8d8eb62a3d42a7ae407e15b3eca6698f745dcbd4446b16f41154a1", "SECURITY.md": "839c254e62ea1cfa4628520ca6d1e145c5fc1f6b109ab4f2f9c711b3db26d930", "clippy.toml": "447c1c2ae9dc0dfef9bae35ca82cb04f08c09bf7943d3835823138b546c03621", "crates/oc-crypto/Cargo.toml": "63a4268cf85daca35746b15080aee8b51fd703e59ae3a08ed76cf479f7970804", @@ -166,7 +166,7 @@ "crates/oc-crypto/src/transcript.rs": "54ed996ac650c3b5a4aa0e804508603981ef3a596b2e9fd52d24953657a733af", "crates/oc-crypto/src/wrap.rs": "99138975a39b1f6c671a88a1756defe9edf8a17b6f601d2d3ee00b8dbdaf06f2", "crates/oc-crypto/src/xwing.rs": "139884797ecbdbe12d22425812e775a182ccf7b1d7d12f488982137f3539c449", - "crates/oc-crypto/tests/crypto_continuation.rs": "52f67862995e54fb56900f5df8c9186c912679e8ff66638535b24234dad18649", + "crates/oc-crypto/tests/crypto_continuation.rs": "12810d3ef380c34612aeb366d4b3f53b591526d6dee38c6bf1f205899507b22e", "crates/oc-crypto/tests/kat.rs": "0dc188ad01491e554049b078cfb3647b4b51de5e57715949d178bd10ff562b2a", "crates/oc-crypto/tests/measure_packing_path.rs": "085b9a74b7931fe970b30263851026c2cbbf8495898638a0861d4c3d07d99a18", "crates/oc-crypto/tests/review_regressions.rs": "84880dde33ab776afbe214bbb48db00898ce0c30aae5c0dc10feb8ce99e969d5", @@ -176,17 +176,17 @@ "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "44ee682d809c6211311e89c6193652adccb432b5e2cc2feabd5f94c1377c09ca", "crates/oc-crypto/tests/spec_labels.rs": "c5a15f4792ffa62c1f49d7b08aa8ef95c13d28b0a795575155e98f6af65022e4", "crates/oc-crypto/tests/zeroize_on_drop.rs": "6049affea1696a0235a3f47685d7126c9e022ca61ca3000d4abcffa6e1c4dc9e", - "crates/oc-engine/Cargo.toml": "f08036694fe9d89898403e2681e789a4f970d89ab5d8708560f675669a0b25c4", + "crates/oc-engine/Cargo.toml": "fb2dcba7b5f16832438dd49e71bb8efc52eff9c1b73de5375614f4c9292c47ae", "crates/oc-engine/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-engine/README.md": "1eece36c00aecf16e31c12ce8c7cdbaa1f7b8299b435879e73f6cfec86bc42b2", "crates/oc-engine/src/edit.rs": "150bb013fc322a0d422f00f41067d37e9a1c3a3ec2282819a531be1daf4db1ba", "crates/oc-engine/src/lib.rs": "344ecc1714d74fb37e62d04b5c4f14f5d81ed70aaaae20bbb89adedda5245b0b", "crates/oc-engine/src/wire.rs": "a04810360f419dcff5441dbf38ed0bd52d4bd0feff8d5539af4793c88d847cbb", - "crates/oc-engine/tests/crypto_continuation.rs": "c2ead30163765b5991d1b5bbc445e771d2529788f7ab988ef7a9a3b77573d914", + "crates/oc-engine/tests/crypto_continuation.rs": "cb4e3c6599b286e6f652d34273f8f2b598f4a07ca2cdfabc970f9fce1d2d09ef", "crates/oc-engine/tests/edit_writer.rs": "8d8d77313772bf184677c79757322073ab29bcca203154050dd0a856dd055c1c", "crates/oc-engine/tests/hardware_hybrid_golden.rs": "cec004a062ab838a2acfbe634f17c54cda1053a14337b57c51d2711e4b2033b7", "crates/oc-engine/tests/p256_author_golden.rs": "6f20067edbc1259cd981e10347c4e1df4f80af6935456b01f42a7e246451f947", - "crates/oc-format/Cargo.toml": "8a06ce853ae2063af066d6ee0416700efb820e8c4c1907011c0eb50bcfa9f09e", + "crates/oc-format/Cargo.toml": "b8e98c566e068f81729e8003777016d6522a9aaeddecb8294fb64c31ede13737", "crates/oc-format/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-format/README.md": "54e78a93669649329fa9b68022c84bd69df7751fca007941b60f2ea0e1b16b2c", "crates/oc-format/examples/verify-header.rs": "5c9ca98e85e1aade70875f9a935ae34618f94737e1b14336dba756d8fd62d59c", @@ -215,7 +215,7 @@ "crates/oc-policy/src/lib.rs": "743eaf0550615071ef98ffab4b42c271981332fe19ad2290e20f1e21dea0ade4", "crates/oc-policy/tests/security_probe_skeptic.rs": "c60f720854250afc356466c6a3d5227ee80e50a41d28d0e3c53a580082d6f769", "crates/oc-policy/tests/server_policy_composition.rs": "c7069b4151bb0660e03050b249fe0f9dcafab44ed3bb3db70ad05977308bb87d", - "crates/oc-protocol/Cargo.toml": "6fa1277b4b24a9d321faafeac346810d858824217c10583466f1a607d181c047", + "crates/oc-protocol/Cargo.toml": "4cea4c593ab2c4b733e4859b0b8ba7ee46f6796db86404be36a9b90e5b4b5b14", "crates/oc-protocol/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-protocol/README.md": "42ca1744fee5ac93b7bbdfcafcd016cf19c427ca208ae2491e3351d65a107c42", "crates/oc-protocol/src/access.rs": "5a846d5285cd6beb48b5bb753a5ae836912580c8d91dc35f7a5f8e03324c37eb", @@ -243,9 +243,9 @@ "crates/oc-protocol/tests/lease_signature.rs": "f8f9de0fd92d546f34cdbf3dd036e664e682d4f40aa7f6f907e680a6d81ea37a", "crates/oc-protocol/tests/session_mac_table.rs": "67d28231e53eb1bcde0e75b3703a62715aab24f9870aa0bcfe7eaad5a7103296", "crates/oc-protocol/tests/unknown_tags.rs": "ddb5efc2c168b863c8b176bb327098bc657c9324ffb92700d98df1854c85d85f", - "crates/opencrate/Cargo.toml": "033d6fd548324ebff26a78d8890762cec4a4e6cf9c9c22fa6667db6893e66a2e", + "crates/opencrate/Cargo.toml": "f16a1cf6c3e69b12072b9cd54d926ad3014b6678a39f5b05be5909f851c82f19", "crates/opencrate/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", - "crates/opencrate/README.md": "1979dd4d5cd02bebb4ad54f418084c14711f94d6572bf35aad5417b1e46c70fe", + "crates/opencrate/README.md": "eeae58841fa2c5b905455150f8adb5a8e2d51b1b29d41377080e40c260326def", "crates/opencrate/examples/seal-file.rs": "802e2d887f54e60386e02c500d12667468b8acb5a89654d4cfc5aa7edaa22709", "crates/opencrate/src/lib.rs": "8a6effdca9c9709505ea59bca87879d432be8ed70808ef4d67c18dcb05612555", "deny.toml": "e06d8f3d9ac96158cc073b84076ca98a9efedb5e5e9e460c60715506cf5c6daa", @@ -256,13 +256,13 @@ "docs/assets/open-crate-banner.svg": "9e169d913482e4eb5c01094b20b6870c4b3c2931514dc0b8b0f37c489c170b9c", "docs/assets/open-crate-mark.svg": "f3039e6557a4a5cac7aff44ec4457119efc1fa543caf17320fafc15489ed0dc0", "docs/assets/quick-start.gif": "4f0232560ff9bd8e1e7a1c34c4c04b5aa94deeb134e22ea722307e0b3aa5bee1", - "docs/format.md": "e33752fd977afd6077b5bd8d2c804464fbcb291f0fa4430e6495b9142252712a", - "docs/getting-started.md": "6a5ed33429c4b51a690dd7ee253e44a1d63dbf4516eb1c59787493c09aa2716f", + "docs/format.md": "d262289257c996e07406d5b456dc56134ebd2281fb993c7181c8e0ef831a9adc", + "docs/getting-started.md": "c0186e6e7d01bf58610265db1c9865f838d54c017867ce91391f13b2bc70e78d", "docs/how-it-works.md": "5a15ef7455a46a9b550a85359fcf0b8e8dd85bcbc45e2ded612676b1cc8826e7", - "docs/index.md": "a6d5883cfd5f88cfc7b8a2c260442aae073fcc0888fdc934c4ea457076d30420", + "docs/index.md": "fbfee7867d4f4f19c286aaf117dc7d70ff25e334f69616a9683c6e406260ca55", "docs/licensing.md": "82d00091b4c173fe02e506fc67821dd6fea96324bf408e6f6a7f8d93243b3ea3", "docs/protocol.md": "ec582526e61914ee6251ba2188d6680d3aa88aacaf2056d49ee6a463ce1e6990", - "docs/review-2026-09-26.md": "4f017558ea668b2f3236d0bf54977df0772c54d03448b8ce66e0df4e7fd43375", + "docs/review-2026-09-26.md": "7bca98756b86ffb3e5df5ad86f21685533c126f34f4031b5a0a4e3e99a4106fe", "docs/translation-notes.md": "c290d69d83f42cd1f9d9311e32b8ad2b08391a94856e68a0b4993dfa1d103065", "examples/README.md": "7a95174a360ec6527fcb86cef5a78c011e4281010bde5ddfac8fc280f7194738", "rust-toolchain.toml": "41308dbe82a83679b7b2081be4d5ae7f9aa52f642bcc3eab104ac77501560e79", @@ -316,6 +316,6 @@ "registry_upload": false }, "reviewed_public_baseline": "7e607dfcc6e3cc948e9ee7de9c1da56a76d8242e", - "release_candidate": "0.0.4", + "release_candidate": "0.0.5", "license": "MPL-2.0" } diff --git a/README.md b/README.md index c7c472f..488ce04 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ decisions. It is the five-library core behind Close Crate. For a Rust project, add the `opencrate` facade to use all five libraries, or depend on the individual `oc-*` crates you need. The facade is version -`0.0.4`; the five core libraries are also `0.0.4` on crates.io: +`0.0.5`; the five core libraries are also `0.0.5` on crates.io: ```toml [dependencies] -opencrate = "=0.0.4" +opencrate = "=0.0.5" ``` Your application supplies storage, transport, keys, secure randomness and time. @@ -63,7 +63,7 @@ extension, or JSON and messages, for one recipient. It re-exports the separate ```toml [dependencies] -opencrate = { version = "=0.0.4", features = ["app-data"] } +opencrate = { version = "=0.0.5", features = ["app-data"] } ``` ```sh diff --git a/crates/oc-crypto/tests/crypto_continuation.rs b/crates/oc-crypto/tests/crypto_continuation.rs index d965daf..1a5c893 100644 --- a/crates/oc-crypto/tests/crypto_continuation.rs +++ b/crates/oc-crypto/tests/crypto_continuation.rs @@ -2,10 +2,15 @@ #![allow(clippy::unwrap_used)] use core::cell::Cell; -use oc_crypto::{AeadAlg, CryptoError, PayloadKey, stream::{StreamError, seal_chunks}}; +use oc_crypto::{ + AeadAlg, CryptoError, PayloadKey, + stream::{StreamError, seal_chunks}, +}; #[derive(Default)] -struct CountingRng { draws: usize } +struct CountingRng { + draws: usize, +} impl rand_core::TryRng for CountingRng { type Error = core::convert::Infallible; @@ -34,17 +39,24 @@ fn xwing_seal_rejects_noncanonical_classical_key_before_rng() { let secret = [0x31; oc_crypto::xwing::SECRET_LEN]; let public = oc_crypto::xwing::public_key(&secret).unwrap(); let canonical = oc_crypto::seal::seal_xwing( - &public, b"context", b"aad", b"secret", &mut CountingRng::default(), - ).unwrap(); - let opened = oc_crypto::seal::open_xwing(&secret, &canonical, b"context", b"aad") - .unwrap(); + &public, + b"context", + b"aad", + b"secret", + &mut CountingRng::default(), + ) + .unwrap(); + let opened = oc_crypto::seal::open_xwing(&secret, &canonical, b"context", b"aad").unwrap(); assert_eq!(opened.as_slice(), b"secret"); let mut alias = public; *alias.last_mut().unwrap() |= 0x80; let mut rng = CountingRng::default(); let result = oc_crypto::seal::seal_xwing(&alias, b"context", b"aad", b"secret", &mut rng); if let Ok(blob) = &result { - assert!(oc_crypto::seal::open_xwing(&secret, blob, b"context", b"aad").is_err(), "control did not reproduce mismatch"); + assert!( + oc_crypto::seal::open_xwing(&secret, blob, b"context", b"aad").is_err(), + "control did not reproduce mismatch" + ); } assert!(matches!(result, Err(CryptoError::BadKey)), "noncanonical X-Wing key accepted"); assert_eq!(rng.draws, 0); @@ -57,11 +69,23 @@ fn zero_chunk_size_is_rejected_before_callbacks_or_rng() { let mut rng = CountingRng::default(); let result = seal_chunks( &PayloadKey::from_bytes([0x31; 32]), - AeadAlg::XChaCha20Poly1305, &[0x42; 16], 0, &mut rng, - |_| { reads.set(reads.get().saturating_add(1)); Ok::<_, core::convert::Infallible>(0) }, - |_| { writes.set(writes.get().saturating_add(1)); Ok(()) }, + AeadAlg::XChaCha20Poly1305, + &[0x42; 16], + 0, + &mut rng, + |_| { + reads.set(reads.get().saturating_add(1)); + Ok::<_, core::convert::Infallible>(0) + }, + |_| { + writes.set(writes.get().saturating_add(1)); + Ok(()) + }, + ); + assert!( + matches!(result, Err(StreamError::Crypto(CryptoError::BadLength))), + "zero capacity accepted" ); - assert!(matches!(result, Err(StreamError::Crypto(CryptoError::BadLength))), "zero capacity accepted"); assert_eq!(reads.get(), 0); assert_eq!(writes.get(), 0); assert_eq!(rng.draws, 0); diff --git a/crates/oc-engine/Cargo.toml b/crates/oc-engine/Cargo.toml index 4e794d4..4c0c508 100644 --- a/crates/oc-engine/Cargo.toml +++ b/crates/oc-engine/Cargo.toml @@ -11,9 +11,9 @@ license = "MPL-2.0" publish.workspace = true [dependencies] -oc-format = { version = "=0.0.4", path = "../oc-format" } -oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } -oc-policy = { version = "=0.0.4", path = "../oc-policy" } +oc-format = { version = "=0.0.5", path = "../oc-format" } +oc-crypto = { version = "=0.0.5", path = "../oc-crypto" } +oc-policy = { version = "=0.0.5", path = "../oc-policy" } # Те же версии, что у `oc-crypto` и `cc-cli`: новых крейтов в дереве ноль. rand_core = { version = "0.10.1", default-features = false } zeroize = { version = "1.9.0", features = ["derive"] } diff --git a/crates/oc-engine/tests/crypto_continuation.rs b/crates/oc-engine/tests/crypto_continuation.rs index c1dcfd4..b3a5d10 100644 --- a/crates/oc-engine/tests/crypto_continuation.rs +++ b/crates/oc-engine/tests/crypto_continuation.rs @@ -1,32 +1,49 @@ // SPDX-License-Identifier: MPL-2.0 #![allow(clippy::unwrap_used, clippy::panic)] -use oc_crypto::{secret::X25519Secret, seal}; +use oc_crypto::{seal, secret::X25519Secret}; use oc_engine::{PackRequest, PublicKeys, Recipient, SealedInfo}; use oc_policy::Policy; struct Fixed; impl rand_core::TryRng for Fixed { type Error = core::convert::Infallible; - fn try_next_u32(&mut self) -> Result { Ok(0x42424242) } - fn try_next_u64(&mut self) -> Result { Ok(0x4242424242424242) } + fn try_next_u32(&mut self) -> Result { + Ok(0x42424242) + } + fn try_next_u64(&mut self) -> Result { + Ok(0x4242424242424242) + } fn try_fill_bytes(&mut self, bytes: &mut [u8]) -> Result<(), Self::Error> { - bytes.fill(0x42); Ok(()) + bytes.fill(0x42); + Ok(()) } } impl rand_core::TryCryptoRng for Fixed {} fn request() -> PackRequest<'static> { - PackRequest { original_name: "private.txt", policy: Policy::deny_all(), - chunk_size: 4096, org_id: b"org".to_vec(), authority_urls: vec![], - recipient: Recipient::None, coauthors: None } + PackRequest { + original_name: "private.txt", + policy: Policy::deny_all(), + chunk_size: 4096, + org_id: b"org".to_vec(), + authority_urls: vec![], + recipient: Recipient::None, + coauthors: None, + } } fn keys() -> PublicKeys<'static> { let public = seal::x25519_public(&X25519Secret::from_bytes([0x31; 32])); - PublicKeys { author: [0x42; 32], authority_sealing: public, - authority_lease_verify: [0x53; 32], device: public, - device_hardware_hybrid: None, device_hybrid: None, device_tpm: None } + PublicKeys { + author: [0x42; 32], + authority_sealing: public, + authority_lease_verify: [0x53; 32], + device: public, + device_hardware_hybrid: None, + device_hybrid: None, + device_tpm: None, + } } #[test] @@ -34,32 +51,60 @@ fn changed_chunk_size_cannot_assemble_a_header_with_a_different_payload_key() { let mut request = request(); let (session, plan) = oc_engine::plan(&request, &mut Fixed); request.chunk_size = 8192; - let result = session.assemble(&request, &keys(), SealedInfo { - total_len: 6, chunk_count: 1, tree_root: [0x64; 32], - }, &mut Fixed); + let result = session.assemble( + &request, + &keys(), + SealedInfo { total_len: 6, chunk_count: 1, tree_root: [0x64; 32] }, + &mut Fixed, + ); if let Ok(assembled) = &result { // The recipient recovers CEK but derives a different payload key from the header. let parsed = oc_format::header::Header::decode(&assembled.header).unwrap(); let header = &parsed.header; let core = oc_format::header::Header::core_hash(&assembled.header, &parsed.spans).unwrap(); - let policy = oc_format::header::Header::policy_hash(&assembled.header, &parsed.spans).unwrap(); - let slot = header.key_slots.iter().find_map(|s| match s { - oc_format::header::KeySlot::Known(s) if s.kind == oc_format::header::SlotKind::AuthorDevice => Some(s), - _ => None, - }).unwrap(); + let policy = + oc_format::header::Header::policy_hash(&assembled.header, &parsed.spans).unwrap(); + let slot = header + .key_slots + .iter() + .find_map(|s| match s { + oc_format::header::KeySlot::Known(s) + if s.kind == oc_format::header::SlotKind::AuthorDevice => + { + Some(s) + } + _ => None, + }) + .unwrap(); let secret = X25519Secret::from_bytes([0x31; 32]); let info = seal::slot_info(oc_crypto::label::SLOT_AUTHOR_DEVICE, slot.kem, &header.file_id); - let opened = seal::open(&secret, &seal::SealedBlob { - enc: slot.enc.clone(), nonce: slot.nonce, ct: slot.ct.clone(), - }, &info, &policy).unwrap(); + let opened = seal::open( + &secret, + &seal::SealedBlob { enc: slot.enc.clone(), nonce: slot.nonce, ct: slot.ct.clone() }, + &info, + &policy, + ) + .unwrap(); let (a, b) = opened.split_at(32); - let kek = oc_crypto::kdf::derive_kek(&header.file_id, &header.org_id, + let kek = oc_crypto::kdf::derive_kek( + &header.file_id, + &header.org_id, &oc_crypto::SecretA::from_bytes(a.try_into().unwrap()), - &oc_crypto::SecretB::from_bytes(b.try_into().unwrap())); - let cek = oc_crypto::wrap::unwrap_cek(&kek, &header.wrapped_cek, &slot.commitment, &core).unwrap(); - let reader_key = oc_crypto::kdf::derive_payload_key(&cek, &header.header_salt, - &header.file_id, header.chunk_size, header.suite.aead); - assert!(reader_key.expose() != plan.payload_key.expose(), "probe did not reproduce key mismatch"); + &oc_crypto::SecretB::from_bytes(b.try_into().unwrap()), + ); + let cek = oc_crypto::wrap::unwrap_cek(&kek, &header.wrapped_cek, &slot.commitment, &core) + .unwrap(); + let reader_key = oc_crypto::kdf::derive_payload_key( + &cek, + &header.header_salt, + &header.file_id, + header.chunk_size, + header.suite.aead, + ); + assert!( + reader_key.expose() != plan.payload_key.expose(), + "probe did not reproduce key mismatch" + ); } assert!(result.is_err(), "assemble accepts metadata inconsistent with the payload KDF"); } @@ -70,7 +115,10 @@ fn pack_request_debug_redacts_private_filename() { let mut other = request(); other.original_name = "another.txt"; assert!(format!("{first:?}") == format!("{other:?}"), "private filename affects Debug"); - assert!(format!("{first:#?}") == format!("{other:#?}"), "private filename affects pretty Debug"); + assert!( + format!("{first:#?}") == format!("{other:#?}"), + "private filename affects pretty Debug" + ); } #[test] @@ -78,7 +126,10 @@ fn editing_piece_debug_redacts_plaintext_and_nonce_seed() { let first = oc_engine::edit::Piece::Seal { plaintext: &[0x51; 32], seed: [0x62; 24] }; let second = oc_engine::edit::Piece::Seal { plaintext: &[0xa7; 32], seed: [0xb8; 24] }; assert!(format!("{first:?}") == format!("{second:?}"), "plaintext or seed affects Debug"); - assert!(format!("{first:#?}") == format!("{second:#?}"), "plaintext or seed affects pretty Debug"); + assert!( + format!("{first:#?}") == format!("{second:#?}"), + "plaintext or seed affects pretty Debug" + ); } #[test] @@ -136,3 +187,33 @@ fn changed_recipient_cannot_silently_seal_to_the_previous_recipient() { assert!(result.is_err(), "assemble silently uses the earlier recipient"); } +#[test] +fn claim_secret_and_recipient_kind_must_match_the_plan() { + let mut planned = request(); + planned.recipient = + Recipient::Claim { secret: oc_crypto::secret::ClaimSecret::from_bytes([0x86; 32]) }; + for (recipient, should_match) in [ + (planned.recipient.clone(), true), + ( + Recipient::Claim { secret: oc_crypto::secret::ClaimSecret::from_bytes([0x97; 32]) }, + false, + ), + (Recipient::None, false), + (Recipient::Identity { public_key: keys().device }, false), + ] { + let (session, _) = oc_engine::plan(&planned, &mut Fixed); + let mut assembled_request = request(); + assembled_request.recipient = recipient; + let result = session.assemble( + &assembled_request, + &keys(), + SealedInfo { total_len: 6, chunk_count: 1, tree_root: [0x64; 32] }, + &mut Fixed, + ); + if should_match { + assert!(result.is_ok(), "unchanged claim must assemble"); + } else { + assert!(result.is_err(), "changed claim or recipient kind must fail before sealing"); + } + } +} diff --git a/crates/oc-format/Cargo.toml b/crates/oc-format/Cargo.toml index 32c7f2f..888f31b 100644 --- a/crates/oc-format/Cargo.toml +++ b/crates/oc-format/Cargo.toml @@ -12,15 +12,15 @@ publish.workspace = true [dependencies] sha2 = "0.11.0" -oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } -oc-policy = { version = "=0.0.4", path = "../oc-policy" } +oc-crypto = { version = "=0.0.5", path = "../oc-crypto" } +oc-policy = { version = "=0.0.5", path = "../oc-policy" } zeroize = "1.9.0" # Программный подписант RSA-PSS — только пробам правки (`tests/edit.rs`); # продуктовая сборка признака не видит (resolver 2 не сливает признаки # dev-зависимостей в обычную сборку). [dev-dependencies] -oc-crypto = { version = "=0.0.4", path = "../oc-crypto", features = ["test-signer"] } +oc-crypto = { version = "=0.0.5", path = "../oc-crypto", features = ["test-signer"] } [lints] workspace = true diff --git a/crates/oc-protocol/Cargo.toml b/crates/oc-protocol/Cargo.toml index 4f90dea..db3c1d8 100644 --- a/crates/oc-protocol/Cargo.toml +++ b/crates/oc-protocol/Cargo.toml @@ -15,9 +15,9 @@ publish.workspace = true # кодек политики и таблица версий живут в `oc-format`. Обратной стрелки нет и # быть не может — ни один разборщик контейнера не зовёт ни один документ # протокола, и это проверено до переноса. -oc-format = { version = "=0.0.4", path = "../oc-format" } -oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } -oc-policy = { version = "=0.0.4", path = "../oc-policy" } +oc-format = { version = "=0.0.5", path = "../oc-format" } +oc-crypto = { version = "=0.0.5", path = "../oc-crypto" } +oc-policy = { version = "=0.0.5", path = "../oc-policy" } [lints] workspace = true diff --git a/crates/opencrate/Cargo.toml b/crates/opencrate/Cargo.toml index ea49f61..e3d203d 100644 --- a/crates/opencrate/Cargo.toml +++ b/crates/opencrate/Cargo.toml @@ -4,7 +4,7 @@ description = "One-dependency entry point for the Open Crate Rust core" repository = "https://github.com/AlexiAxAxA/OpenCrate" homepage = "https://closecrate.com/" readme = "README.md" -version = "0.0.4" +version = "0.0.5" edition.workspace = true rust-version.workspace = true license = "MPL-2.0" @@ -14,12 +14,12 @@ publish.workspace = true app-data = ["dep:opencrate-sdk"] [dependencies] -oc-policy = { version = "=0.0.4", path = "../oc-policy" } -oc-crypto = { version = "=0.0.4", path = "../oc-crypto" } -oc-format = { version = "=0.0.4", path = "../oc-format" } -oc-protocol = { version = "=0.0.4", path = "../oc-protocol" } -oc-engine = { version = "=0.0.4", path = "../oc-engine" } -opencrate-sdk = { version = "=0.0.4", optional = true } +oc-policy = { version = "=0.0.5", path = "../oc-policy" } +oc-crypto = { version = "=0.0.5", path = "../oc-crypto" } +oc-format = { version = "=0.0.5", path = "../oc-format" } +oc-protocol = { version = "=0.0.5", path = "../oc-protocol" } +oc-engine = { version = "=0.0.5", path = "../oc-engine" } +opencrate-sdk = { version = "=0.0.5", optional = true } [[example]] name = "seal-file" diff --git a/crates/opencrate/README.md b/crates/opencrate/README.md index 50477c8..35f3475 100644 --- a/crates/opencrate/README.md +++ b/crates/opencrate/README.md @@ -19,7 +19,7 @@ messages, or the bytes of any file up to 16 MiB, for one X25519 recipient. ```toml [dependencies] -opencrate = { version = "=0.0.4", features = ["app-data"] } +opencrate = { version = "=0.0.5", features = ["app-data"] } ``` ```rust @@ -55,7 +55,7 @@ For `.cc` containers or individual building blocks, use the default facade: ```toml [dependencies] -opencrate = "=0.0.4" +opencrate = "=0.0.5" ``` | Module | Responsibility | @@ -86,7 +86,7 @@ policy. Clone the repository to run them; the first Cargo build may take time. ## License -Version 0.0.4 is licensed under +Version 0.0.5 is licensed under [MPL-2.0](https://github.com/AlexiAxAxA/OpenCrate/blob/main/LICENSE). Commercial use is allowed. Distributed modifications to covered files remain under MPL-2.0; separate application files can use other terms. diff --git a/docs/format.md b/docs/format.md index 3bef515..c5c38e1 100644 --- a/docs/format.md +++ b/docs/format.md @@ -1271,6 +1271,12 @@ checked before DH/KDF. A zero shared secret remains invalid. Low-level the Seal context. K10 formulas, valid output and frozen KAT/golden artifacts remain unchanged. +For `kem_id = 4`, X-Wing Seal also requires the recipient's trailing classical +X25519 coordinate to be canonical before encapsulation and RNG use. The +recipient reconstructs that public key for the combiner and K10 context; an +alias would produce ciphertext it cannot open. This is a Seal boundary check, +not a change to the raw X-Wing primitive or its frozen combiner. + | # | Purpose | Function | salt | ikm | info | bytes | |---|---|---|---|---|---|---| | K1 | file KEK | HKDF-SHA256 | `file_id` | `secret_A‖secret_B` | `"CC/v1/kek"‖org_id‖file_id` | 32 | diff --git a/docs/getting-started.md b/docs/getting-started.md index b65b0cf..d34e348 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -102,11 +102,11 @@ reproducible builds. ## Choose the libraries for your integration The five libraries and the `opencrate` facade are published to crates.io at -`0.0.4`. To use the full core, add one dependency: +`0.0.5`. To use the full core, add one dependency: ```toml [dependencies] -opencrate = "=0.0.4" +opencrate = "=0.0.5" ``` The facade re-exports the libraries as `opencrate::{crypto, engine, format, @@ -115,11 +115,11 @@ individually instead: ```toml [dependencies] -oc-format = "=0.0.4" -oc-crypto = "=0.0.4" -oc-protocol = "=0.0.4" -oc-policy = "=0.0.4" -oc-engine = "=0.0.4" +oc-format = "=0.0.5" +oc-crypto = "=0.0.5" +oc-protocol = "=0.0.5" +oc-policy = "=0.0.5" +oc-engine = "=0.0.5" ``` To seal arbitrary application bytes or a small file without a `.cc` container, @@ -130,7 +130,7 @@ extension up to 16 MiB; it does not save a key or envelope for later use. ```toml [dependencies] -opencrate = { version = "=0.0.4", features = ["app-data"] } +opencrate = { version = "=0.0.5", features = ["app-data"] } ``` For persistent application data, follow the diff --git a/docs/index.md b/docs/index.md index b207d4c..cca26b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,5 +42,5 @@ key protection and enforcement. See [Translation notes](translation-notes.md) for preserved literal examples and how to read the dated specification history. -See [the 0.0.4 review](review-2026-09-26.md) for reproduced defects, checks and +See [the 0.0.5 review](review-2026-09-26.md) for reproduced defects, checks and the scope of the readability changes. diff --git a/docs/review-2026-09-26.md b/docs/review-2026-09-26.md index eb441da..a14d533 100644 --- a/docs/review-2026-09-26.md +++ b/docs/review-2026-09-26.md @@ -45,14 +45,15 @@ The follow-up for 0.0.5 also fixes: - Streaming encryption rejects zero chunk capacity before callbacks or RNG use. - Header assembly rejects a chunk size or recipient different from the plan; otherwise it can derive a different payload key or seal to the old recipient. + A matching claim succeeds; changed claim secrets or recipient kinds fail. - X-Wing Seal requires a canonical classical recipient coordinate before RNG use. The raw X-Wing primitive and its frozen combiner remain unchanged. -Ten new regressions fail against the published 0.0.4 source and pass after +Eleven new regressions fail against the published 0.0.4 source and pass after these fixes. Two private allocator checks reproduce OAEP residue with a synthetic modulus and a real RSA key in registry 0.0.4, and pass after the fix. An arithmetic compatibility test covers RSA-2048, RSA-3072 and RSA-4096. -The updated workspace passes 692 tests, with 15 existing ignored tests; +The updated workspace passes 693 tests, with 15 existing ignored tests; Clippy, Rustdoc and the pure-core WASM build pass. Frozen artifacts are unchanged. The review used separate public source checkouts. Native tests include existing From 1dd0d0813c9c5caaa5c2b1b6c9532a00b50bd1c2 Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 12:54:07 +0300 Subject: [PATCH 09/10] Shorten crypto manifest notes and clarify secret OAEP input --- EXPORT-INVENTORY.json | 2 +- crates/oc-crypto/Cargo.toml | 93 +++++-------------------------------- 2 files changed, 12 insertions(+), 83 deletions(-) diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 8b0cb75..8443052 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -146,7 +146,7 @@ "README.md": "2c77e6daec8d8eb62a3d42a7ae407e15b3eca6698f745dcbd4446b16f41154a1", "SECURITY.md": "839c254e62ea1cfa4628520ca6d1e145c5fc1f6b109ab4f2f9c711b3db26d930", "clippy.toml": "447c1c2ae9dc0dfef9bae35ca82cb04f08c09bf7943d3835823138b546c03621", - "crates/oc-crypto/Cargo.toml": "63a4268cf85daca35746b15080aee8b51fd703e59ae3a08ed76cf479f7970804", + "crates/oc-crypto/Cargo.toml": "811cc93fb4f8bfbc3f844ef6e09c201dd4ea941907f8ef3399281fae21dd4a1e", "crates/oc-crypto/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-crypto/README.md": "9384c17fef50f17bbf95132c02b63c2a412c7515842f4f9b3d37a4539e1b12cf", "crates/oc-crypto/src/aead.rs": "7ec9b62343d04b441965a598309122593e4b910e193346acfcfa363cdece4207", diff --git a/crates/oc-crypto/Cargo.toml b/crates/oc-crypto/Cargo.toml index 65759d0..c0bfbd3 100644 --- a/crates/oc-crypto/Cargo.toml +++ b/crates/oc-crypto/Cargo.toml @@ -10,107 +10,36 @@ rust-version.workspace = true license = "MPL-2.0" publish.workspace = true -# `default-features = false` везде, где дефолт тянет getrandom. -# -# Это не оптимизация размера, а инвариант крейта. Правило «ни случайности из -# воздуха, ни часов» держалось на дисциплине вызывающего: сам crate источник -# энтропии не использовал, но ЗАВИСЕЛ от него транзитивно — `rand_core` со -# своими дефолтами и `chacha20poly1305` через фичу `getrandom` у `aead`. -# Поймала это сборка под wasm32-unknown-unknown, где `getrandom` без фичи -# `wasm_js` отказывается компилироваться: до тех пор чистота была заявлением, а -# не свойством. -# -# Практическое следствие помимо wasm: пока источник энтропии в графе есть, любой -# будущий вызов вроде `OsRng` компилируется молча, и ошибку заметит только ревью. -# Без него — ошибка сборки. +# Callers supply randomness; dependency defaults must not add an OS RNG. [dependencies] blake3 = "1.8.6" chacha20poly1305 = { version = "0.11.0", default-features = false, features = ["alloc"] } -# Большие числа для проверки RSA-PSS (`rsa.rs`) — подписи редактировавшего -# устройства, версия 3 формата. -# -# Обоснование измерено, а не предположено: крейт в дереве УЖЕ был, пришёл с -# `p256`, и объявление его прямой зависимостью не добавило ни одного нового -# крейта — только рёбра. `cargo deny check licenses bans advisories sources` — -# чисто. -# -# Почему не крейт `rsa`: на нём RUSTSEC-2023-0071 (Marvin). К нашему употреблению -# он относится косвенно — атака про приватные операции, а подписывает TPM, — но -# исключение пришлось бы выписывать руками. Нам же нужно возведение в степень с -# ОТКРЫТЫМ показателем плюс разбор набивки: секретов в операции нет вовсе. -# -# `default-features = false` снимает `rand_core` — источник энтропии в этом -# крейте запрещён. Остаётся `alloc` ради `BoxedUint`. +# Public-exponent RSA arithmetic. OAEP encoded input is secret and uses +# fixed-width values wrapped in Zeroizing; signature verification uses public input. crypto-bigint = { version = "0.7.5", default-features = false, features = ["alloc"] } ed25519-dalek = { version = "3.0.0", default-features = false, features = ["zeroize"] } hkdf = "0.13.0" hmac = "0.13.0" -# P-256 нужен потому, что Microsoft Platform Crypto Provider не даёт X25519: -# TPM 2.0 предоставляет ECDH P-256 и RSA. Чистая реализация здесь — не дубль -# аппаратной, а её обязательная пара: без неё согласование за трейтом -# `KeyAgreement` нечем проверить в CI, и весь путь к слоту на P-256 стал бы -# тестируемым только на машине с TPM. -# -# `default-features = false` снимает `std` (а с ним `getrandom` — источник -# энтропии ОС, который в этом крейте запрещён и однажды уже протекал сюда -# транзитивно), `ecdsa` (подписи здесь Ed25519, вторая подписная схема не нужна), -# `pem` и `precomputed-tables`. Остаётся `ecdh` — ровно согласование ключей. +# Software ECDH counterpart for hardware P-256 agreement. p256 = { version = "0.14.0", default-features = false, features = ["ecdh", "alloc"] } -# Только трейты `Rng`/`CryptoRng`: генератор приходит параметром, а дефолтные -# фичи подключили бы ещё и `OsRng`. +# RNG traits only; the caller supplies the implementation. rand_core = { version = "0.10.1", default-features = false } -# Постквантовая половина гибридного слота (`kem_id = 4`, X-Wing). Обоснование — -# три замера 2026-09-08, `spikes/ml-kem-cost/`: `cargo deny` на настоящем дереве -# чист, официальные векторы ACVP FIPS 203 сходятся (390 сравнений), цена ниже -# нынешнего X25519-слота. Пять новых крейтов на весь гибрид. -# -# `default-features = false` снимает `alloc`: ключи и шифротексты здесь -# фиксированной длины, куча им не нужна. `zeroize` ОБЯЗАТЕЛЕН — без него -# приватная половина остаётся на стеке как есть, а затирающий аллокатор стека -# не касается. +# Fixed-size hybrid keys; zeroize private state on drop. ml-kem = { version = "0.3.2", default-features = false, features = ["zeroize"] } sha2 = "0.11.0" -# SHA3-256 для комбинатора X-Wing и SHAKE256 для роста пары из семени. Новых -# крейтов не даёт: тот же `sha3`, что приходит с `ml-kem`. +# X-Wing combiner and seed expansion. sha3 = { version = "0.11.0", default-features = false } subtle = "2.6.1" -# `zeroize` ОБЯЗАТЕЛЕН: без него `Drop` у `StaticSecret` и `SharedSecret` пуст, и -# копия долговременного секрета устройства, сделанная для `diffie_hellman`, -# оставалась на стеке как есть — затирающий аллокатор стека не касается -# (ревью 2026-09-06). Новых крейтов фича не тянет: `zeroize` уже в графе. +# Wipe StaticSecret and SharedSecret when dropped. x25519-dalek = { version = "3.0.0", default-features = false, features = ["static_secrets", "zeroize"] } zeroize = { version = "1.9.0", features = ["derive"] } [features] -# Программный подписант RSA-PSS с зашитыми тестовыми ключами (`rsa_test_signer`). -# Только для `[dev-dependencies]` и стенда: продуктовый ключ правки — в TPM. -# Сторож — `cc-cli/tests/repository_hygiene.rs`. +# Fixture signer with embedded test keys; never use for real identities. test-signer = [] -# ОПАСНЫЕ ФОРМЫ ЗАПЕЧАТЫВАНИЯ: `aead::seal_chunk` и `aead::seal_metadata`, куда -# nonce передаётся аргументом. -# -# Нужны они ровно двоим — замороженным векторам и пробам, которым требуется тот -# самый nonce, что записан в артефакте. Продуктовый путь их не зовёт: там стоят -# `seal_chunk_hedged` и `seal_metadata_hedged`, куда nonce вывести можно, а -# передать нечем. Повтор nonce на одном ключе раскрывает оба открытых текста -# XOR-ом и позволяет подделать тег (И-1), и удержать это может только подпись -# функции, а не абзац в докстроке. -# -# Только для `[dev-dependencies]`: признак выключен по умолчанию, и в списке -# `default` любого манифеста его ловит сторож -# `cc-cli/tests/repository_hygiene.rs`. +# Test-only nonce control. Reusing a nonce under one key breaks AEAD security. explicit-nonce = [] -# МЕТКА ДОМЕНА ВНЕ РЕЕСТРА: `label::Label::ad_hoc`. -# -# Нужен двоим прототипам вне дерева (`experiments/attested-release`, -# `spikes/disclosure-capsules`), каждый из которых подписывает своё утверждение в -# своём домене и потому не вправе занимать номер в нормативном реестре §3.6. -# -# Продуктовый путь его не зовёт и звать не может: весь смысл типа `Label` в том, -# что значение берётся только из реестра, а реестр сверяется со спекой байт в -# байт (И-12). Включённый в поставке признак возвращает дверь, ради закрытия -# которой тип заведён, — поэтому он выключен по умолчанию и внесён в список -# непоставляемых (`cc-cli/tests/repository_hygiene.rs`). +# Experimental labels outside the protocol registry; disabled by default. ad-hoc-label = [] [lints] From 50fb8d208c517014027a4c62f1390f613b099f24 Mon Sep 17 00:00:00 2001 From: AlexiAxAxA Date: Sat, 26 Sep 2026 13:38:43 +0300 Subject: [PATCH 10/10] Shorten source and test comments while preserving contracts --- EXPORT-INVENTORY.json | 118 +++---- crates/oc-crypto/src/aead.rs | 134 ++------ crates/oc-crypto/src/agreement.rs | 56 +--- crates/oc-crypto/src/kdf.rs | 26 +- crates/oc-crypto/src/lib.rs | 314 ++++-------------- crates/oc-crypto/src/merkle.rs | 19 +- crates/oc-crypto/src/mlkem_p256.rs | 40 +-- crates/oc-crypto/src/rsa.rs | 20 +- crates/oc-crypto/src/rsa_test_signer.rs | 24 +- crates/oc-crypto/src/seal.rs | 128 ++----- crates/oc-crypto/src/secret.rs | 50 +-- crates/oc-crypto/src/sign.rs | 48 +-- crates/oc-crypto/src/tpm.rs | 26 +- crates/oc-crypto/src/wrap.rs | 41 +-- crates/oc-crypto/tests/kat.rs | 68 +--- .../oc-crypto/tests/measure_packing_path.rs | 30 +- .../tests/security_probe_keyschedule.rs | 65 +--- crates/oc-crypto/tests/security_probe_seal.rs | 73 +--- .../security_probe_secretbuf_and_shape.rs | 33 +- .../tests/security_probe_tree_signatures.rs | 37 +-- crates/oc-crypto/tests/spec_labels.rs | 47 +-- crates/oc-crypto/tests/zeroize_on_drop.rs | 15 +- crates/oc-engine/src/edit.rs | 23 +- crates/oc-engine/src/lib.rs | 71 +--- crates/oc-engine/src/wire.rs | 20 +- .../oc-engine/tests/hardware_hybrid_golden.rs | 38 +-- crates/oc-engine/tests/p256_author_golden.rs | 17 +- crates/oc-format/src/content.rs | 61 +--- crates/oc-format/src/edit.rs | 18 +- crates/oc-format/src/encoding.rs | 18 +- crates/oc-format/src/frame.rs | 22 +- crates/oc-format/src/header.rs | 149 ++------- crates/oc-format/src/lib.rs | 51 +-- crates/oc-format/src/tlv.rs | 35 +- crates/oc-format/src/verify.rs | 106 ++---- crates/oc-format/tests/kat_policy.rs | 44 +-- .../tests/security_probe_hostile_input.rs | 119 ++----- .../tests/skeptic_recheck_slot_tags.rs | 100 +----- crates/oc-policy/src/lib.rs | 213 ++---------- crates/oc-protocol/src/access.rs | 105 +----- crates/oc-protocol/src/action.rs | 11 +- crates/oc-protocol/src/activation.rs | 116 ++----- crates/oc-protocol/src/agent.rs | 88 ++--- crates/oc-protocol/src/attestation.rs | 32 +- crates/oc-protocol/src/attribute_rule.rs | 37 +-- crates/oc-protocol/src/control.rs | 141 ++------ crates/oc-protocol/src/directory.rs | 38 +-- crates/oc-protocol/src/lease.rs | 44 +-- crates/oc-protocol/src/order.rs | 101 ++---- crates/oc-protocol/src/replica.rs | 35 +- crates/oc-protocol/src/revocation.rs | 26 +- crates/oc-protocol/src/standing.rs | 80 +---- crates/oc-protocol/src/unknown.rs | 8 +- crates/oc-protocol/src/witness.rs | 27 +- crates/oc-protocol/tests/action_vectors.rs | 22 +- crates/oc-protocol/tests/agent_vectors.rs | 16 +- crates/oc-protocol/tests/control_vectors.rs | 16 +- crates/oc-protocol/tests/session_mac_table.rs | 19 +- crates/oc-protocol/tests/unknown_tags.rs | 64 +--- docs/review-2026-09-26.md | 7 + 60 files changed, 765 insertions(+), 2785 deletions(-) diff --git a/EXPORT-INVENTORY.json b/EXPORT-INVENTORY.json index 8443052..364ee68 100644 --- a/EXPORT-INVENTORY.json +++ b/EXPORT-INVENTORY.json @@ -149,100 +149,100 @@ "crates/oc-crypto/Cargo.toml": "811cc93fb4f8bfbc3f844ef6e09c201dd4ea941907f8ef3399281fae21dd4a1e", "crates/oc-crypto/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-crypto/README.md": "9384c17fef50f17bbf95132c02b63c2a412c7515842f4f9b3d37a4539e1b12cf", - "crates/oc-crypto/src/aead.rs": "7ec9b62343d04b441965a598309122593e4b910e193346acfcfa363cdece4207", - "crates/oc-crypto/src/agreement.rs": "dfc0e652f6ff4653fd66c413b2eed08b084134a09e2b10c7ceb4b156ffcc7a49", - "crates/oc-crypto/src/kdf.rs": "589b16690fdc8b8901676831609c8b1813072120a71c97a57601382c443eb3a1", - "crates/oc-crypto/src/lib.rs": "a79823c2e5a733606a8416848a803bcfd17c6fb9457b485fd885ab3bfc0c1bbf", + "crates/oc-crypto/src/aead.rs": "553c0f2b26ecf37df0a6419f14bbb76ef3c3b5f5fc66dd5332c6cff5bdbd8e5b", + "crates/oc-crypto/src/agreement.rs": "fc3d9167a774bec319949c7ded185535d97de9819098018bdf64210c7a22720c", + "crates/oc-crypto/src/kdf.rs": "4831a285c6cab01d1d950781ebb58b1f62aa39a66bb3cb16ca2c610b5b6846d2", + "crates/oc-crypto/src/lib.rs": "6550651f48705f44ed0c21753212381e54af9da7ecdfbb51e88a768993f4e3a1", "crates/oc-crypto/src/mac.rs": "75d6636bfbcbe9d7f9cd307ce5101d9d15e2023553038f42fe90a08efeb08edb", - "crates/oc-crypto/src/merkle.rs": "63e117f274790f36943e7d0e69897241049ab102c14bcae958d1f80941fe93d2", - "crates/oc-crypto/src/mlkem_p256.rs": "f5a77672884ab20e2278d255915cba99e827e9bb5b5f20dca5e84b778513a1f6", - "crates/oc-crypto/src/rsa.rs": "220290a344521af93ccb6306997a998a44e97296dc3fc84140fcad22365ff0a2", - "crates/oc-crypto/src/rsa_test_signer.rs": "c34082bb4d6cd0df456a31eb4af37ca9721b8dc56162a9d6821cf3fcb6fd7f93", - "crates/oc-crypto/src/seal.rs": "765dc5710e9c5330884468ef5499ffaeab5b72eddf17fa5ad9cfacbbbb720371", - "crates/oc-crypto/src/secret.rs": "55bff2d293719d34fcb2de7780a4dce7d81c0c0420b5c5da3b6b2dda83bcb9dd", - "crates/oc-crypto/src/sign.rs": "e231b7a66a86039c1a4e9830bbb3470250a23f046dccc70f1a6c260fc1ef54db", + "crates/oc-crypto/src/merkle.rs": "61068d84ac869c470a4d278cd9d737ab0c254f128db71c7935fffdc9e07c8edf", + "crates/oc-crypto/src/mlkem_p256.rs": "2f4eaf94aad3c0ad564b29f3643acf2c22fda63940b80b4b9e3b2db892e86c64", + "crates/oc-crypto/src/rsa.rs": "7324f90880456b3c7c1db27b3226e7070bedfa5dadf50d9a1c04461efa2990b5", + "crates/oc-crypto/src/rsa_test_signer.rs": "eeba874c081112b7ba6be13706a2d0da30679d264c0fa0f79f839908d3607367", + "crates/oc-crypto/src/seal.rs": "30fe89484bcfb0592b3436fdc7bc7c921b8145d1dd84230d110a7eee41b93567", + "crates/oc-crypto/src/secret.rs": "3244daa5267545f8bb225891edd7a169bdb7a60b89c2bee9795519b98dd459e1", + "crates/oc-crypto/src/sign.rs": "4afb1b4b6b78f4529084388c739a549f3ef1d329db9644e073ca18acf3e3baf4", "crates/oc-crypto/src/stream.rs": "26ea721afd4165e2ff994d90b2896e24bc89463bb463cdb655da5ccc13ac75a3", - "crates/oc-crypto/src/tpm.rs": "73a0c2aa437bf92b67d4967fcb4ff463b77801b8690db9a47b19fc0bef8022b1", + "crates/oc-crypto/src/tpm.rs": "182d3b7e8385a7d03bf22a8f4aef586e9a7387a357046f96cc47d6bf6aec5993", "crates/oc-crypto/src/transcript.rs": "54ed996ac650c3b5a4aa0e804508603981ef3a596b2e9fd52d24953657a733af", - "crates/oc-crypto/src/wrap.rs": "99138975a39b1f6c671a88a1756defe9edf8a17b6f601d2d3ee00b8dbdaf06f2", + "crates/oc-crypto/src/wrap.rs": "6ee30a615b8e36333034aa08814a2dfd5405ea84554ad1353ddced3f718c7d6e", "crates/oc-crypto/src/xwing.rs": "139884797ecbdbe12d22425812e775a182ccf7b1d7d12f488982137f3539c449", "crates/oc-crypto/tests/crypto_continuation.rs": "12810d3ef380c34612aeb366d4b3f53b591526d6dee38c6bf1f205899507b22e", - "crates/oc-crypto/tests/kat.rs": "0dc188ad01491e554049b078cfb3647b4b51de5e57715949d178bd10ff562b2a", - "crates/oc-crypto/tests/measure_packing_path.rs": "085b9a74b7931fe970b30263851026c2cbbf8495898638a0861d4c3d07d99a18", + "crates/oc-crypto/tests/kat.rs": "dcea596aca4ae4a1c1fd28dcd37980a826e4e6d38afe70a230db5e748846214b", + "crates/oc-crypto/tests/measure_packing_path.rs": "eb964f1dce92b0c445fc7516fd8c6b8fe77e7b17a7479b2a43b9d14d66675663", "crates/oc-crypto/tests/review_regressions.rs": "84880dde33ab776afbe214bbb48db00898ce0c30aae5c0dc10feb8ce99e969d5", - "crates/oc-crypto/tests/security_probe_keyschedule.rs": "1eb129e068b2138b2a0fd0465a62866f66e842a2bb4440ea0fd1c7b469e3d859", - "crates/oc-crypto/tests/security_probe_seal.rs": "31380327c7d0af0f9133c1ec264422ca5c5d123167fb87b2661b029e21e799bd", - "crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs": "7d619a7845d35b33426cbbc70441c1f4622742073c4a0bafdde27c2f50ac520e", - "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "44ee682d809c6211311e89c6193652adccb432b5e2cc2feabd5f94c1377c09ca", - "crates/oc-crypto/tests/spec_labels.rs": "c5a15f4792ffa62c1f49d7b08aa8ef95c13d28b0a795575155e98f6af65022e4", - "crates/oc-crypto/tests/zeroize_on_drop.rs": "6049affea1696a0235a3f47685d7126c9e022ca61ca3000d4abcffa6e1c4dc9e", + "crates/oc-crypto/tests/security_probe_keyschedule.rs": "5ae98604f1e2ad4c4565dcbf507c3050efaf1a7a9512431e464d457030e5dd86", + "crates/oc-crypto/tests/security_probe_seal.rs": "eaeaf8e0afcf3bf21ff6ae220e3cc556042aee540e5f79bc5ba4cf06b7d2b896", + "crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs": "19f866580154c52857de035e9de23fc41ad3461c54ba195f9a8c0665e4b2b3e7", + "crates/oc-crypto/tests/security_probe_tree_signatures.rs": "c89444bee9cae604d786f91846b43f6d99ef7edbe1ce41e2d988e081eec9ff89", + "crates/oc-crypto/tests/spec_labels.rs": "99899f2d4363a88a946189318830ace7e007c24467074205572ed61ea81afe0f", + "crates/oc-crypto/tests/zeroize_on_drop.rs": "5bf2790183cb6204539ba43a355977f2848593afa5f44e9b1fa6f9b56b60b4f9", "crates/oc-engine/Cargo.toml": "fb2dcba7b5f16832438dd49e71bb8efc52eff9c1b73de5375614f4c9292c47ae", "crates/oc-engine/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-engine/README.md": "1eece36c00aecf16e31c12ce8c7cdbaa1f7b8299b435879e73f6cfec86bc42b2", - "crates/oc-engine/src/edit.rs": "150bb013fc322a0d422f00f41067d37e9a1c3a3ec2282819a531be1daf4db1ba", - "crates/oc-engine/src/lib.rs": "344ecc1714d74fb37e62d04b5c4f14f5d81ed70aaaae20bbb89adedda5245b0b", - "crates/oc-engine/src/wire.rs": "a04810360f419dcff5441dbf38ed0bd52d4bd0feff8d5539af4793c88d847cbb", + "crates/oc-engine/src/edit.rs": "b4e64ff63729524d4c06be0bb07062f1e7f97fa14082855d9e279648a8d8631d", + "crates/oc-engine/src/lib.rs": "923c836be0b548a1f23a4bd301ccf04219377b7daa3ce81a7055dc09fa759d25", + "crates/oc-engine/src/wire.rs": "2d8775ef1ed7977eb1832a5815c453ac8256a78ad5c79bca764e61dca5a8eb7a", "crates/oc-engine/tests/crypto_continuation.rs": "cb4e3c6599b286e6f652d34273f8f2b598f4a07ca2cdfabc970f9fce1d2d09ef", "crates/oc-engine/tests/edit_writer.rs": "8d8d77313772bf184677c79757322073ab29bcca203154050dd0a856dd055c1c", - "crates/oc-engine/tests/hardware_hybrid_golden.rs": "cec004a062ab838a2acfbe634f17c54cda1053a14337b57c51d2711e4b2033b7", - "crates/oc-engine/tests/p256_author_golden.rs": "6f20067edbc1259cd981e10347c4e1df4f80af6935456b01f42a7e246451f947", + "crates/oc-engine/tests/hardware_hybrid_golden.rs": "4b065b6f605f3e9ae028105b7bf6d7852cd1c10ee0432f136549b24f63040121", + "crates/oc-engine/tests/p256_author_golden.rs": "c81c61533dde75df0a3f196e59ba4ebbe8ed00820901d182246ce40b9e77c3ea", "crates/oc-format/Cargo.toml": "b8e98c566e068f81729e8003777016d6522a9aaeddecb8294fb64c31ede13737", "crates/oc-format/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-format/README.md": "54e78a93669649329fa9b68022c84bd69df7751fca007941b60f2ea0e1b16b2c", "crates/oc-format/examples/verify-header.rs": "5c9ca98e85e1aade70875f9a935ae34618f94737e1b14336dba756d8fd62d59c", - "crates/oc-format/src/content.rs": "c0d32050f9d31ce913fa62bb9b82457612b46f20cf6fca2e98f5f1d6a7977b66", - "crates/oc-format/src/edit.rs": "ebb9edc8f5da165e72bef91cc6d269e1b4422abd79e076485429374291712348", - "crates/oc-format/src/encoding.rs": "f38e4708305c9007888b7bc155af891bd6b03157ae1f768c2204765b150f46f5", + "crates/oc-format/src/content.rs": "aed073d64a274e0d93672dd3938b56500b22777d27e43b351e998e9fecfe85ad", + "crates/oc-format/src/edit.rs": "f95420e63c8f61f75bf477aa846bb0265997af0a6fdfdf043e366795613afec8", + "crates/oc-format/src/encoding.rs": "a5e59b210ffb906ae5267180f6d7f5fa855fbcfe096b049239b270c9e479ab80", "crates/oc-format/src/footer.rs": "6c1728344dde540085c9cec9955fbf8579147e50f5832c2df654eca8ea15b141", - "crates/oc-format/src/frame.rs": "dc2638063a297d61586181f507665f03e027fa1a8c3ce5616cda8cbe95418c87", - "crates/oc-format/src/header.rs": "f64e47093f3730edf22d36e7d734bd7827f23d5f2b3bf4a7013925816a0f7624", - "crates/oc-format/src/lib.rs": "2eb89b68917cbdf605f6d0d3252dedf82ae2eeb2f45cc8eda4d26c8d83cd28db", + "crates/oc-format/src/frame.rs": "37222b8614c51bf416baeb7e09b53744c7dc4485eca1ac12556ecede13f7606c", + "crates/oc-format/src/header.rs": "3a7e07d8a34f170316ab1da16044edaf849ebe59a79c4fa4f3d90569d969f9f2", + "crates/oc-format/src/lib.rs": "637e6d653727fc35aa84e3c3ab852529595d65322b9e3c3e275124dbecaba8a2", "crates/oc-format/src/policy_codec.rs": "ad498c2c177ac76d64818e94c1e0eb708947400273c4d2f3f7f35158ced3a25b", "crates/oc-format/src/text.rs": "2c84dc2ec2a0e9d57ee52f1478401753fc3798a5d6ff15ccaacaf53d0bc27d2a", - "crates/oc-format/src/tlv.rs": "39a033337bafaedd69961254d4dbe4642656201aed85e69855c3e8663ad169dc", - "crates/oc-format/src/verify.rs": "48fb0398d98131167192eeb0b0edbde55eaaeab9fb603030aeda9854a4da08a5", + "crates/oc-format/src/tlv.rs": "769bffb358af16a4624ce2c34c1417e7a1c415f8148e82176829baad6995a574", + "crates/oc-format/src/verify.rs": "4cd075a3e23f33071b2c25fb7ba75f326f81ecde604a50679151ac7a11d80830", "crates/oc-format/tests/crypto_continuation.rs": "8183106d9f0a0e9bf5345171cd4ac63e09bdc768dbf6923c08a1d02cae213b26", "crates/oc-format/tests/edit.rs": "2e840455d87c97a1dc90145a048711a656211a4baef30b8e883ac6a96b642b2b", - "crates/oc-format/tests/kat_policy.rs": "bb04d93c27a5c86f7fa533dbf224b801378229875d5fcc147cffd1c049a6a5c4", + "crates/oc-format/tests/kat_policy.rs": "6b1cf1c956e9e0738b2b14e6280aa08ce73b68ee4f4055e7538a87b71b365658", "crates/oc-format/tests/parser_robustness.rs": "5c53c6053bd3322fa11d6c184305907cc03f9b23ef463dac842b55a96b9f1ecf", - "crates/oc-format/tests/security_probe_hostile_input.rs": "3bb25839ccdbe063a866cb2828088e82591c9d52234209d0a774a5347e270bb6", + "crates/oc-format/tests/security_probe_hostile_input.rs": "3139caf9d068942bf13c3838a78e31196eca56dbc6fbaea1b6a5d2297959a14d", "crates/oc-format/tests/security_probe_policy_enforcement.rs": "7806a2a6a5aca6f287f6d58dc2b0c2be5e397e806587913bbd22264599322628", - "crates/oc-format/tests/skeptic_recheck_slot_tags.rs": "706524f0927ea9759b2c9e88b350bf2287a679dd7609f9bda428b8918a6f440b", + "crates/oc-format/tests/skeptic_recheck_slot_tags.rs": "482ba0a288a543e58206c6181bea7253a06181645be40da6b49e780bc25d44eb", "crates/oc-policy/Cargo.toml": "2136eb1e010d9b5f7846d91e9f580036647e5659e7071cea2207448cd92d9a86", "crates/oc-policy/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-policy/README.md": "b832b57b1da0d89897ced3a9e4ae0580d2dd9aef2d84e0f96bcae1fc07f1913d", "crates/oc-policy/examples/access-policy.rs": "4a06f17c144d0f6c0eecb35cb166131c2f8755c333825567139852b7b23a9208", - "crates/oc-policy/src/lib.rs": "743eaf0550615071ef98ffab4b42c271981332fe19ad2290e20f1e21dea0ade4", + "crates/oc-policy/src/lib.rs": "55570391090e59bf4e432d60d04b36647c7da2f74e7d701ff41f867d12cf22f9", "crates/oc-policy/tests/security_probe_skeptic.rs": "c60f720854250afc356466c6a3d5227ee80e50a41d28d0e3c53a580082d6f769", "crates/oc-policy/tests/server_policy_composition.rs": "c7069b4151bb0660e03050b249fe0f9dcafab44ed3bb3db70ad05977308bb87d", "crates/oc-protocol/Cargo.toml": "4cea4c593ab2c4b733e4859b0b8ba7ee46f6796db86404be36a9b90e5b4b5b14", "crates/oc-protocol/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/oc-protocol/README.md": "42ca1744fee5ac93b7bbdfcafcd016cf19c427ca208ae2491e3351d65a107c42", - "crates/oc-protocol/src/access.rs": "5a846d5285cd6beb48b5bb753a5ae836912580c8d91dc35f7a5f8e03324c37eb", - "crates/oc-protocol/src/action.rs": "1620404953a33fa6b3f636ad4eabf0a211f94c3c9be1c383a178514d85fb949e", - "crates/oc-protocol/src/activation.rs": "08b870f5ee24831ebbe02e9ada50e84cd66fe44dd58d98a4ce8113935fbb9da7", - "crates/oc-protocol/src/agent.rs": "539989796d3f1526a65ee91d1aa3bd852de4dce569369ffe1dbc3508d92b5856", - "crates/oc-protocol/src/attestation.rs": "02d1464d33f65dc97b7852dfde2866cfcae8dc13d9c073815c66beeef2530709", - "crates/oc-protocol/src/attribute_rule.rs": "d7de24657fd3226037304bb1940d6813528f52afb69544afdc3d15756da82309", - "crates/oc-protocol/src/control.rs": "b4fbeb59ac45c1ad3d4e6251691b8debc02aa7ca0bc838e95549e262d00b2db0", - "crates/oc-protocol/src/directory.rs": "980855f4c0d8eae8b0adca595c04150375583845be0b6392984c472b4a7dfc65", - "crates/oc-protocol/src/lease.rs": "def55db5224208f47aa79d517040bd54d127df7bf6128c310fe662ff064c8987", + "crates/oc-protocol/src/access.rs": "9caecad71790996b1b831a134da3b4ee9d54cbc965d33490acf10c35117d2284", + "crates/oc-protocol/src/action.rs": "95da2ce6ee9e6a391d49c70dd1195b976fa1eb2e950504a87391eb330c60e40b", + "crates/oc-protocol/src/activation.rs": "1c3db0f6628b928b1d666f44b2ee5f76cd9d51ce6d63de7f5640e557f6c98b6f", + "crates/oc-protocol/src/agent.rs": "4efa8ec3abed588b287c1e003fb9e64c10419a69797f5417e8e89d0f6c8b02e1", + "crates/oc-protocol/src/attestation.rs": "f9b52563e022e96293e27ea4dd6f736c69feaf1b7f04dbcc28a479f6e4cf6f09", + "crates/oc-protocol/src/attribute_rule.rs": "7dc5a978be4b437d202760b1b5d5863cb0bee716b511cc6bdcbe6b4f2210a270", + "crates/oc-protocol/src/control.rs": "c44b1a45619f8c1c4a0305cd44c4591144d7c1cae745f893798476dbfdf0557e", + "crates/oc-protocol/src/directory.rs": "d38707f7f164b706192788cf3cb0740b8975205a8e5b0ff1e5e2ecbcb606c3b3", + "crates/oc-protocol/src/lease.rs": "f4ce7e581f0439e72b58d7a7a54006e19c6d9d92be2f035c2f4aae44dda86cea", "crates/oc-protocol/src/lib.rs": "d3021e2dda152298f31faed1769e32ab8139937a0edcfe1f0fad450a816e7c79", - "crates/oc-protocol/src/order.rs": "aaaad74887af649c627e4accca124a1d88e4f2f46c43ff69d8b351a01632a413", - "crates/oc-protocol/src/replica.rs": "40a14190e291ebff1feb95a005736de2d7529abd27ce09edca36c4e672d8d26a", - "crates/oc-protocol/src/revocation.rs": "95c9fca669f223fd145d6d4f4a918c11161def38961d4744107ee97424a7fa4d", - "crates/oc-protocol/src/standing.rs": "3c98c330382769c869fab6ac40c23d77cfc0c4dc362662c06bfbee68879ee1f1", - "crates/oc-protocol/src/unknown.rs": "5a8d7006089d636f315b752a1a67109337cfcb5b00a9824fab140af0064d18c8", - "crates/oc-protocol/src/witness.rs": "bf712ea8812e6f53a56e4e81b58a04f0734edd5da799859e9056f26c46393cff", - "crates/oc-protocol/tests/action_vectors.rs": "27901e8ea9f23ac2f4727d11c90b89e638534a6249474f6ca4a653b643b0080c", - "crates/oc-protocol/tests/agent_vectors.rs": "0764c3650b0b92f05706f9483c34a3b9a1c55abadbf8845b8b733b1c7c944f81", - "crates/oc-protocol/tests/control_vectors.rs": "86249ac910fcf4e9d42962f4d6916fcfcb545e016bc27420dada35b4bf8f1a2a", + "crates/oc-protocol/src/order.rs": "40d6a3900f690be2e5ca216cc7974d1c89c4915d1c8fbb498e7b5abcc1749dff", + "crates/oc-protocol/src/replica.rs": "4f8ce8755dd4e49fa7da9849ed0ced41973814ae0d927aa237aff8b19b7305cc", + "crates/oc-protocol/src/revocation.rs": "82db163595052e13c5cfc3bd63c74bf176b06601d132b806c86d77a9cb6c78cf", + "crates/oc-protocol/src/standing.rs": "8ae0980c6f6c73660c7146dce61207815ee28477e0caad2adeb41a3386215967", + "crates/oc-protocol/src/unknown.rs": "e8e2d3fcb9e8873e9d615c1011f23fd023f70a5ae8c7f0e1807461538aeac294", + "crates/oc-protocol/src/witness.rs": "24dabd23432b566d1a6eb075e6f4b793ed2245d1894a7f7dec80e95050ca836a", + "crates/oc-protocol/tests/action_vectors.rs": "efada0443af609c7e83ef7e7abe3342ca769530756377d0564916079bf6cb996", + "crates/oc-protocol/tests/agent_vectors.rs": "847f4c12d1c7a71c616340c807853aa4747dc4587ca04fc9c20fa0499b8590b1", + "crates/oc-protocol/tests/control_vectors.rs": "a1d0a7d1b552be0926d24f21f8d4a2b9b905e01f8306ecc1f925906b0c6fccd0", "crates/oc-protocol/tests/kat_operation.rs": "5ce9366e992ff1a62cc681e7d98439dbf8d5dae5d14812351233a0657a71ff0c", "crates/oc-protocol/tests/lease_attestation.rs": "009f06a398e64be2a5a85d09baba559a1c9e396a6bdafb64edc38a322980007e", "crates/oc-protocol/tests/lease_server_policy.rs": "3dd4d56dcba4f847e6f1e629fa9bf925a1d6982efb62574bfb38df57a62da257", "crates/oc-protocol/tests/lease_signature.rs": "f8f9de0fd92d546f34cdbf3dd036e664e682d4f40aa7f6f907e680a6d81ea37a", - "crates/oc-protocol/tests/session_mac_table.rs": "67d28231e53eb1bcde0e75b3703a62715aab24f9870aa0bcfe7eaad5a7103296", - "crates/oc-protocol/tests/unknown_tags.rs": "ddb5efc2c168b863c8b176bb327098bc657c9324ffb92700d98df1854c85d85f", + "crates/oc-protocol/tests/session_mac_table.rs": "d06b0cac6735660bf1b73d7a9c3cf10c0ee9fe743c4c3566b1d809140456b148", + "crates/oc-protocol/tests/unknown_tags.rs": "8d63878eae708d97d078b640a92f35d3bd1a2d8a0554cad41b8daee72c39bc8b", "crates/opencrate/Cargo.toml": "f16a1cf6c3e69b12072b9cd54d926ad3014b6678a39f5b05be5909f851c82f19", "crates/opencrate/LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", "crates/opencrate/README.md": "eeae58841fa2c5b905455150f8adb5a8e2d51b1b29d41377080e40c260326def", @@ -262,7 +262,7 @@ "docs/index.md": "fbfee7867d4f4f19c286aaf117dc7d70ff25e334f69616a9683c6e406260ca55", "docs/licensing.md": "82d00091b4c173fe02e506fc67821dd6fea96324bf408e6f6a7f8d93243b3ea3", "docs/protocol.md": "ec582526e61914ee6251ba2188d6680d3aa88aacaf2056d49ee6a463ce1e6990", - "docs/review-2026-09-26.md": "7bca98756b86ffb3e5df5ad86f21685533c126f34f4031b5a0a4e3e99a4106fe", + "docs/review-2026-09-26.md": "11714521edb2c6a7cc73d963cc79db60e60640b006a9c74d80937505ecc0d9ef", "docs/translation-notes.md": "c290d69d83f42cd1f9d9311e32b8ad2b08391a94856e68a0b4993dfa1d103065", "examples/README.md": "7a95174a360ec6527fcb86cef5a78c011e4281010bde5ddfac8fc280f7194738", "rust-toolchain.toml": "41308dbe82a83679b7b2081be4d5ae7f9aa52f642bcc3eab104ac77501560e79", diff --git a/crates/oc-crypto/src/aead.rs b/crates/oc-crypto/src/aead.rs index 2f87d04..b17c31f 100644 --- a/crates/oc-crypto/src/aead.rs +++ b/crates/oc-crypto/src/aead.rs @@ -92,21 +92,11 @@ pub fn metadata_aad(file_id: &[u8; 16]) -> [u8; META_AAD_LEN] { aad } -/// Whether this build supports encryption with the declared AEAD profile. +/// Whether this build supports the AEAD profile. /// -/// A second boundary for `aead_id`, symmetric to [`crate::merkle::ensure_supported`] -/// for the tree hash. Without it, an identifier in a signed header parsed -/// successfully, with rejection only at the first chunk, after signature -/// verification, slot parsing, key agreement, and CEK unwrapping. Parsing a number and -/// being able to execute it are different things; both must be resolved together, -/// at parsing time. -/// -/// AES profiles are declared by the format (docs/format.md §6.1), but their crates are -/// not included: the AES-GCM counter nonce does not fit a 24-byte argument and -/// requires its own path, not a branch in `match`. -/// -/// The `match` deliberately has no `_`: adding a member to [`AeadAlg`] must break -/// the build here, beside the cipher, rather than pass silently. +/// Header parsing checks support before key agreement and chunk processing. +/// AES profiles need a separate nonce path and are not implemented here. +/// The exhaustive match requires an explicit decision for each new profile. pub fn ensure_supported(alg: AeadAlg) -> Result<(), CryptoError> { match alg { AeadAlg::XChaCha20Poly1305 => Ok(()), @@ -142,19 +132,11 @@ fn seal_with( } } -/// The sole AEAD profile selection point for CHUNK decryption. -/// -/// Decrypts in place: `buffer` contains ciphertext on input and plaintext -/// of the same length on output; the tag is a separate argument. +/// Decrypt a chunk in place using the selected AEAD profile. /// -/// Exists to prevent plaintext from acquiring its own heap copy: -/// see the detailed explanation in [`open_chunk_inner`]. Rejection looks -/// identical for a substituted chunk, another file, and a corrupted byte, for the -/// same reason as in [`open_with`]. -/// -/// The contents of `buffer` on failure are unspecified and must not be relied -/// on: an implementation may leave partially decrypted bytes there. -/// The [`open_chunk`] wrapper guarantees wiping, structurally. +/// The input buffer holds ciphertext; successful output has the same length. +/// The tag is supplied separately. Failure may leave partially decrypted bytes; +/// [`open_chunk`] wipes the buffer before returning an error. fn open_in_place( key: &[u8; 32], alg: AeadAlg, @@ -196,27 +178,11 @@ fn open_with( } } -/// Seal a chunk, DERIVING the nonce internally, and return it with the tree leaf. -/// -/// # Why this function exists at all -/// -/// Because it physically prevents passing a nonce, which is exactly what is required -/// when a chunk is REWRITTEN. Nonce reuse under one key is fatal (I-1): -/// two different plaintexts under one keystream are exposed through XOR, and -/// reuse of the one-time Poly1305 key enables tag forgery. Yet the most natural -/// approach when editing a document, "keep the nonce already in the frame", -/// leads directly to that failure. +/// Seal a chunk with an internally derived nonce and return its tree leaf. /// -/// Rewriting is safe today as a CONSEQUENCE of hedging, rather than by API design: -/// the nonce derives from the seed and plaintext, so a different plaintext -/// naturally produces a different nonce. The property is real, but depends on the caller -/// not getting clever. A comment cannot enforce it; only a function -/// signature with no room for a nonce can. -/// -/// The seed is a parameter: this crate has no RNG and must not have one -/// (I-1 requires stored nonces, not reader-derived ones, but the SENDER derives them -/// from a random seed PLUS plaintext, so that repeating -/// RNG state after snapshot rollback does not repeat the nonce). +/// The caller supplies a random seed, not a nonce. Hedging binds the plaintext, +/// so changed plaintext gets a different nonce even after RNG rollback. +/// Reusing a nonce under one key would expose plaintexts and permit tag forgery. /// /// # Errors /// Returns [`CryptoError`] on encryption failure or an incorrect output length. @@ -235,28 +201,11 @@ pub fn seal_chunk_hedged( Ok((nonce, leaf)) } -/// Encrypt a chunk, appending `ciphertext ‖ tag` to `out`, and return the tree leaf. -/// -/// `out` is cleared before writing. -/// -/// # The nonce is an argument here: this is the DANGEROUS form -/// -/// Retained for frozen vectors and probes needing precisely the -/// nonce recorded in the artifact. Production paths do not use it: they use -/// [`seal_chunk_hedged`], where a nonce can be derived but cannot be supplied. +/// Encrypt a chunk, appending `ciphertext ‖ tag` to `out`, and return its leaf. /// -/// If you are writing a new call unrelated to KATs, this is not the function you need. -/// Rewriting a chunk with the nonce already in its frame destroys confidentiality -/// of both plaintexts at once (I-1). -/// -/// # Why a feature gate rather than just a doc-comment warning -/// -/// Because documentation is not a boundary. While production code can see the function, -/// the "dangerous form" relies on the next caller reading the paragraph above; -/// `explicit-nonce` is disabled by default and enabled ONLY through -/// `[dev-dependencies]`, so a production build cannot see this function -/// at all: the call does not compile. A boundary visible to the compiler cannot -/// be overlooked in a hurry. +/// Clears `out` before writing. The `explicit-nonce` feature exposes this form +/// for frozen vectors and probes; normal callers use [`seal_chunk_hedged`]. +/// Never reuse a supplied nonce under the same key, including when editing. #[cfg(any(test, feature = "explicit-nonce"))] pub fn seal_chunk( key: &PayloadKey, @@ -342,19 +291,9 @@ fn open_chunk_inner( .ok_or(CryptoError::BadLength)?; let aad = chunk_aad(file_id, index, alg); - // Расшифровка идёт НА МЕСТЕ, в буфере вызывающего, и это не оптимизация. - // - // Раньше здесь выделялся `Zeroizing>` под открытый текст, и он - // затирался при уничтожении — то есть защищал от того, что блок достанется - // аллокатору читаемым. От второй беды он не защищал вовсе: блок обычной кучи - // может уехать в файл подкачки, пока живёт. Просмотрщик запирает свою память - // (`VirtualLock`) именно затем, чтобы этого не случилось, и промежуточная - // копия в незапертой куче сводила бы всю меру на нет — на каждый чанк. - // - // Порядок операций здесь единственно возможный: сначала объявить длину, потом - // скопировать шифротекст, потом расшифровать. Затирать буфер перед копией - // нельзя не по соображениям скорости — `as_declared_mut` не затирает - // намеренно: стереть буфер после копии значило бы стереть вход. + // Decrypt in the caller's buffer to avoid an extra plaintext heap copy. + // Declare the length, copy ciphertext, then decrypt. Wiping after the copy + // would erase the input; `as_declared_mut` therefore does not wipe it. let room = out.as_capacity_mut().get_mut(..ct.len()).ok_or(CryptoError::BadLength)?; room.copy_from_slice(ct); out.declare_len(ct.len())?; @@ -366,23 +305,11 @@ fn open_chunk_inner( Ok(leaf_of(index, nonce, tag, ct)) } -/// Seal private metadata, DERIVING the nonce internally, and return it alongside. -/// -/// # Why this function exists +/// Seal private metadata with an internally derived nonce. /// -/// For the same reason as [`seal_chunk_hedged`]. It was added later not -/// because metadata is safer: hedging existed here, but the CALLER assembled it; -/// the engine derived a nonce itself and passed it here ready-made. While -/// that derivation step is external, it can be skipped, and the function signature -/// cannot prevent that: it accepts any 24 bytes. Reuse here is especially -/// costly: `CEK` and `header_salt` come from the same RNG, so VM snapshot -/// rollback would repeat both the K5 key and nonce, while plaintexts -/// (the name and size of another document) would differ. -/// -/// The seed is a parameter: this crate has no RNG and must not have one. -/// -/// Returns `(nonce, ciphertext)`: the nonce is not secret, but the block cannot -/// be decrypted without it, and it is stored ready-made in the file (I-1). +/// The caller supplies a random seed. Hedging also binds plaintext, preventing +/// nonce reuse for changed metadata when RNG state is repeated. +/// Returns `(nonce, ciphertext)`; store the nonce with the encrypted metadata. /// /// # Errors /// Returns [`CryptoError`] on nonce derivation or encryption failure. @@ -703,19 +630,8 @@ mod tests { } } - /// ONE SEED FOR TWO DIFFERENT PLAINTEXTS PRODUCES DIFFERENT NONCES. - /// - /// # What this test protects - /// - /// Chunk rewriting, which does not exist yet. When it arrives (phase 6), - /// the most natural approach will be "take the nonce already in the - /// frame", which would destroy confidentiality of both plaintexts at once: - /// one keystream for two different plaintexts exposes them through XOR, and - /// the one-time Poly1305 key permits tag forgery (I-1). - /// - /// This checks that protection is a PROPERTY OF DERIVATION rather than - /// caller discipline: even with exactly the same seed, as after - /// VM snapshot rollback, a different plaintext yields a different nonce. + /// Changed plaintext must produce a different nonce even with the same seed. + /// This covers chunk rewriting and repeated RNG state after snapshot rollback. #[test] fn the_same_seed_still_yields_different_nonces_for_different_plaintexts() { let seed = [0x33u8; NONCE_LEN]; diff --git a/crates/oc-crypto/src/agreement.rs b/crates/oc-crypto/src/agreement.rs index 16786e4..ab816d7 100644 --- a/crates/oc-crypto/src/agreement.rs +++ b/crates/oc-crypto/src/agreement.rs @@ -1,16 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 -//! Key agreement behind a trait: X25519 in software, P-256 in software or in a TPM. +//! Software and hardware key agreement behind one trait. //! -//! This module exists for one property: the private key may never leave -//! hardware. `NCryptSecretAgreement` returns a shared secret, not the key, so -//! deriving an AEAD key by "taking the private key and computing DH" is fundamentally impossible: -//! key agreement must be an extension point rather than an implementation detail -//! of sealing. That is why the sealing construction is implemented manually following -//! RFC 9180 rather than using an off-the-shelf HPKE crate: that crate requires the key. -//! -//! The crate remains pure. This contains only the abstraction and software -//! implementations; TPM support lives in `cc-keystore`, which depends on this trait, -//! not the reverse. +//! A provider can perform agreement without releasing its private key to Seal. +//! Software X25519/P-256 live here; platform TPM adapters implement the trait +//! outside this pure crate. use zeroize::Zeroizing; @@ -24,23 +17,11 @@ use crate::CryptoError; /// turning this constant into a function of the mechanism. pub const SHARED_SECRET_LEN: usize = 32; -/// The key-agreement shared secret, in **big-endian** order. -/// -/// ## Why there is one constructor and why it names the byte order -/// -/// The `spikes/tpm-ecdh` spike discovered something easily overlooked: -/// `NCryptSecretAgreement` with Microsoft Platform Crypto Provider returns the shared -/// secret in **little-endian** order, whereas all pure P-256 implementations, including -/// `p256`, use the big-endian X-coordinate representation, as prescribed by RFC 5903 (ECDH -/// for IKE). -/// -/// Byte order left to a comment is a bug waiting to happen: -/// a reversed secret yields a different AEAD key; the slot will not open, appearing -/// as "file corrupted". The error would be found, but not at its actual source. +/// Shared secret bytes, with an explicit big-endian constructor. /// -/// There is therefore **exactly one** constructor, and its name states the byte order. -/// An implementation receiving NCrypt bytes must reverse them to invoke it -/// truthfully; this cannot be silently forgotten, since the type has no other entry point. +/// P-256 providers supply the big-endian X coordinate. Microsoft Platform +/// Crypto Provider returns NCrypt agreement bytes in little-endian order; +/// its adapter must reverse them before calling the constructor. #[derive(Clone)] pub struct SharedSecret(Zeroizing<[u8; SHARED_SECRET_LEN]>); @@ -302,24 +283,9 @@ mod tests { assert!(x.agree(&[0x00; 65]).is_err(), "X25519 принял 65 байт"); } - /// `agree` ACCEPTS compressed points; the compressed-form prohibition belongs elsewhere. - /// - /// The neighboring probe previously claimed the opposite, "P-256 accepted compressed - /// form", while feeding `agree` thirty-three bytes of `0x04`. That is not a compressed - /// point: compressed points start with `0x02` or `0x03`; `0x04` denotes uncompressed form. - /// Rejection came from SEC1 parsing, so the probe passed for an unrelated - /// reason, while `agree` does not have the property it claimed at all: - /// `from_sec1_bytes` parses both forms, and both yield the same secret. - /// - /// The specification's requirement (§3.3: `enc` for `kem_id = 2` is exactly 65 - /// bytes, an uncompressed point) is enforced by the EXACT LENGTH table in `oc-format`. - /// This separation is deliberate and documented on - /// [`KeyAgreement::agree`]: wire form is checked by the format, not the key-agreement - /// trait. Otherwise the hardware implementation would have to duplicate - /// the length table, and two copies of one table would diverge. - /// - /// The probe records this layer's actual behavior rather than a desired one; the verifiable - /// claim about compressed form lives in `oc-format`. + /// Low-level P-256 agreement accepts compressed and uncompressed SEC1 points. + /// Both represent the same point and yield the same secret. Seal separately + /// requires the canonical 65-byte uncompressed form for its wire/KDF context. #[test] fn a_compressed_p256_point_is_accepted_here_and_yields_the_same_secret() { use p256::elliptic_curve::sec1::ToSec1Point as _; diff --git a/crates/oc-crypto/src/kdf.rs b/crates/oc-crypto/src/kdf.rs index 7df4690..e845fd6 100644 --- a/crates/oc-crypto/src/kdf.rs +++ b/crates/oc-crypto/src/kdf.rs @@ -23,20 +23,11 @@ use zeroize::{Zeroize, Zeroizing}; /// implementation detail. const KEK_IKM_LEN: usize = 64; -/// Expand HKDF-SHA256 into a buffer whose length is specified by its type. +/// Expand HKDF-SHA256 into a self-wiping fixed-size array. /// -/// `expand` fails in only one case: requesting more than 255×32 -/// bytes; here the array type specifies output length, at most 32 bytes, -/// so the error branch is unreachable. K1…K8 therefore return a key rather than `Result`: -/// threading an impossible variant through every key-schedule call would -/// train callers to use `?` where nothing can fail. -/// Returns a wiping wrapper rather than a bare array. -/// -/// This function outputs key material. A bare `[u8; N]` remains on the stack until -/// the calling function ends and can enter the pagefile with it, without an owner -/// to wipe it on destruction. The wrapper makes wiping -/// a property of the type: previously every caller had to remember it individually, -/// and not all did. +/// Current callers request at most 32 bytes, below HKDF's 255×32-byte limit, +/// so expansion cannot fail for those sizes. The wrapper wipes the owned +/// key material when dropped. fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[&[u8]]) -> Zeroizing<[u8; N]> { let hk = Hkdf::::new(Some(salt), ikm); let mut okm = Zeroizing::new([0u8; N]); @@ -397,12 +388,9 @@ pub fn derive_witness_key(device: &crate::secret::X25519Secret) -> MacKey { /// "CC/v1/mark-choice" ‖ layout ‖ u64be(copy mark) ‖ u32be(point))`, /// the first eight bytes interpreted as `u64be`, modulo the variant count. /// -/// The key is a SEPARATE organization marking key, not a device or -/// author key: selection must be unpredictable for the recipient and reproducible for -/// whoever investigates a leak; this key serves no other purpose. The layout is -/// MAC-covered: one copy mark in two documents yields independent choices. -/// For up to sixteen variants, modulo bias is on the order of 2^-60 and -/// requires no correction. +/// Use a separate organization marking key. The MAC-covered layout binds +/// selection to the document. For up to sixteen variants, modulo bias is +/// on the order of 2^-60. /// /// # Errors /// [`CryptoError::BadLength`]: zero variants. diff --git a/crates/oc-crypto/src/lib.rs b/crates/oc-crypto/src/lib.rs index 2bba09f..a974a06 100644 --- a/crates/oc-crypto/src/lib.rs +++ b/crates/oc-crypto/src/lib.rs @@ -132,23 +132,13 @@ impl AeadAlg { pub enum SigAlg { /// Author signature. The only one this build executes. Ed25519 = 1, - /// Editing-device signature, format version 3: RSA-PSS-SHA256, - /// MGF1-SHA256, 32-byte salt, exponent 65537. - /// - /// Chosen for its failure mode, not taste: ECDSA consumes an ephemeral `k` for - /// every signature, and signing two different messages with the same `k` reveals - /// the private key by arithmetic, exactly what virtual-machine snapshot - /// rollback causes. With PSS, repeated salt yields one extra valid signature and - /// nothing more. See `docs/format.md`, "VERSION 3 OPENED", item 7. - /// - /// Verifier: [`rsa::verify_pss_sha256`], in pure Rust: `oc-format` - /// verifies the signature and must build for - /// `wasm32-unknown-unknown`. - /// - /// Used **only by mutable-region tag 6**. This number is invalid in `suite.sig_alg`: - /// that field specifies the AUTHOR signature, frozen in version 1 as - /// Ed25519. Header parsing checks placement: `ensure_supported` - /// answers "can we execute it", not "does it belong here". + /// Editing-device signature: RSA-PSS-SHA256, MGF1-SHA256, + /// 32-byte salt and exponent 65537 (format version 3). + /// + /// PSS salt repetition does not disclose the signing key, unlike ECDSA + /// nonce reuse. Verification is [`rsa::verify_pss_sha256`] and builds for WASM. + /// This identifier is valid only for mutable-region tag 6; the author's + /// `suite.sig_alg` remains Ed25519. RsaPssSha256 = 2, } @@ -219,44 +209,22 @@ pub enum KemAlg { } impl KemAlg { - /// Whether the build can execute the declared mechanism: ONE name for this question. - /// - /// The name was not introduced for neatness. "Is this mechanism executable?" - /// was answered separately by the fingerprint table ([`kdf::device_fpr`]), header length - /// tables, and the client's share-B issuance branch; the answers agreed - /// only through the editor's memory. Such a set can diverge exactly once: - /// in the direction of "somewhere a mechanism this build cannot execute was considered - /// executable". + /// Reject a KEM this build cannot execute. /// - /// The answer comes from [`seal::supports_kem`] rather than being duplicated here: the `match` - /// without `_` must sit BESIDE THE IMPLEMENTATION, so adding a [`KemAlg`] - /// member breaks the build at the code responsible for executing it. This is - /// the `Result` form of the same answer, for callers needing rejection rather than `bool`, - /// following [`SigAlg::ensure_supported`]. + /// Delegates to [`seal::supports_kem`], keeping the support table beside + /// the implementations. This is its `Result` form for callers needing an error. /// /// # Errors - /// [`CryptoError::UnsupportedAlgorithm`]: the registry has the number but the build - /// lacks the mechanism. + /// [`CryptoError::UnsupportedAlgorithm`] for a registered but unsupported KEM. pub fn ensure_supported(self) -> Result<(), CryptoError> { if seal::supports_kem(self) { Ok(()) } else { Err(CryptoError::UnsupportedAlgorithm) } } - /// Parse an identifier from a file. - /// - /// # Why this has NO second boundary, unlike its neighbors + /// Parse a registered KEM identifier. /// - /// [`AeadAlg::from_u8`], [`SigAlg::from_u8`], and [`TreeHashAlg::from_u8`] - /// call `ensure_supported` during parsing: an unimplemented header algorithm - /// must reject the FILE, the earlier the better. For `kem_id`, the consequence - /// differs normatively: `docs/format.md` §3.3 and §3.5 require - /// SKIPPING a slot with an unimplemented or unknown mechanism rather than rejecting - /// the file; a usable slot for this recipient may be adjacent. - /// Rejection built in here would move "skip or reject" from - /// the caller's level into number parsing, which knows nothing - /// about slots. - /// - /// The boundary therefore remains a separate [`Self::ensure_supported`] call, - /// while parsing answers only "is this number in the registry?". + /// An unsupported or unknown slot is skipped rather than rejecting the file + /// (format §3.3/§3.5), because another slot may be usable. Check + /// [`Self::ensure_supported`] separately before executing the mechanism. pub fn from_u8(v: u8) -> Result { match v { 1 => Ok(Self::X25519HkdfSha256), @@ -307,30 +275,11 @@ impl TreeHashAlg { /// None is used twice across the system. The only route into /// a signature is through [`Transcript`], whose constructor requires a label. pub mod label { - /// A domain label: a value that CANNOT be invented. - /// - /// # Why a type where `&'static [u8]` used to suffice - /// - /// I-12 requires unique, prefix-free labels, guarded by - /// four probes: uniqueness, prefix-freeness, versioning, and agreement with - /// specification §3.6. All four inspect [`ALL`], the REGISTRY. They never saw - /// call sites: `Transcript::new` and `seal::slot_info` - /// accepted arbitrary bytes, and a caller could pass - /// `b"CC/v1/lease-cache"`, a string absent from the registry and extending - /// [`LEASE`]. No probe would detect that, because it would - /// check the list rather than the call. - /// - /// The new type closes exactly this gap: `Label` values come ONLY - /// from this module's constants because the constructor is private and the field - /// is not public. The registry's guarantee becomes a guarantee of every call, - /// checked by the compiler rather than a probe. - /// - /// # Why `Debug` prints the string itself - /// - /// A label is not secret: it is plaintext in every file and in the specification. - /// I-11 forbids secrets in `Debug`, not domain names; hiding - /// the label would blind signature-failure debugging without the slightest - /// benefit. + /// Domain label from the protocol registry. + /// + /// The private constructor restricts values to this module's constants. + /// [`ALL`] is checked for uniqueness, prefix-freeness and agreement with §3.6. + /// Labels are public, so Debug shows their text for diagnosing domain mismatches. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Label(&'static [u8]); @@ -397,26 +346,12 @@ pub mod label { pub const HEADER_SIG: Label = Label::new(b"CC/v1/header-sig"); pub const REVOCATION: Label = Label::new(b"CC/v1/revocation"); pub const GRANT: Label = Label::new(b"CC/v1/grant"); - /// Author signature on an AGENT GRANT: door, expiry, depth, and shares B - /// for the subtree (`oc_protocol::agent::AgentGrant`, Agent Protocol, stage 1). - /// - /// Its own label, not [`GRANT`], for good reason: `"CC/v1/grant"` marks - /// author approval of ONE request for one file - /// (`oc_protocol::access::decision_transcript`), whereas an agent grant distributes shares - /// for an entire tree and names the key with which the door signs delegations. - /// If the domains coincided, approval of one file would also serve as a signature - /// for distributing the entire subtree. - /// - /// Prefix-free: `"CC/v1/grant"` is not its prefix (seventh byte `a` - /// versus `g`); its nearest `a` neighbors, `"CC/v1/activate-req"`, - /// `"CC/v1/audit-entry"`, `"CC/v1/audit-head"`, `"CC/v1/attest-nonce"`, - /// `"CC/v1/attest-qualify"`, `"CC/v1/author-order"`, - /// `"CC/v1/authority-binding"`, and `"CC/v1/authority-transfer"`, differ - /// at byte eight (`g` versus `c`, `u`, `t`). - /// - /// Does not affect the container: it changes no format version and enters no - /// header. It is registered because prefix-freeness can be proved - /// only here (I-12). + /// Author signature on an agent grant: door, expiry, depth and subtree shares B + /// (`oc_protocol::agent::AgentGrant`, Agent Protocol stage 1). + /// + /// Separate from [`GRANT`], which approves access to one file. Sharing a signing + /// key does not make the two statements interchangeable; their labels remain + /// prefix-free and do not change the container layout. pub const AGENT_GRANT: Label = Label::new(b"CC/v1/agent-grant"); /// Parent-door signature on a DELEGATION to a child /// (`oc_protocol::agent::Delegation`, same source). @@ -431,57 +366,23 @@ pub mod label { /// at byte eight (`e` versus `e`/`i`; for `device-fpr`, at byte nine, /// `l` versus `v`). pub const DELEGATION: Label = Label::new(b"CC/v1/delegation"); - /// Author signature on an ACTION GRANT: which actions, subject to which - /// constraints, the door may request from the server - /// (`oc_protocol::action::ActionGrant`, Agent Protocol, stage 2, - /// `docs/agent-protocol/stage-2-actions.md` §4.1). - /// - /// Distinct from [`AGENT_GRANT`], for the same reason separating an agent grant from - /// [`GRANT`]: an agent grant distributes shares B for READING a subtree; an action - /// grant distributes permission to ACT outside the sandbox: push a branch, - /// delete a file, contact the outside. If domains matched, a signature granting - /// read access would grant actions too: an author opening a directory - /// to an agent would silently grant it `git push` as well. - /// - /// Prefix-free: among `a` labels, the closest prefix is `"CC/v1/activate-req"`, - /// which differs at the fifth byte of the name (`o` versus `v`: - /// `action` versus `activate`); its neighbor [`ACTION_LEASE`] shares only - /// `"CC/v1/action-"`, followed by `g` versus `l`. Neither extends the other. - /// - /// Does not affect the container: it changes no format version, enters no header, - /// and none of its bytes occur in `.cc`. It is registered - /// because prefix-freeness can be proved only here (I-12). + /// Author signature on an action grant: permitted actions and constraints + /// (`oc_protocol::action::ActionGrant`, Agent Protocol stage 2, §4.1). + /// + /// Separate from [`AGENT_GRANT`] and [`GRANT`]: permission to execute an action + /// must not be accepted as permission to distribute file shares, or vice versa. pub const ACTION_GRANT: Label = Label::new(b"CC/v1/action-grant"); - /// SERVER signature on a single-use action lease - /// (`oc_protocol::action::ActionLease`, same source, §4.3). - /// - /// The key is the same one the server uses to sign file leases - /// (`authority.lease_verify_key` from the header), but the label is distinct for a - /// reason: a file lease permits OPENING a file; an action lease permits - /// EXECUTING an action with specified arguments. If domains matched, one - /// signed document could replace the other under the same key, reducing - /// the distinction between "read" and "push a branch" to how the recipient - /// interprets the bytes. - /// - /// Prefix-free with [`LEASE`] (`"CC/v1/lease"` is not its prefix) and - /// [`ACTION_GRANT`] (see there). + /// Server signature on a single-use action lease + /// (`oc_protocol::action::ActionLease`, Agent Protocol stage 2, §4.3). + /// + /// Uses the file lease-signing key with a distinct, prefix-free domain. + /// A file lease authorizes opening; this document authorizes a specific action. pub const ACTION_LEASE: Label = Label::new(b"CC/v1/action-lease"); - /// AUTHOR signature on a decision concerning an action-execution request - /// (`oc_protocol::action::ActionDecision`, stage 2, §5 step 3). - /// - /// Distinct from [`GRANT`], which signs decisions about ACCESS requests - /// (`oc_protocol::access::decision_transcript`). Both use the same key, - /// from the header and recorded by the server at file registration; - /// only the label separates the domains. If they coincided, two different author - /// statements would share one signature: "issue this device the file's share B" - /// and "let this door execute `git push` to this branch". Differing tag - /// numbers cannot be relied on to separate them: both bodies use - /// TLV, and matching numbers are a matter of time, not construction. - /// - /// Prefix-free: with [`ACTION_GRANT`] and [`ACTION_LEASE`] it shares only - /// `"CC/v1/action-"`, then `d` versus `g` and `l`; no registry label - /// starts with `"CC/v1/action-d"`. Like both neighbors, it does not affect - /// the container: it changes no format version and enters no header. + /// Author signature on an action-execution decision + /// (`oc_protocol::action::ActionDecision`, Agent Protocol stage 2, §5). + /// + /// Separate from access decisions under [`GRANT`], although both use the author + /// key. The label distinguishes the statements independently of their TLV tags. pub const ACTION_DECISION: Label = Label::new(b"CC/v1/action-decision"); pub const LEASE: Label = Label::new(b"CC/v1/lease"); pub const ACTIVATE_REQ: Label = Label::new(b"CC/v1/activate-req"); @@ -567,24 +468,11 @@ pub mod label { pub const PAYLOAD: Label = Label::new(b"CC/v1/payload"); pub const NONCE_BASE: Label = Label::new(b"CC/v1/nonce-base"); pub const PRIVATE_META: Label = Label::new(b"CC/v1/private-meta"); - /// DEVICE keypair derived from a claim code. - /// - /// # Why a third code-related label was needed when two already existed - /// - /// `SLOT_B_CLAIM` derives share B directly from the code: that is how a - /// `RecipientClaim` slot works, correctly in that case: the recipient's share can be - /// anything, provided both parties derive the same value. - /// - /// For a code-based heir the share is FIXED: this file's share B, stored - /// in the author slot. It cannot be derived from an arbitrary code: derivation produces - /// what it produces. The code therefore derives a KEYPAIR, not a share, and the bequest - /// is sealed to its public key with ordinary `seal`, just as for any - /// device. No new primitives: the same HKDF, the same X25519, the - /// same `seal`. + /// Device keypair derived from a claim code. /// - /// The label is separate and must remain so: using `SLOT_B_CLAIM` with the same - /// `ikm` would yield a private key equal to the slot share, so a code - /// opening one file would reveal the key used to sign another. + /// A code-based heir receives the file's existing share B sealed to this keypair. + /// This domain stays separate from `SLOT_B_CLAIM`, which derives a share directly; + /// reusing that derivation would make the private key equal to the slot share. pub const CLAIM_DEVICE: Label = Label::new(b"CC/v1/claim-device"); pub const SLOT_B_CLAIM: Label = Label::new(b"CC/v1/slot-b-claim"); pub const SLOT_B_COMMIT: Label = Label::new(b"CC/v1/slot-b-commit"); @@ -616,17 +504,9 @@ pub mod label { pub const WRAP_NONCE: Label = Label::new(b"CC/v1/wrap-nonce"); /// Payload-frame nonce hedging (§6.1). /// - /// The fourth label serving this purpose; its later appearance was not about - /// completeness: decision C-13 was applied to slot sealing and CEK wrapping, - /// while two nonces, frame and private metadata, still took bytes directly - /// from the RNG. The same hole, simply in less conspicuous places. - /// - /// Deliberately **not** `"CC/v1/chunk-nonce"`: that string would extend the - /// [`CHUNK`] label, and labels also prefix HKDF `info`, where no - /// zero byte separates them: `"CC/v1/chunk"‖"-nonce"‖X` would equal - /// `"CC/v1/chunk-nonce"‖X`. Exactly the same case as [`CACHED_LEASE`], caught - /// by the same prefix-freeness test. Hence "frame" rather than "chunk": - /// the nonce belongs to the on-disk frame, not the logical chunk. + /// The label must not extend [`CHUNK`]: labels prefix HKDF `info` without a + /// separator, so `"CC/v1/chunk"‖"-nonce"‖X` would collide with + /// `"CC/v1/chunk-nonce"‖X`. The registry checks prefix-freeness. pub const FRAME_NONCE: Label = Label::new(b"CC/v1/frame-nonce"); /// Private-metadata nonce hedging (§2.0). /// @@ -678,21 +558,11 @@ pub mod label { pub const PROVE_ECHO: Label = Label::new(b"CC/v1/prove-echo"); /// Request MAC key after proof (K24). pub const SESSION_MAC: Label = Label::new(b"CC/v1/session-mac"); - /// Conversation-bound proof-of-possession echo (K31, - /// `docs/protocol.md` §9.4, decision 2026-09-21). - /// - /// One label for both steps of ONE derivation: it labels the handshake - /// transcript and also separates the HMAC domain whose key is the challenge - /// secret. There is no second domain, only "echo over transcript"; - /// the label in the HMAC message prevents an echo colliding with K23 under - /// the same key. - /// - /// **The name deliberately does not extend `"CC/v1/prove-echo"`**: this set is - /// prefix-free, and `"CC/v1/prove-echo-bound"` would extend an already - /// occupied label, exactly what I-12 forbids. The nearest `e` neighbors, - /// `"CC/v1/editor-sig"`, `"CC/v1/editor-cert"`, `"CC/v1/edit-session"`, - /// and `"CC/v1/edition-claim"`, differ at byte eight (`c` versus - /// `d`). + /// Conversation-bound proof-of-possession echo (K31, protocol §9.4). + /// + /// Labels both the handshake transcript and its challenge-keyed HMAC. + /// Domain separation prevents collision with K23 under the same key; the label + /// also remains prefix-free with the older `"CC/v1/prove-echo"` domain. pub const ECHO_TRANSCRIPT: Label = Label::new(b"CC/v1/echo-transcript"); /// Device fingerprint for mechanisms whose public keys exceed 32 bytes @@ -721,25 +591,11 @@ pub mod label { /// is its prefix. pub const SERVER_FRESH: Label = Label::new(b"CC/v1/server-fresh"); - /// Publisher-key signature on the DISTRIBUTION PACKAGE manifest (`manifest.txt` in a - /// Close Crate package, verified by `cc install`). - /// - /// # Why a label rather than "sign the manifest bytes" - /// - /// Because the publisher key is ordinary Ed25519, and without a domain its signature over - /// arbitrary bytes would work anywhere the same key verifies - /// something else. A distribution manifest has its own domain: it is neither a container, - /// protocol document, nor operator file, but an inventory of programs in an archive. + /// Publisher signature on a distribution-package `manifest.txt`. /// - /// The label does not affect the container at all: it changes no format version and enters no - /// header. It belongs in the registry not because the format needs it, - /// but because the registry is the ONLY place prefix-freeness is proved - /// (I-12): a label declared elsewhere is checked by nothing. - /// - /// Prefix-free: the nearest `p` labels are `"CC/v1/payload"`, - /// `"CC/v1/private-meta"`, `"CC/v1/policy-hash"`, and `"CC/v1/prove-echo"`; - /// all differ at byte eight; `"CC/v1/recovery-manifest"` - /// shares only a suffix, not a prefix. + /// A separate domain prevents reuse of an Ed25519 publisher signature as a + /// container or protocol signature. Registration keeps the label prefix-free; + /// the domain does not change the container layout. pub const PACKAGE_MANIFEST: Label = Label::new(b"CC/v1/package-manifest"); /// K29: qualifying data for a TPM statement about the device key (B6a, @@ -776,39 +632,15 @@ pub mod label { ]; } -/// Minimum claim-code entropy. -/// -/// Not a cosmetic number. XChaCha20-Poly1305 is not key-committing, and without -/// slot-commitment verification a low-entropy code can be recovered through -/// a partitioning oracle substantially faster than exhaustive search. A six-digit code -/// is unacceptable. -/// -/// The boundary is checked **at build time**, not runtime, and could not be otherwise. -/// `ClaimSecret::from_bytes` accepts any 32 bytes and must: by then -/// the code has been hash-compressed and the result does not reveal entropy. It must be measured -/// at generation, where it is exactly measurable: code length and alphabet size -/// are known constants. The check lives in `cc_cli::claim` (`const _: () = -/// assert!(...)`), so a short code will not "fail a test"; it will not compile. +/// Minimum claim-code entropy, checked by the code generator. /// -/// Human-invented codes are absent from the product for the same reason: the commitment -/// is plaintext in the container, guessing is offline, and attempt counts -/// cannot be limited, since the guesses are not made against us. +/// A compressed 32-byte `ClaimSecret` does not reveal the original code's entropy. +/// Enforce this bound using the generator's alphabet and length. Guessing is +/// offline against the public commitment, so server attempt limits cannot help. pub const MIN_CLAIM_BITS: u32 = 128; -/// SHA-256 of a byte slice. -/// -/// # Why a shared function when format hashes are computed in `oc-format` -/// -/// Because not everything that needs hashing is format data. A distribution manifest -/// lists programs and checksums; it is not a container, has no format versions, -/// and creating a tag-registry entry for it would be a mistake. -/// -/// It lives here rather than in `cc-cli` under the crate rules: `sha2` is already a dependency -/// of this crate, and a second edge to `cc-cli` would introduce a direct -/// dependency where none is needed. This crate remains pure: -/// no I/O, clocks, or RNGs here. -/// -/// Compare results only through [`digest_eq`]. +/// SHA-256 of arbitrary bytes, including data outside the container format. +/// Compare digests through [`digest_eq`]. #[must_use] pub fn sha256(bytes: &[u8]) -> [u8; 32] { use sha2::Digest as _; @@ -833,24 +665,10 @@ pub fn digest_eq(a: &[u8; 32], b: &[u8; 32]) -> bool { bool::from(a.ct_eq(b)) } -/// Compare two PUBLIC keys of possibly different lengths in constant time. -/// -/// A separate function, not [`digest_eq`] weakened to slices; this distinction is essential. -/// The rule "comparison accepts fixed-length arrays" prevents -/// length from becoming a second source of errors; extending the exception to every -/// comparison in the repository would abolish that rule. Exactly one circumstance -/// justifies this exception: key length is a function of the mechanism (`kem_id`), differing for -/// X25519 and P-256, and both comparison operands come from the **author-signed** -/// header, where length is public by construction. -/// -/// Length mismatch returns `false` immediately, before comparing contents, without leaking -/// anything: the length is a public number in the file, already known to an attacker. This means -/// "the slot is not for our mechanism", hence "not our slot", exactly the same as -/// mismatching key bytes. +/// Compare public key byte strings. /// -/// Contents are compared using `subtle` (I-13): the key is public, but response timing -/// must not reveal how many bytes matched, or a byte-by-byte oracle for guessing -/// the slot's recipient would emerge. +/// Unequal public lengths return `false` immediately. Equal-length contents use +/// `subtle`; this API makes no timing guarantee for secret lengths. #[must_use] pub fn public_key_eq(a: &[u8], b: &[u8]) -> bool { use subtle::ConstantTimeEq; diff --git a/crates/oc-crypto/src/merkle.rs b/crates/oc-crypto/src/merkle.rs index 48698cb..3136580 100644 --- a/crates/oc-crypto/src/merkle.rs +++ b/crates/oc-crypto/src/merkle.rs @@ -307,22 +307,15 @@ impl MerkleTree { crate::digest_eq(&candidate, root) } - /// Consistency proof (RFC 9162, §2.1.4.1): the tree over the first - /// `old_count` leaves is a prefix of this tree. + /// Prove that the first `old_count` leaves are a prefix of this tree + /// (RFC 9162, §2.1.4.1). /// - /// Needed by the log witness (D3): it stores only an old head, not entries; - /// without such a proof, only someone holding the full log could verify - /// "the new head extends the old one". - /// - /// One difference from the RFC follows from [`root_of`]: the verifier knows - /// ROOTS rather than apexes. The RFC omits the old tree's apex when - /// `old_count` is a power of two, since the verifier can supply it. Here - /// there is nothing to supply, so it is placed at the path's start and is not - /// taken on trust: verification maps it through `root_of` to the - /// old head's root. + /// This format authenticates roots through [`root_of`], not raw apexes. For a + /// power-of-two old tree, include its apex at the path's start; verification + /// maps it through `root_of` before comparing the old root. /// /// # Errors - /// [`CryptoError::IndexOutOfRange`]: `old_count` is zero or exceeds the tree size. + /// [`CryptoError::IndexOutOfRange`] if `old_count` is zero or exceeds the tree size. pub fn consistency(&self, old_count: u32) -> Result, CryptoError> { let leaves = self.levels.first().ok_or(CryptoError::TreeMismatch)?; let m = usize::try_from(old_count).map_err(|_| CryptoError::IndexOutOfRange)?; diff --git a/crates/oc-crypto/src/mlkem_p256.rs b/crates/oc-crypto/src/mlkem_p256.rs index 0591123..a5510d1 100644 --- a/crates/oc-crypto/src/mlkem_p256.rs +++ b/crates/oc-crypto/src/mlkem_p256.rs @@ -134,21 +134,11 @@ pub fn keypair_from_seed(seed: &[u8; KEYPAIR_SEED_LEN]) -> Result Zeroizing<[u8; ML_KEM_SEED_LEN]> { let mut xof = Shake256::default(); @@ -243,25 +233,15 @@ pub fn encapsulate( encapsulate_derand(public_key, &seed) } -/// Decapsulation with BOTH halves, the classical half behind a trait. -/// -/// The trait is the entire point of this mechanism: the P-256 private key may reside in -/// the TPM without leaving it, replacing the software key here without changing -/// a single byte of secret derivation. -/// -/// The public point comes FROM THE PARTY ITSELF, not an argument: it enters -/// the combiner, so accepting it externally would make the shared secret externally controllable. +/// Decapsulate with both halves, using the classical key-agreement provider. /// -/// There is and can be no "invalid ciphertext" rejection here: ML-KEM responds -/// to corrupted ciphertext with implicit rejection, deriving a secret from `z` and -/// returning it normally. The caller detects the false secret -/// through the slot commitment (I-4) and AEAD tag, both in constant -/// time. Only structurally invalid input is rejected: an incorrect -/// length or an off-curve point. +/// The provider supplies its own public point for the combiner. This permits a +/// TPM-held P-256 private key without changing the derivation. +/// ML-KEM uses implicit rejection: corrupted ciphertext can return a different +/// secret normally. The caller checks the slot commitment and AEAD tag. /// /// # Errors -/// Incorrect ciphertext length, unparseable ML-KEM seed, or an ephemeral point not -/// on the curve. +/// Incorrect ciphertext length, unparseable seed or an off-curve ephemeral point. pub fn decapsulate_with( ml_kem_seed: &[u8; ML_KEM_SEED_LEN], classical: &dyn KeyAgreement, diff --git a/crates/oc-crypto/src/rsa.rs b/crates/oc-crypto/src/rsa.rs index 5de1564..8df94d5 100644 --- a/crates/oc-crypto/src/rsa.rs +++ b/crates/oc-crypto/src/rsa.rs @@ -148,22 +148,10 @@ pub fn verify_pss_sha256( } } -// --------------------------------------------------------------------------- -// ОТКРЫТЫЕ ОПЕРАЦИИ ДЛЯ ПРОВЕРКИ АТТЕСТАЦИИ (B6a, `docs/protocol.md` §9.11). -// -// Подпись редактировавшего устройства выше прибита к RSA-2048 и PSS с солью 32: -// её производит наш подписывающий, и больше проверяющему уметь незачем. У -// аттестации подписывающие чужие — вендоры TPM и сам TPM, — и им нужно -// другое: PKCS#1 v1.5 (`sha256WithRSAEncryption` в сертификатах, RSASSA у -// ключа удостоверителя) и модули 3072 и 4096 бит у корней вендоров. Плюс одна -// операция в обратную сторону — OAEP-шифрование семени учётных данных на ключ -// подтверждения (TPM2_MakeCredential). -// -// Секрета у проверяющего здесь по-прежнему нет, кроме семени OAEP, а оно — -// ОСНОВАНИЕ, возводимое в открытую степень: время операции зависит от модуля и -// показателя, не от него. Засев OAEP приходит параметром, как всякая -// случайность в этом крейте. -// --------------------------------------------------------------------------- +// Attestation RSA operations (protocol §9.11): PKCS#1 v1.5 verification and +// OAEP encryption for TPM2_MakeCredential, with 2048/3072/4096-bit public keys. +// OAEP input is secret even though the exponent is public. Randomness arrives +// as a parameter; heap cleanup and compiler-copy limits are documented below. /// Modulus lengths accepted by attestation verification: 2048, 3072, and 4096 bits. /// diff --git a/crates/oc-crypto/src/rsa_test_signer.rs b/crates/oc-crypto/src/rsa_test_signer.rs index 1c0dbe1..30358b9 100644 --- a/crates/oc-crypto/src/rsa_test_signer.rs +++ b/crates/oc-crypto/src/rsa_test_signer.rs @@ -1,24 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 -//! SOFTWARE RSA-PSS-SHA256 signer: ONLY for probes. +//! Deterministic RSA-PSS signer for tests and probes. //! -//! # Why it exists at all -//! -//! The production editing key resides in a TPM (`docs/format.md`, "EDITING IS EXECUTABLE", -//! item G), and only the TPM signs. But every run must test editing verification, -//! including Linux containers without TPMs, while PSS signatures are randomized: the live-TPM golden -//! vector (`tests/kat/rsa_pss.kat`) covers one message, whereas -//! probes need signatures over ARBITRARY regions. Hence a signer with HARDCODED -//! test keys and a salt parameter: the same salt gives the same -//! signature, making `tests/kat/edit.kat` reproducible. -//! -//! # Why it is permitted -//! -//! This module exists only under `cfg(test)` or feature `test-signer`, enabled -//! only by `[dev-dependencies]` and testbed builds -//! (guarded in `cc-cli/tests/repository_hygiene.rs`). The private operation here -//! is NOT constant-time, and keys are public in source, precisely why no -//! production path reaches it. The keys were generated for this file -//! (2026-09-17, Miller–Rabin primes, 40 rounds) and live nowhere else. +//! Embedded keys are public test fixtures, not real identities. A supplied salt +//! makes signatures reproducible beyond the frozen live-TPM vector. +//! Available under `cfg(test)` or `test-signer`; the private operation is not +//! constant-time and must not be used with production keys. use crate::CryptoError; use crate::rsa::MODULUS_LEN; diff --git a/crates/oc-crypto/src/seal.rs b/crates/oc-crypto/src/seal.rs index 291ba06..5deeae1 100644 --- a/crates/oc-crypto/src/seal.rs +++ b/crates/oc-crypto/src/seal.rs @@ -1,17 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 -//! Seal secrets to a slot recipient's public key. +//! Seal secrets to a slot recipient using the container's key schedule. //! -//! The construction is Base-mode DHKEM(X25519, HKDF-SHA256) with -//! XChaCha20-Poly1305 AEAD, following RFC 9180. An off-the-shelf HPKE crate is deliberately -//! not used: it would pull in a second curve25519-dalek version and an RNG with -//! incompatible traits, while the TPM path in phase 1 already requires our own -//! key-agreement abstraction: Microsoft Platform Crypto Provider does not offer -//! X25519, and its private key cannot be passed to external code. -//! -//! **This is a hand-written construction and the first candidate for external review.** -//! -//! Key derivation includes both public keys, ephemeral and recipient. Otherwise -//! the same ciphertext could be bound to someone else's identity. +//! Supports classical agreement and hybrid KEMs with HKDF-SHA256 and +//! XChaCha20-Poly1305. Providers allow hardware-held private keys. +//! Derivation binds the encapsulation and recipient public bytes. +//! This custom construction remains a surface for independent cryptographic review. use crate::{CryptoError, KemAlg}; use crate::label::Label; @@ -104,19 +97,14 @@ pub fn a_to_device_info( info } -/// K21 derivation `info`: author → requesting device (`docs/format.md`, -/// "Access request"). +/// K21 derivation `info`: author to requesting device (format, Access request). /// /// ```text /// info = "CC/v1/b-to-device" ‖ u8(kem_id) ‖ file_id(16) ‖ device_fpr(32) /// ``` /// -/// There is NO lease sequence here, deliberately: share A is issued under rules -/// and lives between issuances; share B is issued to a person once and survives any -/// renewal. A sequence in `info` would bind it to a single lease. -/// -/// In the core for the same reason as [`a_to_device_info`]: TWO parties assemble -/// these bytes, the author sealing the share and the device opening it. +/// No lease sequence: share B survives renewals, unlike per-lease share A. +/// Both the sealing author and opening device use this encoding. #[must_use] pub fn b_to_device_info(kem: KemAlg, file_id: &[u8; 16], device_fpr: &[u8; 32]) -> Vec { let mut info = @@ -128,22 +116,14 @@ pub fn b_to_device_info(kem: KemAlg, file_id: &[u8; 16], device_fpr: &[u8; 32]) info } -/// Attestation-challenge `info`: domain label and device fingerprint. +/// Attestation-challenge `info`, shared by server and opening device. /// /// ```text /// info = "CC/v1/attest-nonce" ‖ device_fpr(32) /// ``` /// -/// The fingerprint enters `info`, not only AAD, preventing a challenge issued to one -/// device from opening with another device's key when agreement keys match. -/// -/// Unlike shares A and B, `info` contains NO mechanism. This is not an omission but -/// the frozen handshake form: both halves of a hybrid key open -/// the challenge consecutively, using identical `info`. Adding -/// `kem_id` here would change wire bytes. -/// -/// In the core for the same reason: TWO parties assemble it, the server sealing the challenge -/// and the device opening it. This string previously had three manual constructions. +/// The fingerprint binds the challenge to the device even if agreement keys match. +/// The frozen handshake omits `kem_id`: hybrid halves use identical `info`. #[must_use] pub fn challenge_info(device_fpr: &[u8; 32]) -> Vec { let mut info = Vec::with_capacity(crate::label::ATTEST_NONCE.len().saturating_add(32)); @@ -171,22 +151,11 @@ pub struct SealedBlob { /// inexpressible. [`open`] checks length exactly before any /// cryptography: variable length in the type does not mean unchecked length. pub enc: Vec, - /// AEAD nonce. - /// - /// **Stored, not derived**: the same rule as for payload - /// chunks (§6.1 of the specification), for the same reason, with worse - /// consequences here. + /// Stored 24-byte AEAD nonce. /// - /// A nonce derived from the shared PRK would be entirely determined by the ephemeral pair - /// and would repeat with it. Repeated RNG state after virtual-machine - /// snapshot rollback, disk-image cloning, or backup - /// restoration would produce two blobs with the same key and nonce, - /// hence one keystream. Then `ct₁ ⊕ ct₂ = pt₁ ⊕ pt₂`, and the plaintexts - /// here are secret shares: `secret_A` and `secret_A‖secret_B` - /// reveal `secret_B`, together they reveal KEK, and KEK reveals the content key. - /// A full bypass without any private key. Twenty-four bytes - /// alongside the already stored thirty-two close this completely: the key may - /// repeat, but the keystream will not. + /// Sealing derives it from a random seed and plaintext rather than solely from + /// agreement. If RNG state repeats, different plaintext must still get a different + /// nonce; otherwise `ct₁ ⊕ ct₂ = pt₁ ⊕ pt₂` would expose the secret shares. pub nonce: [u8; NONCE_LEN], /// Ciphertext with appended tag. pub ct: Vec, @@ -224,19 +193,9 @@ pub fn seal( let eph_sk = StaticSecret::from(*eph_bytes); let eph_pk = PublicKey::from(&eph_sk).to_bytes(); - // Nonce хранится в блобе и **не выводится из общего секрета** — выведенный, - // он определялся бы эфемерной парой и совпадал бы всегда, когда совпала она. - // - // Но и просто взять его из генератора недостаточно, и это стоило отдельного - // решения (С-13). Эфемерная пара и nonce приходят из ОДНОГО генератора - // подряд, поэтому полный повтор его состояния — откат снапшота виртуальной - // машины, клон образа диска, восстановление из резервной копии — повторял бы - // оба значения разом, а с ними и поток ключей. Здесь это дороже всего: - // открытые тексты — доли секрета схемы 2-из-2. - // - // Поэтому случайные байты идут не в nonce, а в засев производной, куда - // входит ещё и открытый текст: при повторе генератора и разных секретах - // nonce расходятся. Подробности и границы гарантии — у `hedged_nonce`. + // Store a hedged nonce: random seed plus plaintext. Deriving only from agreement, + // or copying RNG bytes directly, would repeat the keystream after full RNG rollback. + // See `hedged_nonce` for the construction and its limits. let mut nonce_seed = Zeroizing::new([0u8; NONCE_LEN]); rng.fill_bytes(nonce_seed.as_mut_slice()); let nonce = crate::kdf::hedged_nonce::( @@ -350,21 +309,14 @@ fn validate_dh_public(public: &[u8], expected_len: usize) -> Result<(), CryptoEr Ok(()) } -/// Seal a slot using the X-Wing HYBRID, `kem_id = 4`. +/// Seal a slot with X-Wing, `kem_id = 4`. /// -/// A separate function beside [`seal`] and [`seal_p256`] for the same reason -/// those are separate: each mechanism has its own RNG consumption order, -/// while X25519's is frozen by golden artifacts. Here consumption is sixty-four bytes in one -/// call for encapsulation, then twenty-four for the nonce seed. -/// -/// The X-Wing shared secret enters the same key schedule as a DH shared secret, -/// not as an adaptation: `derive_key` binds BOTH public values to it, -/// the ciphertext and recipient key. X-Wing binds its own half itself (§5.3 -/// of the draft), but a second binding is free, while distinct schedules for different -/// mechanisms would mean two key schedules instead of one. +/// Consumes 64 bytes for encapsulation, then 24 for the nonce seed. +/// The common key schedule binds the shared secret, KEM ciphertext and recipient +/// public key. Rejects a noncanonical classical recipient coordinate before RNG use. /// /// # Errors -/// Incorrect public-half length or unparseable ML-KEM key. +/// Incorrect key length, invalid ML-KEM encoding or noncanonical classical key. pub fn seal_xwing( recipient_public: &[u8], info: &[u8], @@ -558,20 +510,12 @@ fn open_core( Ok(Zeroizing::new(plaintext)) } -/// Shared seal/open key schedule: derive the AEAD key from the DH shared secret -/// and both public keys. -/// -/// The nonce is **not** derived here: stored in the blob, see [`SealedBlob::nonce`]. +/// Derive the slot AEAD key from the shared secret and both public values. /// -/// `ikm` contains **both** public keys. Omitting either the ephemeral key or -/// the recipient key individually allows binding the same -/// ciphertext to another identity: an adversary who knows their own private key -/// chooses a public key yielding the same shared secret, then presents someone else's blob as -/// addressed to them. All three fields have fixed 32-byte lengths, making -/// concatenation unambiguous without delimiters. -/// -/// HKDF salt is deliberately empty: the parties have no shared random value at this -/// step, and domain separation relies entirely on `info`. +/// Binding encapsulation and recipient bytes prevents identity substitution. +/// Their sizes are fixed by the selected KEM profile, making concatenation +/// unambiguous. HKDF salt is empty; `info` provides domain separation. +/// The nonce is derived separately and stored in [`SealedBlob::nonce`]. fn derive_key( shared: &crate::agreement::SharedSecret, eph_public: &[u8], @@ -763,19 +707,9 @@ mod kem_binding_tests { ); } - /// Whether this build can execute the declared mechanism is a different question from - /// whether it can parse the mechanism's shape. - /// - /// The P-256 assertion changed here **by decision**, - /// recorded in `docs/format.md` ("VERSION 2 OPENED"), not simply following the code. - /// Before version 2, the registry declared the mechanism without implementing it, - /// and slots using it had to be skipped. It is now implemented because there is no other way - /// to address a TPM key: Platform Crypto Provider lacks X25519. - /// - /// RSA-OAEP remains unimplemented, a tested property rather than unfinished work: - /// the format must retain a numbered but unimplemented mechanism, - /// or the "skip the slot rather than reject the file" branch ceases - /// to be tested at all. + /// Support is separate from recognizing a KEM identifier or its wire shape. + /// P-256 is executable; RSA-OAEP slots remain unsupported and must be skipped. + /// Standalone RSA-OAEP credential encryption is a different API. #[test] fn the_build_executes_exactly_the_mechanisms_it_claims() { assert!(supports_kem(DEFAULT_SEALING_KEM)); diff --git a/crates/oc-crypto/src/secret.rs b/crates/oc-crypto/src/secret.rs index 1142472..0c8d8bf 100644 --- a/crates/oc-crypto/src/secret.rs +++ b/crates/oc-crypto/src/secret.rs @@ -129,22 +129,11 @@ secret_type! { X25519Secret } -/// Self-wiping plaintext buffer that never grows. +/// Self-wiping plaintext buffer with fixed capacity. /// -/// Not `Zeroizing>`; the distinction is fundamental. `Zeroizing` wipes -/// contents **on destruction**, but cannot intervene on growth: when -/// `Vec` reallocates, it returns its old buffer to the allocator unchanged, including -/// all accumulated plaintext. A file decrypted into a growing vector -/// leaves heap copies at every capacity doubling. -/// -/// Capacity is therefore set once and never changes, while the array always -/// has its full length: even the "tail" beyond meaningful data must be wiped, -/// or remnants of a previous longer chunk would survive a subsequent -/// shorter write. -/// -/// This type is deliberately required by [`crate::aead::open_chunk`]'s signature. Previously -/// it accepted an ordinary `Vec`, so wiping relied on caller -/// discipline, meaning it relied on nothing. +/// Growing a Vec can release an unwiped old allocation before its owner drops. +/// This buffer never reallocates and wipes its full capacity, including the tail +/// left by a shorter subsequent write. [`crate::aead::open_chunk`] requires it. pub struct SecretBuf { /// Length always equals capacity, so wiping covers the unused tail too. bytes: Vec, @@ -201,19 +190,10 @@ impl SecretBuf { Ok(()) } - /// Writable meaningful bytes for transforming contents IN PLACE. + /// Return the declared bytes for in-place transformation, without wiping them. /// - /// Differs from [`SecretBuf::as_capacity_mut`] in two essential - /// ways: only the declared portion is returned, and the buffer is **not** - /// wiped before access. In-place decryption needs exactly this: the buffer - /// already contains ciphertext, so wiping before decryption would erase - /// the input. - /// - /// Not added for convenience. Without it, decryption would have to allocate - /// a plaintext vector and copy it here, routing every - /// decrypted chunk through the ordinary heap, whose pages - /// can enter the pagefile. The viewer locks its memory - /// (`VirtualLock`); an intermediate copy would defeat that. + /// Unlike [`SecretBuf::as_capacity_mut`], this preserves ciphertext already in + /// the buffer and exposes only its meaningful length, avoiding a plaintext copy. pub fn as_declared_mut(&mut self) -> &mut [u8] { let len = self.len; self.bytes.get_mut(..len).unwrap_or_default() @@ -259,19 +239,11 @@ impl fmt::Debug for SecretBuf { /// An order-of-magnitude margin, no more: a system thread-pool thread reserves 1 MiB of stack. pub const STACK_WIPE_BYTES: usize = 64 * 1024; -/// Wipe stack BELOW the current frame, where frames of already returned -/// calls resided. -/// -/// Why: `SecretBuf` wipes its memory, but decryption and copying -/// pass through frames placing plaintext fragments on the stack -/// (temporary cipher blocks, syscall copies), and return does not clear -/// the stack. Measurement C2: after view cooldown, broker memory retained -/// 26 document lines, all on the thread stack that had supplied data to the driver. +/// Best-effort wipe of stack space below the current frame. /// -/// How: a local array of the same depth is wiped with writes the -/// compiler cannot remove (`zeroize`), while the function is not inlined, -/// or the array would occupy the caller's frame rather than space below it. Call AFTER plaintext -/// processing, from the same depth that invoked that processing. +/// A non-inlined local array is zeroized after plaintext processing, at the same +/// call depth. This may overwrite returned callees' frames; it is not proof that +/// all compiler copies, registers or other stack regions have been erased. #[inline(never)] pub fn wipe_stack_below() { let mut pad = [0u8; STACK_WIPE_BYTES]; diff --git a/crates/oc-crypto/src/sign.rs b/crates/oc-crypto/src/sign.rs index f961f42..a1a3930 100644 --- a/crates/oc-crypto/src/sign.rs +++ b/crates/oc-crypto/src/sign.rs @@ -1,16 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 -//! Signatures. +//! Domain-separated signatures over [`Transcript`] values. //! -//! Only [`Transcript`] can be signed, and its constructor requires a domain -//! label. Signing bytes without domain separation is therefore impossible: -//! the compiler prevents it. -//! -//! Verification uses `verify_strict`, not `verify`, to avoid inheriting -//! malleability from the cofactor and noncanonical key representations, -//! which would lose the property "one signature, one file". -//! -//! The [`Signer`] trait exists because author and device keys will reside in TPMs -//! in phase 1 without leaving them: hardware will sign, not this code. +//! Transcript construction requires a registry label. Verification uses +//! `verify_strict` to reject small-order points and noncanonical representations. +//! The [`Signer`] trait also supports providers that retain their private key. use crate::transcript::Transcript; use crate::{CryptoError, SigAlg}; @@ -290,35 +283,12 @@ mod tests { ); } - /// SIGNATURES UNDER SMALL-ORDER KEYS ARE REJECTED: that is what `strict` means. - /// - /// # Why `verify_strict` was unguarded without this probe - /// - /// Because every other probe supplies either a valid signature or garbage, - /// and `verify` and `verify_strict` handle both IDENTICALLY. Replacing one - /// with the other broke no test and no vector: - /// the difference is visible in exactly one input class, small-order - /// points, and the repository had no such input. - /// - /// # The vector - /// - /// The classic "signature valid for any message" forgery: public - /// key `A` is the curve identity (`01` followed by 31 zeroes), `R` is the same, - /// and `S = 0`. The verification equation reduces to `[0]B = R + h·A`, hence - /// `identity = identity`, true for ANY message. `verify_strict` - /// rejects this input because `A` and `R` have small order; `verify` does not. - /// - /// # Why the probe includes an `ed25519_dalek` half - /// - /// Without it, the probe would prove nothing: "nonstrict verification accepts - /// this vector" is a claim about a dependency, to be checked by execution rather than - /// reasoning. It also guards the dependency itself: if `dalek` starts - /// rejecting weak keys on the nonstrict path, this half fails and reports that - /// the behavioral discriminator no longer exists. + /// Strict verification rejects a small-order forgery that lax verification accepts. /// - /// The cost of omission is stated in I-6: one signature verifies under - /// multiple keys; "one signature, one file" ceases to hold, - /// and revocation and the log lose their unambiguous file binding. + /// The vector uses identity points for `A` and `R` (`01` then 31 zeroes), with + /// `S = 0`. The equation `[0]B = R + h·A` reduces to `identity = identity` for + /// any message. The lax dependency check is the positive control; without it, + /// rejection would not distinguish strict verification from generic invalid input. #[test] fn a_small_order_forgery_is_refused_although_the_lax_check_would_take_it() { // Нейтральный элемент в сжатом виде: y = 1, то есть `01` и 31 ноль. diff --git a/crates/oc-crypto/src/tpm.rs b/crates/oc-crypto/src/tpm.rs index b5a7d77..142beec 100644 --- a/crates/oc-crypto/src/tpm.rs +++ b/crates/oc-crypto/src/tpm.rs @@ -1,25 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 -//! TPM 2.0 credential protection: the computable part of `TPM2_MakeCredential` -//! (B6a, `docs/protocol.md` §9.11). +//! TPM 2.0 credential protection: the pure part of TPM2_MakeCredential +//! (protocol §9.11). //! -//! # Why in the core -//! -//! The server binds an attestation key to an endorsement key through credential -//! activation: it encrypts a secret so only the TPM containing both keys -//! can open it. Apart from the block cipher, everything is hash computation without -//! I/O, clocks, or an RNG: the seed is a parameter, like every source -//! of randomness in this crate, and every test is deterministic. -//! -//! # The form is not ours -//! -//! TPM 2.0, part 1, "Credential Protection", and KDFa from the same part (SP 800-108 -//! in HMAC counter mode). Labels `"STORAGE"`, `"INTEGRITY"`, and `"IDENTITY"` -//! belong to the TPM specification, not the domain-label registry (I-12): we did not -//! choose them and cannot change them. Labels are supplied WITH the trailing -//! zero, as the reference TPM implementation hashes them and real TPMs -//! expect them; a discrepancy would silently produce credentials no TPM could -//! open. Thus the form is confirmed by activation using -//! a software TPM (`crates/cc-authority/tests/attest/`), not merely by reading the specification. +//! The caller supplies the seed. TPM KDFa uses SP 800-108 HMAC counter mode +//! and the specification's labels `"STORAGE"`, `"INTEGRITY"`, `"IDENTITY"`, +//! including their trailing zero byte. These labels are fixed by TPM, not by +//! this crate's domain registry. Compatibility has software-TPM test evidence. use hmac::{Hmac, KeyInit, Mac}; use sha2::Sha256; diff --git a/crates/oc-crypto/src/wrap.rs b/crates/oc-crypto/src/wrap.rs index fef7416..dab3e87 100644 --- a/crates/oc-crypto/src/wrap.rs +++ b/crates/oc-crypto/src/wrap.rs @@ -236,26 +236,9 @@ mod tests { #[test] fn a_tampered_commitment_is_refused_even_though_the_aead_would_open() { - // ЧТО ЭТА ПРОБА ДОКАЗЫВАЕТ: испорченное обязательство отвергается, хотя - // всё остальное во входе честно и AEAD открылся бы. То есть проверка - // обязательства ЕСТЬ и она несущая, а не украшение. - // - // ЧЕГО ОНА НЕ ДОКАЗЫВАЕТ — и здесь стояло обратное. Комментарий называл - // её «ключевым тестом порядка проверок» и утверждал, что перестановка - // проверки после открытия AEAD её уронит. Это неправда, и проверено - // мутацией: при переставленных строках проба остаётся зелёной. Причина - // в том, что коды ошибок у обеих проверок УРАВНЕНЫ НАМЕРЕННО (см. - // соседнюю `a_wrong_commitment_and_a_wrong_tag_are_indistinguishable`), - // а при верном KEK оба порядка дают одно и то же значение. Порядок - // поведением НЕ РАЗЛИЧИМ — ни этой пробой, ни какой-либо другой. - // - // КТО СТЕРЕЖЁТ ПОРЯДОК: сторож по тексту - // `the_order_of_checks_is_the_one_the_invariants_require` в - // `crates/cc-cli/tests/repository_hygiene.rs`. Он читает тело - // `unwrap_cek` и требует, чтобы вызов `verify_commitment` стоял РАНЬШЕ - // вызова `.decrypt(`. Текстовый сторож здесь не лень, а предел метода: - // доказать порядок поведением нечем, пока коды ошибок одинаковы, а - // делать их разными нельзя — это и был бы оракул. + // A bad commitment is rejected even when KEK and AEAD input are otherwise valid. + // This proves commitment enforcement, not whether it runs before AEAD: both + // failures intentionally share one error. Check ordering separately in source. let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap(); let mut tampered = commitment; if let Some(byte) = tampered.get_mut(0) { @@ -295,21 +278,9 @@ mod tests { #[test] fn the_core_hash_is_bound_by_the_aead_itself_and_not_only_by_the_commitment() { - // Случай, которого не хватало: обязательство СХОДИТСЯ, а `core_hash` у - // AEAD другой. Тогда первая проверка пропускает, и отказать может - // только связывание по AAD (И-2) — больше нечему. - // - // Собирается это без всяких приватных уловок: `slot_commitment` — часть - // публичной поверхности крейта (`oc_crypto::kdf::slot_commitment`), и - // ровно так же обязательство считает `oc-engine`, собирая слот. Проба - // живёт рядом с остальными пробами обёртки только потому, что это её - // место по смыслу. - // - // ЧТО ДОКАЗАНО: байты обёртки, снятые под одним заголовком, не - // открываются под другим, даже если противник пересчитал обязательство - // под новый заголовок — а пересчитать его он может, KEK у него есть, он - // законный получатель этого слота. Именно поэтому одного обязательства - // мало и AAD несёт `core_hash`. + // A valid commitment with a different core_hash must still fail AEAD. + // The recipient can recompute the commitment using KEK, so the AAD binding + // must independently prevent moving a wrapped CEK to another header. let (wrapped, _) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap(); // Обязательство ПОД ДРУГОЙ заголовок, тем же KEK. diff --git a/crates/oc-crypto/tests/kat.rs b/crates/oc-crypto/tests/kat.rs index c801ba1..38cbc60 100644 --- a/crates/oc-crypto/tests/kat.rs +++ b/crates/oc-crypto/tests/kat.rs @@ -9,21 +9,11 @@ clippy::disallowed_methods, clippy::disallowed_types )] -//! Frozen key-schedule known-answer tests (KAT). +//! Frozen key-schedule known-answer tests. //! -//! Vectors live **outside** code, in the repository-root `tests/kat/`; this is not -//! an organizational detail. Their purpose is to let a second format implementation -//! (server, editor, third-party client) compare results without reading our Rust. A vector -//! encoded as a test constant cannot provide that. -//! -//! **Change rule.** `tests/kat/*.kat` files change only alongside -//! a decision recorded in `docs/format.md` and a format-version increase. "Test -//! failed → fix the vector" is forbidden: the vector would stop being -//! evidence and become a reflection of code, making the test tautological. -//! -//! The file format is deliberately primitive: `name = hexadecimal bytes`, with -//! `#` comments. Twenty dependency-free parsing lines leave an implementer -//! no excuse not to compare. +//! `tests/kat/*.kat` uses `name = hexadecimal bytes` with `#` comments so other +//! implementations can consume the same vectors. Changes require a recorded +//! format decision and version increase; do not regenerate vectors to hide a failure. use std::collections::BTreeMap; use std::path::PathBuf; @@ -126,20 +116,12 @@ fn check(vectors: &BTreeMap>, name: &str, actual: &[u8]) { ); } -/// Recompute all vectors and print them in the file format. -/// -/// Marked `#[ignore]` because it is a tool, not a check. Run -/// manually: +/// Print regenerated vectors for a deliberate, recorded format change. /// +/// This ignored test is a tool, not a check: /// ```text /// cargo test -p oc-crypto --test kat -- --ignored --nocapture /// ``` -/// -/// Exists because reissuing vectors is sometimes legitimate: a deliberate format -/// change, documented in `docs/format.md`, with a version increase. -/// A tool is better than editing thirty lines manually: by hand people -/// would fix only the failing lines and silently leave the others -/// inconsistent. #[test] #[ignore = "инструмент перевыпуска векторов, а не проверка"] fn print_all_vectors_for_regeneration() { @@ -283,20 +265,10 @@ const SEAL_RECIPIENT_SECRET: [u8; 32] = [0x5e; 32]; /// Sealing associated data: `policy_hash` in the format. const SEAL_AAD: [u8; 32] = [0x9d; 32]; -/// The `Seal` vector is frozen as an **opening** vector, not a sealing vector. -/// -/// Not a simplification, but the only honest form. Sealing is random by -/// construction: the ephemeral pair comes from an RNG, the nonce from a seed -/// (§3.1), and a second implementation cannot and should not reproduce identical bytes. -/// Opening is fully deterministic, and every container reader -/// performs it: the server opens its slot, the recipient theirs. +/// Open a frozen Seal vector with its private key, `info` and AAD. /// -/// Thus the vector says: "here are the private key, blob, `info`, and `aad`; you -/// must recover exactly this plaintext". It checks the entire construction -/// at once: X25519 agreement, K10 derivation including `info`, parsing -/// of the stored nonce, and AEAD with associated data. A separate vector for the intermediate -/// K10 key is unnecessary and worse: it would freeze an internal value -/// an implementation may compute differently provided results agree. +/// Opening is deterministic; this checks X25519, K10, the stored nonce and AEAD +/// against fixed plaintext without requiring randomized sealing to reproduce bytes. #[test] fn the_slot_sealing_matches_its_frozen_vectors() { use oc_crypto::seal::{SealedBlob, open, slot_info, x25519_public}; @@ -393,25 +365,9 @@ fn print_seal_vector() { println!("seal_plaintext = {}", hex(&plaintext)); } -/// A P-256 slot opens from a frozen vector. -/// -/// Wire-format version 2 check: 65-byte `enc`, mechanism in `info`, -/// stored nonce, AEAD with associated data. Failure means -/// bytes promised immutable changed; fix the code, not the vector. -/// -/// This is K10 with `kem_id = 2`, NOT K11. Plan F-6 promised a K11 vector with -/// the wrong number: K11 is a server→device message containing `lease.seq` in `info`, -/// belonging to the lease path. Implementing K11 instead of K10 would make the slot -/// fail to open, appearing as file corruption. -/// THE MLKEM768-P256 HYBRID MATCHES EXTERNAL VECTORS. -/// -/// Ten vectors from CFRG draft appendix A.1, five checks each: -/// ML-KEM seed from the common seed, P-256 scalar (checked via its public point), -/// composite public half, deterministic encapsulation, and decapsulation -/// with both halves. -/// -/// Negative controls live beside it in module probes: this tests -/// MATCHING external bytes; those test that substituting a half breaks the match. +/// Compare the MLKEM768-P256 hybrid with ten CFRG draft appendix A.1 vectors. +/// Check expanded seeds, the scalar's public point, composite public key, +/// deterministic encapsulation and decapsulation with both halves. #[test] fn the_mlkem_p256_hybrid_matches_the_drafts_own_vectors() { use oc_crypto::agreement::KeyAgreement as _; diff --git a/crates/oc-crypto/tests/measure_packing_path.rs b/crates/oc-crypto/tests/measure_packing_path.rs index 28fc753..91252e9 100644 --- a/crates/oc-crypto/tests/measure_packing_path.rs +++ b/crates/oc-crypto/tests/measure_packing_path.rs @@ -12,33 +12,13 @@ clippy::disallowed_types )] -//! MEASUREMENT: what makes up packing throughput. +//! Measure the three packing passes: nonce hedging, AEAD and leaf hashing. //! -//! # Why it exists and what it corrects +//! Hedging includes a SHA-256 pass over plaintext; omitting it overstates the +//! cipher's share of total time. Compare the reciprocal-rate sum +//! `1/(1/hedge + 1/aead + 1/leaf)` with measured packing throughput. //! -//! From the answer to `docs/deferred.md` §14, "is hardware AEAD worthwhile for -//! speed?" The answer there is no; analysis is in `docs/plan.md`, item `D-aead`. -//! -//! Something more important emerged en route. `D-measure` labeled its figure -//! **"complete packing (AEAD + leaf)"**, incorrectly: there are **three** passes -//! through chunk bytes, not two. The third is nonce hedging, `HKDF-SHA256`, with plaintext -//! **as IKM**, meaning a complete SHA-256 pass for twenty-four -//! bytes. -//! -//! The omission was not cosmetic: I inferred "AEAD ≈ 542 MiB/s" by subtraction -//! and opened §14 on that basis. Direct measurement gives 690–800, so the conclusion -//! rested on a figure nobody had measured. -//! -//! # What is measured here -//! -//! Three passes individually, and packing as their sum: -//! `1/(1/hedge + 1/aead + 1/leaf)`. The calculation agrees with direct `D-measure` results -//! (417–462 versus 454 MiB/s), making the model credible. -//! -//! Its main finding is the **ceiling**: even an instantaneous cipher would speed up -//! packing only two- to threefold. One pass out of three cannot give more. -//! -//! Run (only `--release`, or this measures missing optimizations): +//! Run with optimizations: //! `cargo test -p oc-crypto --release --test measure_packing_path -- --ignored --nocapture` use std::time::{Duration, Instant}; diff --git a/crates/oc-crypto/tests/security_probe_keyschedule.rs b/crates/oc-crypto/tests/security_probe_keyschedule.rs index 03c318c..a6ae673 100644 --- a/crates/oc-crypto/tests/security_probe_keyschedule.rs +++ b/crates/oc-crypto/tests/security_probe_keyschedule.rs @@ -1,25 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 -// Файл-проба состязательной проверки безопасности. Линтерные запреты рабочего -// кода к пробам не применяются — проба вправе делать то, чего продукт делать не -// должен. -// -// ИСТОРИЯ. Проба была выведена из сборки переименованием в `.rs.txt` и лежала в -// docs/security-review, то есть не компилировалась и не исполнялась. Возвращена -// пунктом Д-13 и переписана: три её находки из пяти с тех пор ИСПРАВЛЕНЫ, и -// прежний текст описывал конструкции, которых больше нет. -// -// Что стало с каждой находкой: -// 1. Повтор nonce обёртки CEK — исправлено: nonce стал храниться, а не -// выводиться из KEK; номер K2 в таблице производных сожжён. Тест переписан -// как регрессионный. -// 2. «K2 разворачивает не тот PRK» — исчезло вместе с самим K2. -// 3. Расхождение §3.1 и §3.5 по обязательству слота — исправлено в спеке: обе -// секции теперь говорят `‖ core_hash`. Тест сторожит это и проверяет, что -// прежняя формула (`‖ file_id`) НЕ совпадает. -// 4. MIN_CLAIM_BITS не проверяется — ЖИВО. Оставлено как зафиксированное -// ограничение, см. комментарий у теста. -// 5. K3 и K5 — один тип — исправлено введением `MetaKey`. Перепутать их -// теперь ошибка компиляции, и тест это фиксирует. +// Key-schedule regression and misuse probes. +// Test-local lint allowances permit adversarial inputs. Checks cover stored +// wrapper nonces, slot commitments and distinct key types. The low-entropy +// claim-code probe records a caller-side requirement the primitive cannot enforce. #![allow( clippy::expect_used, clippy::unwrap_used, @@ -192,31 +175,12 @@ fn claim_from_pin(pin: u32) -> ClaimSecret { ClaimSecret::from_bytes(bytes) } -/// KNOWN LIMITATION, captured by a test rather than concealed. +/// Demonstrate offline guessing of a low-entropy claim code. /// -/// `MIN_CLAIM_BITS = 128` is declared the claim code's sole defense against a -/// partitioning oracle, yet no type enforces it: `ClaimSecret::from_bytes` -/// accepts any 32 bytes. The slot commitment is plaintext in the container -/// and acts as an offline oracle: an attacker knowing the server share -/// (a compromised license server holding a container copy) can enumerate -/// a four-digit code in ten thousand attempts of a few HMACs each, without -/// network access. Under §3.4's intended design, the server share should not suffice. -/// -/// The product avoids this hole today, but not because it has been closed. -/// Claim codes are issued (`cc protect --claim`), `RecipientClaim` slots -/// are created and opened, and `cc_cli::container` calls `secret_b_from_claim`: -/// F-3 is closed. The boundary rests in ONE place: -/// `cc_cli::claim` generates exactly 30 characters from a five-bit alphabet, giving -/// 150 bits, enforced by `const _: () = assert!(...)`: a short code there will -/// not fail a test; it will not compile. -/// -/// The lower-level boundary is still absent: `ClaimSecret::from_bytes` accepts -/// any 32 bytes and must, because by that point a hash has compressed the code and the result -/// cannot reveal entropy. The test remains to show the cost of -/// a second way to create `ClaimSecret` bypassing `cc_cli::claim`: a -/// human-invented code, one from an external field, or a future API. This test -/// must fail when such a path appears and `MIN_CLAIM_BITS` ceases -/// to be checked at build time. +/// A known server share plus the public slot commitment lets a caller test guesses +/// without networking. `ClaimSecret::from_bytes` cannot infer the original code's +/// entropy after hashing; generators must enforce at least `MIN_CLAIM_BITS`. +/// The product generator supplies 150 random bits; this probe deliberately uses a PIN. #[test] fn a_low_entropy_claim_code_is_still_brute_forcible_known_limitation() { assert_eq!(MIN_CLAIM_BITS, 128); @@ -258,15 +222,8 @@ fn a_low_entropy_claim_code_is_still_brute_forcible_known_limitation() { // 4. Разделение назначений ключей типами // --------------------------------------------------------------------------- -/// BUG (fixed): K3 and K5 returned the same `PayloadKey` type, silently accepting a -/// private-metadata key where a payload key belonged, -/// contrary to `secret.rs`'s promise that swapping them "becomes a compile -/// error". -/// -/// Fixed by introducing `MetaKey`. An ordinary test cannot check this: -/// the code formerly caught by the probe simply no longer compiles. The test -/// therefore records the observable part, differing derived values, -/// while types prevent confusion, documented here: +/// K3 and K5 derive different values and return different key types. +/// `MetaKey` cannot be passed where a payload key is required: /// /// ```compile_fail /// let meta = derive_private_meta_key(&cek, &SALT, &FILE_ID); diff --git a/crates/oc-crypto/tests/security_probe_seal.rs b/crates/oc-crypto/tests/security_probe_seal.rs index 4193c1e..5fd7df2 100644 --- a/crates/oc-crypto/tests/security_probe_seal.rs +++ b/crates/oc-crypto/tests/security_probe_seal.rs @@ -1,18 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 -// Файл-проба состязательной проверки безопасности. Линтерные запреты рабочего -// кода к пробам не применяются — проба вправе делать то, чего продукт делать не -// должен. -// -// ИСТОРИЯ. Проба была выведена из сборки переименованием в `.rs.txt` и лежала в -// docs/security-review, то есть не компилировалась и не исполнялась. При этом -// именно эту конструкцию docs/format.md §3.3 называет «первым кандидатом на -// внешнюю проверку». Тест, вынесенный из дерева сборки, — это удалённый тест с -// видом сохранённого. Возвращена пунктом Д-13. -// -// Две проверки при возврате оказались красными, и обе переписаны на то, что есть -// на самом деле, а не на то, чего хотел автор прежней редакции: расхождение с -// RFC 9180 (осознанное, см. §3.3) и повтор потока ключей при полном повторе -// состояния генератора (известное ограничение, решение — С-13 в docs/plan.md). +// Seal construction probes with adversarial inputs. +// The construction follows its own specification, rather than RFC 9180's +// byte schedule. Rollback checks cover repeated RNG state with different plaintexts. #![allow( clippy::expect_used, clippy::unwrap_used, @@ -158,22 +147,10 @@ fn every_known_low_order_point_is_refused_both_on_seal_and_on_open() { #[test] fn no_info_can_make_two_slot_purposes_derive_the_same_key() { - // Проверяемых пар здесь две, и обе живы. - // - // Первая — исходная: `"CC/v1/seal-key"‖info1 == "CC/v1/seal-nonce"‖info2` - // означало бы равенство ключа и nonce. Метка `CC/v1/seal-nonce` НА МЕСТЕ - // (`oc_crypto::label::SEAL_NONCE`, format.md §3.6, строка K17): nonce - // читателем не вычисляется — он лежит в записи слота готовым, — но - // отправитель его выводит, чтобы значение перестало быть чистой функцией - // состояния генератора (решение С-13). Метка, которую кто-то выводит, — это - // метка, которую надо держать беспрефиксной. - // - // Вторая пара — та же опасность, переехавшая на назначения слотов. `info` - // приклеивается к метке БЕЗ разделителя (`expand_info`), а - // сам `info` — это `метка назначения слота ‖ kem_id ‖ file_id`. Значит - // столкнуть можно уже не ключ с nonce, а назначение слота с назначением - // другого слота: блоб, адресованный серверу, открылся бы как блоб, - // адресованный устройству автора. Условие ровно то же — беспрефиксность. + // Both derivation labels and slot-purpose labels must be prefix-free: + // `expand_info` concatenates label and info without a delimiter. + // The sender still derives `SEAL_NONCE`, although the receiver reads the + // stored nonce. Slot-purpose labels also distinguish server and author blobs. assert!( !label::SEAL_NONCE.as_bytes().starts_with(label::SEAL_KEY.as_bytes()), "метка nonce — расширение метки ключа: подобрав хвост info, отправитель \ @@ -331,20 +308,11 @@ fn rfc9180_derivation( (key, nonce) } -/// The sealing construction DELIBERATELY DIFFERS from RFC 9180. +/// Record the specified Seal construction's divergence from RFC 9180. /// -/// `docs/format.md` §3.3 says "a hand-written construction **following** RFC 9180, -/// not off-the-shelf HPKE", giving the reason there: NCrypt with Microsoft Platform -/// Crypto Provider lacks X25519, so TPM agreement uses P-256 -/// over the raw `NCryptSecretAgreement` result, and such a key cannot be passed to an -/// existing HPKE crate. -/// -/// An earlier version of this test demanded byte-for-byte RFC 9180 agreement and -/// failed. That requirement was its own invention: the specification never promised agreement. -/// The test was rewritten to do something valuable: record divergence -/// as fact and retain a reference RFC 9180 implementation showing -/// exactly where it occurs. Suddenly matching values would mean the -/// construction had silently been rewritten as HPKE, which must be noticed. +/// The reference implementation identifies the schedule difference; byte-for-byte +/// HPKE agreement is not this format's contract. Raw TPM P-256 agreement is supplied +/// through the provider rather than an off-the-shelf HPKE key type. #[test] fn the_sealing_construction_deliberately_diverges_from_rfc9180() { let mut rng = SeedRng::seeded(33); @@ -395,22 +363,11 @@ fn the_sealing_construction_deliberately_diverges_from_rfc9180() { // 5. Nonce выводится из того же prk, что и ключ // --------------------------------------------------------------------------- -/// A full RNG-state repeat does not expose plaintexts (C-13). -/// -/// A realistic, ordinary scenario: virtual-machine snapshot rollback, disk-image -/// cloning, and backup restoration return the RNG to -/// its previous state. Previously this repeated BOTH ephemeral pair AND nonce, taken -/// consecutively from one RNG, repeating the entire keystream and -/// making two ciphertexts reveal `ct₁ ⊕ ct₂ = pt₁ ⊕ pt₂`. These plaintexts -/// are 2-of-2 secret shares, exposing both shares, KEK, and the content -/// key without a single private key. +/// Repeat the RNG state with different secrets. /// -/// Random bytes now seed a derivation that also includes plaintext, -/// so repeated RNG state with **different** secrets produces different nonces. -/// The ephemeral pair still repeats, as it must, since it is computed -/// before plaintext. Hence this check's structure: require -/// the premise (the RNG repeated) to hold while XOR still -/// fails to yield plaintext. +/// The ephemeral key repeats, while plaintext-dependent nonce hedging changes the +/// nonce. Check that ciphertext XOR does not equal plaintext XOR. This bounded +/// case does not establish safety for every rollback or repeated-input scenario. #[test] fn a_full_generator_repeat_no_longer_reuses_the_keystream() { let pt_one = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; diff --git a/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs b/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs index 8e19c9b..e02697f 100644 --- a/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs +++ b/crates/oc-crypto/tests/security_probe_secretbuf_and_shape.rs @@ -286,19 +286,8 @@ fn a_signature_over_a_length_prefixed_field_does_not_move_to_a_resplit() { // Часть II. Заведены как воспроизведение дефектов; ПОЧИНЕНО, теперь регрессия. // --------------------------------------------------------------------------- -/// BUG (fixed): `as_capacity_mut` returned writable storage WITHOUT wiping the buffer, -/// while `declare_len` could then declare any length up to capacity. Plaintext -/// from a previous longer write escaped through `as_slice`. -/// -/// Type promise: "even the tail beyond meaningful data must be wiped, -/// or remnants of a previous longer chunk survive a shorter write". -/// `fill_from` fulfilled this promise, but `as_capacity_mut` plus -/// `declare_len` did not. That is the path used by -/// `cc-cli::payload::seal_stream`. -/// -/// Fixed by wiping inside `as_capacity_mut` itself. Retained as a -/// regression: "long write → short write → long again" is shorter than any -/// scenario exposing the flaw in a real file. +/// Write long, then short, then long through capacity/length accessors. +/// `as_capacity_mut` must wipe old contents before `declare_len` can expose the tail. #[test] fn probe_bug_declare_len_hands_out_stale_plaintext_written_before_the_current_one() { let mut buf = SecretBuf::with_capacity(64); @@ -322,21 +311,11 @@ fn probe_bug_declare_len_hands_out_stale_plaintext_written_before_the_current_on ); } -/// BUG (fixed): the proof did not bind the (index, leaf count) pair. -/// -/// Closed by binding the root to leaf count (`root_of`). Retained as a -/// regression: enumerates trees through 16 leaves, seeking a second pair -/// under which the same path verifies, checking absence of collisions rather than -/// one known case. -/// -/// Verification follows path shape, and pair (i, n) may share a shape -/// with (j, m), with i ≠ j and n ≠ m. Then identical leaf bytes and the same -/// path verify TWO different claims about a chunk's position in a file. +/// Reuse an identical leaf and proof across index/count pairs through 16 leaves. /// -/// The ordinary probe (`a_proof_does_not_move_to_another_index_or_another_leaf_count`) -/// misses this: it recomputes the leaf for another index, and `leaf_of` includes -/// the chunk index. Here the leaf is IDENTICAL, testing -/// `verify_proof` itself rather than `leaf_of`. +/// The root's leaf-count binding must reject changed claims even when path shapes +/// match. Keeping the leaf unchanged isolates `verify_proof` from `leaf_of`'s +/// separate index binding. #[test] fn probe_bug_a_proof_verifies_under_a_second_pair_of_index_and_leaf_count() { let mut collisions: Vec<(u32, u32, u32, u32)> = Vec::new(); diff --git a/crates/oc-crypto/tests/security_probe_tree_signatures.rs b/crates/oc-crypto/tests/security_probe_tree_signatures.rs index 761add3..bae1783 100644 --- a/crates/oc-crypto/tests/security_probe_tree_signatures.rs +++ b/crates/oc-crypto/tests/security_probe_tree_signatures.rs @@ -12,20 +12,11 @@ clippy::disallowed_methods, clippy::disallowed_types )] -//! Track 7: primitives, integrity tree, and signatures. +//! Tree and signature probes. //! -//! This file deliberately has two parts. -//! -//! Part I: attempts to BREAK the tree, comparing shape with the RFC 6962 -//! reference implementation, agreement between the four sites encoding promotion, -//! searching for two leaf sets with one root (CVE-2012-2459 class), and moving -//! a proof to another index or length. -//! -//! Part II: tests prefixed `probe_bug_`, introduced to reproduce -//! found defects and failing against the code at that time. After fixes, each -//! serves as a regression. The suite description was updated alongside the code: -//! a header outliving a fix misrepresents the build's state -//! just as a comment outliving a construction change does. +//! Compare tree shapes with an RFC 6962 reference, check promotion consistency, +//! search for duplicate-root leaf sets, and try moving proofs to other indices or +//! counts. Tests prefixed `probe_bug_` retain regressions for corrected defects. use std::collections::BTreeMap; @@ -355,24 +346,8 @@ fn every_secret_type_is_zeroize_on_drop() { // Часть II. Заведены как воспроизведение дефектов; ПОЧИНЕНО, теперь регрессия. // --------------------------------------------------------------------------- -/// BUG (fixed): the tree-hash identifier parsed and entered the author-signed -/// header, but was never applied. -/// -/// `merkle::leaf_of` and `merkle::node_of` hardcode BLAKE3 and accept -/// no `TreeHashAlg` argument or other input (see signatures), while -/// `TreeHashAlg::from_u8(2)` returned `Ok(Sha256)`. The build thus CLAIMED -/// SHA-256 tree support while silently computing BLAKE3, the same defect class -/// that produced `alg: none` in JWS. -/// -/// Fixed by a second boundary, `merkle::ensure_supported`, called -/// directly from `from_u8`. The former AEAD comparison here ("AEAD has a second -/// boundary, the tree does not") no longer describes the code: AEAD's boundary then -/// lived in `seal_with`/`open_with`, firing only at the first chunk; -/// item D-9 moved it into `AeadAlg::from_u8`. Both identifiers are now -/// checked identically, at the same point: parsing. -/// -/// The test asserts what must hold: a build unable to compute a SHA-256 -/// tree must reject this identifier rather than accept it. +/// Reject SHA-256 tree identifiers when this build computes only BLAKE3. +/// The parser must not advertise an algorithm that the hashing path ignores. #[test] fn probe_bug_a_tree_hash_identifier_this_build_cannot_honour_is_accepted() { // Лист считается ровно как BLAKE3, независимо ни от чего. Прообраз собран diff --git a/crates/oc-crypto/tests/spec_labels.rs b/crates/oc-crypto/tests/spec_labels.rs index 030a756..c44e4ef 100644 --- a/crates/oc-crypto/tests/spec_labels.rs +++ b/crates/oc-crypto/tests/spec_labels.rs @@ -12,26 +12,10 @@ clippy::arithmetic_side_effects )] -//! The specification's domain-label list must match `label::ALL` byte for byte. +//! Check specification labels against `label::ALL` byte for byte. //! -//! # Why this test was missing and why it is needed -//! -//! I-12 has two halves. The first, uniqueness and prefix-freeness, -//! is guarded by tests inside `oc-crypto`, but all obtain their list **from code**. The second, -//! "the list in `docs/format.md` §3.6 must match `label::ALL`", was checked by -//! NOTHING: the repository had no tests reading `.md` files. -//! -//! While lists agree, the missing check is invisible. The next person adding -//! a label may miss: code without specification means an unseparated domain for the second -//! implementation; specification without code means a dead line the document itself -//! calls a defect. The specification wins conflicts, but a conflict would -//! be discovered only through an attack or manual byte comparison. -//! -//! # Why `include_str!` instead of reading a file -//! -//! The compiler embeds the string, avoiding runtime filesystem access: -//! this crate's ban on clocks, files, and networking remains intact. A side -//! benefit: the test cannot "pass" because the file was not found. +//! Other tests check uniqueness and prefix-freeness; this catches code/spec drift. +//! `include_str!` embeds the specification without runtime filesystem access. const SPEC: &str = include_str!("../../../docs/format.md"); @@ -156,29 +140,10 @@ fn declared_constants(body: &str) -> Vec<&str> { names } -/// EVERY DECLARED LABEL APPEARS IN `ALL`. -/// -/// # The gap this test closes -/// -/// Three tests guard I-12: uniqueness, prefix-freeness, and specification -/// agreement. All obtain their list FROM `label::ALL`, so a label declared -/// beside the others but omitted from `ALL` bypasses all of them. None -/// notices either the label or its absence from §3.6. -/// -/// This is precisely the class that occurred eleven times in this repository: -/// a check is written and tested, but the inspected value -/// is produced by something other than what it appears. Here the value is a list, manually -/// produced by a person appending a name to an array. -/// -/// There are no discrepancies today; the test is for tomorrow, when someone adds a label -/// and forgets the second line. -/// -/// # Why parse source rather than values +/// Check that every declared label appears in `ALL`. /// -/// Because the question concerns source: an omitted constant's value never enters -/// the program, and by definition cannot be queried through `ALL`. -/// `include_str!` embeds text through the compiler, without runtime filesystem -/// access, preserving crate purity. +/// Uniqueness, prefix and spec checks all consume `ALL`, so they cannot detect a +/// constant omitted from it. Inspect the embedded source to catch that omission. #[test] fn every_declared_label_is_listed_in_all() { let body = label_module_body(); diff --git a/crates/oc-crypto/tests/zeroize_on_drop.rs b/crates/oc-crypto/tests/zeroize_on_drop.rs index 9e1e470..20bc5cc 100644 --- a/crates/oc-crypto/tests/zeroize_on_drop.rs +++ b/crates/oc-crypto/tests/zeroize_on_drop.rs @@ -1,16 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 -//! Secrets from dependency crates wipe on destruction too, not just ours. +//! Require dependency secret types to implement `ZeroizeOnDrop`. //! -//! `seal::open` COPIES the long-term device secret into -//! `x25519_dalek::StaticSecret` for `diffie_hellman`, and the agreement result -//! lives in `SharedSecret`. Both are stack-resident, untouched by a wiping -//! allocator: only the type's own `Drop` wipes them. For `x25519-dalek`, the -//! `zeroize` feature controls this; `default-features = false` removes it with the others, -//! which is exactly what happened (review 2026-09-06): `Drop` was empty, leaving the device -//! secret copy unchanged in the stack frame. -//! -//! The check is at type level: without the feature, `StaticSecret` lacks `ZeroizeOnDrop`, -//! and this file fails to compile. Compilation proves the feature is enabled. +//! Agreement copies use `x25519_dalek::StaticSecret` and `SharedSecret`; their +//! cleanup depends on the `zeroize` feature. These trait bounds fail compilation +//! if feature selection removes that cleanup. They do not prove stack-copy erasure. fn zeroizes_on_drop() {} diff --git a/crates/oc-engine/src/edit.rs b/crates/oc-engine/src/edit.rs index 4898c52..e790286 100644 --- a/crates/oc-engine/src/edit.rs +++ b/crates/oc-engine/src/edit.rs @@ -1,22 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 -//! Edit writer: frames, tree, mutable region (`docs/format.md`, -//! "EDITING IS EXECUTABLE", item G). +//! Pure edit writer for frames, tree and mutable region (format, Editing). //! -//! Pure like the entire engine: no I/O, clocks, or RNG; nonce seeds -//! are parameters. It does not sign: the editing key resides in a TPM, -//! and only a signing transcript and the completed return signature are exchanged with the engine. -//! -//! # What the writer must do and must not do -//! -//! * A frame whose plaintext is unchanged **is copied unchanged**. Repeating -//! (nonce, ciphertext, tag) at the same index does not reuse a nonce for -//! DIFFERENT plaintext; it is the same record, not a new one. -//! * A changed frame is sealed only through `seal_chunk_hedged`, which accepts no -//! nonce, while the derived nonce depends on plaintext (I-1). -//! * A copied frame must retain ITS OWN index: AAD binds the index, so a -//! frame moved elsewhere will not open for the reader. The writer -//! does not check indices; that is the caller's promise, with violations detected -//! rather than accepted by the reader. +//! Nonce seeds come from the caller; signing happens outside the engine. +//! Unchanged frames can be copied byte for byte only at their original index: +//! AAD binds that index. Changed frames use `seal_chunk_hedged`, so changed +//! plaintext gets a different nonce even with a repeated seed. +//! The caller must preserve copied indices; the reader rejects a misplaced frame. use oc_crypto::aead::{NONCE_LEN, TAG_LEN, seal_chunk_hedged}; use oc_crypto::merkle::{Leaf, MerkleTree, leaf_of}; diff --git a/crates/oc-engine/src/lib.rs b/crates/oc-engine/src/lib.rs index 7fba0b7..188e4b5 100644 --- a/crates/oc-engine/src/lib.rs +++ b/crates/oc-engine/src/lib.rs @@ -1,18 +1,18 @@ // SPDX-License-Identifier: MPL-2.0 -//! Packing keys, recipient slots, and the container header. +//! Packing keys, recipient slots and the container header. //! //! The host supplies randomness and processes document bytes. The engine has no -//! I/O or clocks, and retains the CEK while releasing only the derived payload key. +//! I/O or clocks and retains CEK while returning the derived payload key. //! //! ```text //! plan() -> session and payload key //! seal_chunks -> ciphertext, length, and tree root (host) -//! assemble() -> header and signing transcript +//! assemble() -> header and mutable region //! sign+write -> author signature and container (host) //! ``` //! -//! Assembly follows streaming because private metadata includes the final length. -//! The host signs the entire header with the author's key; the engine does not sign. +//! Assembly follows streaming because private metadata includes final length. +//! The host derives the signing transcript from the completed header and signs it. use rand_core::CryptoRng; use oc_crypto::secret::{Cek, ClaimSecret, PayloadKey, SecretA, SecretB}; @@ -124,8 +124,7 @@ pub enum Recipient { Identity { public_key: [u8; 32] }, /// Recipient hybrid key: `kem_id = 4`, X-Wing (format version 4). /// - /// Boxed because 1216 bytes in an enum would inflate EVERY - /// variant to that size, including `None`. + /// Boxed so the 1216-byte public key does not enlarge every enum variant. Hybrid { public_key: Box<[u8; oc_crypto::xwing::PUBLIC_KEY_LEN]> }, /// Recipient HARDWARE hybrid: `kem_id = 5`, MLKEM768-P256 (version 5). /// @@ -252,19 +251,11 @@ impl core::fmt::Debug for PackRequest<'_> { } } -/// What the engine returns after assembly. +/// Completed header and mutable region. /// -/// # NO signing transcript here, deliberately -/// -/// It existed and was removed when the engine began moving into a separate process. The reason -/// is not protocol convenience: the device must sign what ITSELF -/// derived from the header it intends to write, not what the engine -/// sent. Otherwise a hostile engine could pair one header's transcript -/// with another header's bytes, making the author's signature authenticate the wrong file. -/// -/// Transcript derivation is a pure function of the header and suite -/// (`oc_format::verify::header_signing_transcript`), available to the device and -/// essentially free. Omitting it would exchange verifiable data for supplied data. +/// The host derives the signing transcript from the header it will write, using +/// `oc_format::verify::header_signing_transcript`. Accepting a transcript supplied +/// by an untrusted engine could authenticate a different header. #[derive(Debug)] pub struct Assembled { /// Entire header, ready to write. @@ -479,23 +470,10 @@ impl Session { RecipientPlan::Claim(claim_commit) => slots.push(claim_slot(claim_commit, commitment)), } - // АВТОРСКИЙ СЛОТ НЕ ВПРАВЕ БЫТЬ СЛАБЕЕ СЛОТА ПОЛУЧАТЕЛЯ. - // - // Здесь безусловно стоял классический слот — на ключ в TPM либо на - // программный, — и он несёт ОБЕ доли разом (`both_shares`). Значит - // гибридный слот получателя не давал контейнеру ничего: противник, - // умеющий решать дискретный логарифм, брал авторский слот, получал A‖B, - // выводил KEK и открывал файл, не притрагиваясь к ML-KEM. Стойкость - // контейнера равна стойкости СЛАБЕЙШЕГО достаточного пути к CEK, а - // авторский путь достаточен всегда. - // - // Поэтому при гибридном получателе авторский слот тоже гибридный. - // - // Цена названа вслух: X-Wing определён на X25519, а ключ в TPM — P-256, - // поэтому на таком файле автор ТЕРЯЕТ аппаратную привязку. Это та же - // взаимоисключимость, что записана для получателя (`docs/threat-model.md` - // §2), только теперь и на стороне автора. Выбор между «постквантово» и - // «ключ не покидает TPM» делает автор, называя получателя. + // The author slot contains both shares and can recover CEK by itself, so it must + // be at least as strong as the recipient slot. Hybrid recipients require a + // matching hybrid author slot. X-Wing uses software X25519; the hardware hybrid + // uses the P-256 provider rather than claiming the same hardware properties. let author_slot = match (hardware_recipient, keys.device_hardware_hybrid) { // Получатель на аппаратном гибриде — авторский слот тот же механизм. // Слабее нельзя: авторский слот несёт ОБЕ доли, и классический @@ -783,24 +761,11 @@ fn seal_private_meta( #[cfg(test)] mod tests { - //! RNG consumption order is NAMED rather than implied. - //! - //! [`plan`]'s documentation freezes the order: `file_id`, - //! `header_salt`, `CEK`, share A, share B. Until now only golden - //! artifacts guarded it: those in `cc-cli` and adjacent `hardware_hybrid_golden.rs`. An artifact - //! responds to swapped adjacent calls with "bytes differ at - //! position N", insufficient to reconstruct order: all file bytes - //! change together because everything else derives from these five - //! values. - //! - //! Four of five requests are 32 bytes, so lengths alone CANNOT reveal - //! neighbor swaps. Therefore we also check WHERE the bytes went: stream - //! piece K must land in field X, and on a shift - //! the probe names what took its place. + //! Check RNG byte destinations as well as draw lengths. //! - //! This probe is inside the crate rather than `tests/`: [`Session`] fields are private; - //! exposing them for comparison would widen the engine API for a test, - //! releasing file secrets to anyone importing the crate. + //! The frozen order is file_id, header_salt, CEK, share A, share B. Four draws + //! have the same size; checking only lengths would miss their permutation. + //! Tests live here to inspect private Session fields without exposing file secrets. // Литы сняты для теста: `unwrap`/`panic` — словарь проверки, индексирование // и арифметика — нарезка потока заведомо известной длины. diff --git a/crates/oc-engine/src/wire.rs b/crates/oc-engine/src/wire.rs index b52eb3c..1e9e2ac 100644 --- a/crates/oc-engine/src/wire.rs +++ b/crates/oc-engine/src/wire.rs @@ -272,23 +272,11 @@ fn aead_from_wire(value: u8) -> Result { } } -/// One wire message's bytes in a fixed-capacity buffer. +/// Wire message bytes in a self-wiping fixed-capacity buffer. /// -/// Not introduced for taste. The payload key and claim code travel over this -/// wire, while an appended-to `Vec` returns its old block to the allocator -/// on every reallocation, with secrets inside and before any `Drop` (I-11). -/// Here capacity is set once and contents wiped on destruction: -/// this is [`SecretBuf`], whose guarantees cover the entire message, -/// not one "secret" field; the wire layout does not divide bytes into important -/// and unimportant ones. -/// -/// One type for both sides, deliberately. Pipe reception is the same problem: frame -/// length is known in advance, and reading it into an ordinary vector would create -/// a second, unwiped copy of the same message. -/// -/// [`Deref`] to `[u8]` deliberately exposes bytes: they are meant to enter -/// a pipe. The type promises not unreadability but a buffer that -/// never reallocates or outlives itself. +/// Messages can contain payload keys or claim codes. [`SecretBuf`] avoids copies +/// left behind by Vec growth and wipes the full buffer on drop. Both sending and +/// receiving use this type; [`Deref`] exposes bytes for the host's I/O. pub struct Message(SecretBuf); impl Message { diff --git a/crates/oc-engine/tests/hardware_hybrid_golden.rs b/crates/oc-engine/tests/hardware_hybrid_golden.rs index bb23a9b..c11ea5d 100644 --- a/crates/oc-engine/tests/hardware_hybrid_golden.rs +++ b/crates/oc-engine/tests/hardware_hybrid_golden.rs @@ -10,21 +10,9 @@ //! the private metadata. Changing the artifact requires a format decision; //! its version-6 witness has not been created. -// Литы отключены только здесь и только те, без которых тест нечитаем: -// `unwrap`/`expect`/`panic` — потому что провал пробы и есть паника, а -// `indexing_slicing`/`arithmetic_side_effects` — потому что срезы эталона -// режутся по заведомо известным границам, проверенным соседними `assert`. -// -// `disallowed_methods` — отдельный случай, и он требует не отговорки, а довода. -// `clippy.toml` запрещает движку `std::fs` и `std::env` под лозунгом «ввод-вывод -// живёт в cc-cli», и запрет этот про КРЕЙТ: пустой от машины обязан быть тот -// код, который поедет в анклав, то есть `src`. Эталон же по определению лежит -// файлом, и проба, которая его не читает, не проба. Читать через `include_bytes!` -// было бы соблазнительно и хуже: отсутствующий эталон стал бы ошибкой СБОРКИ, и -// инструмент, которым его заводят, перестал бы собираться вместе с ним. То же -// послабление и по той же причине стоит у контейнерных эталонов -// (`cc-cli/tests/golden.rs`). Гейт чистоты это не задевает: под -// `wasm32-unknown-unknown` собирается библиотека, а не её пробы. +// Test-only lint allowances cover assertions and fixed fixture slicing. +// Filesystem reads load frozen artifacts at runtime so their generator can +// build before they exist. The pure-core gate applies to library source. #![allow( clippy::unwrap_used, clippy::expect_used, @@ -246,23 +234,11 @@ fn the_hardware_hybrid_header_is_byte_identical_to_the_frozen_golden() { } } -/// BOTH GOLDEN HYBRID SLOTS OPEN WITH THEIR OWNERS' HALVES. +/// Open both frozen hybrid slots and recover private metadata. /// -/// A positive control without which byte comparison is blind: it proves -/// byte stability while saying nothing about usability. This checks the converse: -/// the frozen artifact is usable by BOTH paths, author and recipient. -/// -/// The recipient path is exactly the device path: `open_mlkem_p256` using both -/// halves, share A from the server slot, `derive_kek`, `unwrap_cek`, also checking -/// the slot commitment (I-4: `unwrap_cek` compares it in constant -/// time BEFORE opening AEAD). The author path differs because its slot carries -/// BOTH shares together and needs no server. -/// -/// Continues through private-metadata PLAINTEXT rather than stopping at `CEK`: -/// stopping at the key would prove shares agree, but not that the data encrypted -/// with that key agrees. No file contents here, since the engine does not see -/// the stream, so the path ends at private metadata; this limit is stated -/// explicitly rather than hidden behind "fully". +/// The recipient uses its two halves, the server share, KEK derivation and CEK +/// unwrapping; the author slot supplies both shares. Check the commitment as well. +/// This validates the metadata path; the engine does not process payload streams. #[test] fn both_hardware_hybrid_slots_open_for_their_owners() { let bytes = std::fs::read(golden_path()).expect("эталон движка обязан лежать в репозитории"); diff --git a/crates/oc-engine/tests/p256_author_golden.rs b/crates/oc-engine/tests/p256_author_golden.rs index c85d7cb..bbdc815 100644 --- a/crates/oc-engine/tests/p256_author_golden.rs +++ b/crates/oc-engine/tests/p256_author_golden.rs @@ -12,20 +12,9 @@ //! Changing fixtures requires a format decision; a version-6 witness has not been //! created for these artifacts. -// Литы отключены только здесь и только те, без которых тест нечитаем: -// `unwrap`/`expect`/`panic` — потому что провал пробы и есть паника, а -// `indexing_slicing`/`arithmetic_side_effects` — потому что срезы эталона -// режутся по заведомо известным границам, проверенным соседними `assert`. -// -// `disallowed_methods` — отдельный случай, и довод тот же, что у соседа -// (`hardware_hybrid_golden.rs`): `clippy.toml` запрещает движку `std::fs` и -// `std::env` под лозунгом «ввод-вывод живёт в cc-cli», и запрет этот про КРЕЙТ — -// пустым от машины обязан быть код, который поедет в анклав, то есть `src`. -// Эталон же по определению лежит файлом, и проба, которая его не читает, не -// проба. `include_bytes!` было бы хуже: отсутствующий эталон стал бы ошибкой -// СБОРКИ, и инструмент, которым его заводят, перестал бы собираться вместе с -// ним. Гейт чистоты это не задевает: под `wasm32-unknown-unknown` собирается -// библиотека, а не её пробы. +// Test-only lint allowances cover assertions and fixed fixture slicing. +// Runtime fixture reads let the generator build before artifacts exist. +// The pure-core gate applies to library source. #![allow( clippy::unwrap_used, clippy::expect_used, diff --git a/crates/oc-format/src/content.rs b/crates/oc-format/src/content.rs index 8458d93..3f7d318 100644 --- a/crates/oc-format/src/content.rs +++ b/crates/oc-format/src/content.rs @@ -21,19 +21,12 @@ pub mod tag { pub const CHUNK_COUNT: u16 = 2; pub const TREE_ROOT: u16 = 3; pub const VERSION_COUNTER: u16 = 4; - /// Footer offset. **The authoritative source of this value**: a decision moved to version 4. + /// Current footer offset, authenticated by the mutable-region MAC. /// - /// Tag 16 in the signed header declares the same value and is permanently retired: the footer - /// follows the payload, whose length changes with edits, so the offset must live where - /// it can change without the author, alongside `total_len` and `chunk_count`, - /// which likewise describe "what the file looks like now". - /// - /// The offset is under a MAC rather than a signature, so a `CEK` holder can - /// replace it. The decision therefore requires this condition: **the footer - /// may carry only self-authenticating content**, such as a timestamp authority token - /// or a countersignature. A tag table does not authenticate itself and cannot go here - /// without revisiting the decision. Discussion: `docs/format.md`, section - /// "VERSION 3 IS OPEN", item 6. + /// Header tag 16 is retired because edits can move the footer without the author. + /// A CEK holder can update this offset, so the footer may contain only data with + /// its own authentication, such as TSA tokens or countersignatures. An unsigned + /// tag table would need a different trust rule (format, version 3 item 6). pub const FOOTER_OFFSET: u16 = 5; /// Signature of the editing device. **Since version 4.** /// @@ -478,26 +471,11 @@ impl ContentDesc { Ok(Self { total_len, chunk_count, tree_root, version_counter, footer_offset, editor }) } - /// Bytes covered by the MAC. - /// - /// `file_id` is mandatory and comes first: without it, a description cut out of one - /// container would be accepted in another, so substituting a length and tree root - /// would require only copying a hundred bytes between files. - /// - /// Next come the **raw body bytes**, not parsed values. This is the same - /// decision as for the header signature and for the same reason: recomputing over - /// a re-encoding of a parsed structure causes the entire family - /// of canonicalization bugs known from JWS and XML-DSig. + /// MAC input: file_id followed by the raw body bytes. /// - /// Despite first impressions, this does not hinder version compatibility. A MAC over - /// raw bytes **always** agrees across versions: the reader has exactly the bytes - /// authenticated by the writer, without any reconstruction between them. - /// It also removes the need to bind the body length separately: it is already included. - /// - /// The main practical consequence: the MAC can be verified **before** parsing - /// the body. Otherwise distinct parse errors (missing required field, wrong length, - /// unknown tag) would be reported for unauthenticated bytes, and corruption - /// of the mutable region would look like truncation rather than forgery. + /// The file ID prevents cross-container substitution. Authenticating the original + /// bytes avoids re-encoding ambiguity and permits MAC verification before parsing, + /// so malformed unauthenticated data cannot produce distinguishable parse errors. fn mac_transcript(file_id: &[u8; 16], body: &[u8]) -> Transcript { let mut t = Transcript::new(label::CONTENT_MAC); // `fixed` для `file_id` — его длина задана типом; `field` для тела — @@ -532,24 +510,11 @@ mod tests { } } - /// Assemble a region from an arbitrary body with a correct MAC, producing input - /// that our writer cannot generate but an attacker holding the CEK could. - /// MAC verification occurs BEFORE body parsing: a guard on the order of two lines. - /// - /// Before this test, the order relied on a comment and attentiveness: - /// swapping `mac::verify` and `decode_body` did not fail any check. - /// Tests for parse error codes supplied a correct MAC, tests for a bad MAC - /// supplied a valid body, and nobody tested the case combining both faults. - /// - /// The cost of regression is not theoretical. Distinguishable parse errors for - /// UNAUTHENTICATED bytes give an attacker an oracle: a forged mutable region - /// would report "missing required field" or "unknown tag" instead of "MAC - /// mismatch", thus appearing as file truncation rather than forgery - /// (I-5). + /// An invalid MAC must reject an unparseable body before parsing. /// - /// The body here is deliberately unparseable, the key deliberately wrong. There is - /// exactly one correct response: `BadContentMac`; any parse error means the body - /// was read before authentication. + /// The body cannot be produced by the ordinary writer; a CEK holder can construct + /// it. Combining a wrong key and malformed body distinguishes MAC-first ordering: + /// the expected error is BadContentMac, never a body parsing error. #[test] fn the_mac_is_checked_before_the_body_is_parsed() { let file_id = [0x11u8; 16]; diff --git a/crates/oc-format/src/edit.rs b/crates/oc-format/src/edit.rs index c32697c..1724ea6 100644 --- a/crates/oc-format/src/edit.rs +++ b/crates/oc-format/src/edit.rs @@ -1,19 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 -//! Editing: edit key certificate, editor signature, session head, and -//! revision verification (`docs/format.md`, "EDITING IS EXECUTABLE", decision 2026-09-17). +//! Edit certificates, editor signatures, session heads and revision verification. //! -//! # What is here and what is not -//! -//! This module contains bytes and the decision "this revision is signed by someone -//! the author authorized". The counter reference (accepted-revision journal, server registry) lives above: -//! it needs state, which this crate does not hold. Time is also supplied as a parameter. -//! -//! # Ordering -//! -//! Called AFTER verifying the mutable region MAC and parsing its body -//! (`ContentDesc::decode_verified_with_body`): the editor signature is verified -//! over authenticated bytes rather than attacker-supplied input (item 4 of the -//! version 3 decision). +//! Checks author authorization for a revision. Accepted-revision counters require +//! host state; time is a parameter. Call after verifying the mutable-region MAC +//! and parsing its body through `ContentDesc::decode_verified_with_body`. use crate::content::{ContentDesc, EditorSignature, FIRST_EDITING_VERSION, editor_tag, tag}; use crate::header::Header; diff --git a/crates/oc-format/src/encoding.rs b/crates/oc-format/src/encoding.rs index 2fa48a0..677ca5a 100644 --- a/crates/oc-format/src/encoding.rs +++ b/crates/oc-format/src/encoding.rs @@ -63,20 +63,12 @@ pub const ALL: [Encoding; 3] = [Encoding::Cp1251, Encoding::Koi8R, Encoding::Cp8 /// under any of the three tables, so there would always be a "winner". const CONFIDENT: u32 = 3; -/// Guess the encoding from the bytes. +/// Guess text encoding; return None when confidence is insufficient. /// -/// `None` means "no confident candidate": a binary file, text in a language without -/// Cyrillic, or a fragment that is too short. The caller must then NOT display -/// the text as text, or it will show plausible nonsense. -/// -/// # Distinguishing CP1251 from KOI8-R -/// -/// Both place Cyrillic in `0xC0..=0xFF`, so "did these become letters" cannot -/// distinguish them. CASE does: KOI8-R puts lowercase letters in `0xC0..=0xDF`, -/// while CP1251 uses `0xE0..=0xFF`. Russian text is mostly lowercase, so the correct -/// table yields many lowercase letters and the wrong one many uppercase letters. -/// -/// Hence the metric: count LOWERCASE Cyrillic letters rather than all letters. +/// Short, binary or non-Cyrillic input may have no candidate and should not be +/// silently displayed as guessed text. CP1251/KOI8-R scoring counts lowercase +/// Cyrillic: KOI8-R uses `0xC0..=0xDF`, CP1251 `0xE0..=0xFF`, so letter count +/// alone would not distinguish the two tables. #[must_use] pub fn guess(bytes: &[u8]) -> Option { let high = bytes.iter().filter(|byte| **byte >= 0x80).count(); diff --git a/crates/oc-format/src/frame.rs b/crates/oc-format/src/frame.rs index 084b79b..d6de49a 100644 --- a/crates/oc-format/src/frame.rs +++ b/crates/oc-format/src/frame.rs @@ -1,24 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 -//! Message framing: four length bytes, least significant byte first. +//! Message framing with a four-byte little-endian length. //! -//! # Why this lives here rather than with the first consumer -//! -//! Because there are already three consumers: the engine behind a pipe, its client, -//! and the server behind a socket. All share one rule: **check the limit BEFORE -//! allocating**; two copies of that rule already existed, both only in prose. -//! -//! Divergent copies here mean different interpretations of length between -//! processes, not a stylistic difference: one side accepts a message, while -//! the other ends the conversation. Hence one implementation, in a crate all can see. -//! -//! This also receives what appeared in `oc-engine` yesterday: once there was a third -//! consumer, the former location was no longer shared. -//! -//! # What is NOT here -//! -//! I/O. The engine uses a pipe, the client a child process, the server -//! a socket; combining three different forms of I/O into one type would hide -//! differences that must remain visible: each has its own errors and end of conversation. +//! Check the declared limit before allocating. Pipe and socket hosts share this +//! encoding but keep their own I/O, errors and end-of-stream handling. use crate::FormatError; diff --git a/crates/oc-format/src/header.rs b/crates/oc-format/src/header.rs index 58db24c..a0581e3 100644 --- a/crates/oc-format/src/header.rs +++ b/crates/oc-format/src/header.rs @@ -51,19 +51,10 @@ pub mod tag { /// field, but **excluded** from [`super::Header::core_hash`] to avoid a /// circular dependency: associated data binds the wrapper to the core hash. pub const WRAPPED_CEK: u16 = 17; - /// Membership authorized to administer the file. **Since version 4.** + /// Coauthor membership authorized to administer the file (version 4). /// - /// The first OPTIONAL tag in the signed header, deliberately, not - /// accidentally. The general rule, "a field tightening a requirement must be - /// critical", does not apply because this field tightens - /// nothing for the READER: coauthor membership changes neither keys nor access - /// rules, only whose orders the SERVER will execute. - /// A client skipping the tag opens the file entirely correctly. - /// - /// A critical tag would make an old viewer refuse - /// a file with two administrators instead of one: rejection without - /// cause, precisely the cost the optional range exists - /// to avoid. + /// Optional because it changes which orders the server accepts, not reader keys + /// or access rules. Older readers can skip it without misinterpreting access. pub const COAUTHORS: u16 = 0x8001; } @@ -728,20 +719,9 @@ fn decode_suite(bytes: &[u8]) -> Result { // шифром и решил бы, что файл повреждён. suite_tag::SIG => { let parsed = SigAlg::from_u8(field.u8()?).map_err(|_| unsupported(field.tag))?; - // ЗДЕСЬ ГОДИТСЯ ТОЛЬКО Ed25519, и проверка нужна с тех пор, как - // `SigAlg` перестал быть одноместным. - // - // Номер 2 (RSA-PSS) сборка теперь ИСПОЛНЯЕТ — им подписывает - // правку редактировавшее устройство, — поэтому `from_u8` его - // пропускает. Но `suite.sig_alg` описывает подпись АВТОРА, и она - // заморожена версией 1: проверяется она `verify_strict` над - // Ed25519 безусловно. - // - // Без этой строки заголовок мог бы объявить RSA-PSS, а проверен - // был бы по Ed25519 — то есть объявление алгоритма снова стало бы - // украшением. Ровно тот дефект, ради которого заведён - // `ensure_supported`, только на уровень выше: там «умеем ли», а - // здесь «в этом ли месте». + // The author's suite signature is Ed25519. RSA-PSS support applies only to + // editing-device signatures; accepting it here would declare one algorithm + // while header verification still uses another. if parsed != SigAlg::Ed25519 { return Err(unsupported(field.tag)); } @@ -754,27 +734,9 @@ fn decode_suite(bytes: &[u8]) -> Result { tree_hash = Some(TreeHashAlg::from_u8(field.u8()?).map_err(|_| unsupported(field.tag))?); } - // Правило диапазона действует и внутри вложенных map, а не только на - // верхнем уровне заголовка: иначе оно перестаёт быть свойством - // КОНТЕЙНЕРА и становится свойством конкретного декодера, а их в - // крейте несколько — `header`, `content`, `edit`, `footer`, — и все - // их пришлось бы помнить. Раньше здесь стоял безусловный отказ, то - // есть необязательного диапазона внутри `suite` не существовало - // вовсе. - // - // «Свойство контейнера», а не «свойство формата», и это уточнение - // стоило правки: разборщиков в самом крейте несколько, и правило - // обязано быть одним на всех. - // - // Документы протокола (`oc-protocol`) с 2026-09-21 подчиняются ТОМУ - // ЖЕ правилу и зовут ту же `unknown_tag_action` (`docs/protocol.md` - // §0.1). До того дня они отвергали любой незнакомый тег, в каком бы - // диапазоне тот ни стоял, и исключением был один лизинг; вопрос, - // оставленный решением Р-2 открытым, закрыт тем, что стороны после - // выпуска обновляются в разное время. Исключение теперь одно и - // обратное по смыслу: решение автора (`oc_protocol::access`) - // остаётся строгим, потому что его подпись покрывает не сырые - // байты, а пересобранное тело. + // Nested maps follow the same unknown-tag range rule as the outer container. + // Protocol codecs share that rule except access decisions, whose signature is + // reconstructed from parsed fields and therefore cannot skip unknown bytes. other => match unknown_tag_action(other) { UnknownTag::Refuse => { return Err(FormatError::UnknownCriticalField { tag: other }); @@ -1024,19 +986,9 @@ fn check_slot_shape( }); } if !seals { - // МЕХАНИЗМ У СЛОТА КОДА-ПРЕТЕНЗИИ ПРИБИТ К ЕДИНИЦЕ, и это требование §2 - // п.5, записанное там дословно: «объявляет `kem_id = 1` всегда». - // - // Причина в спеке названа, и она про БАЙТЫ, а не про стройность: длина - // нулевого `enc` берётся из механизма, и слот, объявивший P-256, несёт - // 65 нулей вместо 32. Оба числа проходят проверку «все нули», обе длины - // законны каждая для своего механизма — то есть `claim.cc` мог быть - // выпущен в двух разных байтовых видах, оба принимаемые. Для формата, - // объявленного замороженным, это и есть «байты поехали молча». - // - // Проверка стоит и на записи, и на чтении. Опытная проба показала, что - // до неё слот с `kem_id = 2` и 65 нулями принимали ОБЕ стороны: писатель - // производил то, что читатель обязан был отвергнуть. + // Claim slots always use kem_id = 1 (format §2, item 5). + // That fixes zero enc to 32 bytes and prevents two accepted encodings for the + // same claim slot. Enforce the rule on both writing and reading. if kem != KemAlg::X25519HkdfSha256 { return Err(FormatError::BadFieldLength { tag: slot_tag::KEM, len: kem as usize }); } @@ -1284,20 +1236,9 @@ fn decode_coauthors(bytes: &[u8]) -> Result { } fn decode_slot(version: u16, bytes: &[u8]) -> Result { - // Разбор в ДВА прохода, и это не стиль. - // - // Первый проход только раскладывает поля по срезам и выясняет, наш ли это слот: - // вид, механизм, нет ли незнакомого критичного тега. Второй — уже зная, что - // слот наш, — применяет точные длины. - // - // Раньше проход был один, и длина `enc` проверялась в нём же, до того как - // становилось известно, чей это слот. Из-за этого слот на чужом механизме - // отвергал весь файл (см. [`expected_enc_len`]), а ветка «незнакомый механизм → - // пропустить слот» была недостижима: она стоит после цикла. - // - // И-8 при этом не ослаблен. Для слота, который эта версия разбирает, длина - // по-прежнему проверяется на точное соответствие, и несоответствие по-прежнему - // фатально: короткое не дополняется нулями, длинное не обрезается. + // First collect fields and identify the slot's kind, mechanism and critical tags. + // Apply exact lengths only after deciding the slot is supported. Checking them + // earlier would reject a file whose unfamiliar slot should simply be skipped. let mut reader = TlvReader::new(bytes); let mut kind_raw = None; let mut kem = None; @@ -1326,24 +1267,9 @@ fn decode_slot(version: u16, bytes: &[u8]) -> Result { slot_tag::KEY_FPR => key_fpr = Some(field.value), slot_tag::NONCE => nonce = Some(field.value), slot_tag::CLAIM_COMMIT => claim_commit = Some(field.value), - // Неизвестный тег ВНУТРИ слота разбирается тем же правилом диапазона, - // что и снаружи, но последствие у критичного тега другое: непригоден - // СЛОТ, а не файл. - // - // Молча пропустить критичное поле нельзя. Слот — не пассивный мешок - // байтов: из него достаётся ключевой материал, и поле, добавленное - // будущей версией в критичном диапазоне, меняет то, КАК его - // доставать. Пропустив, клиент открыл бы слот не по тем правилам и - // решил бы, что всё в порядке. - // - // Но и отвергать весь файл нельзя — а раньше делалось именно это, и - // получалось, что точка расширения, ради которой слоты и заведены, - // закрыта: любой будущий вид слота с новым критичным полем делал файл - // нечитаемым для клиентов версии 1, даже когда у них есть собственный - // вполне открываемый слот. Правило чтения §3.3 звучит иначе: понять - // хотя бы один пригодный слот, остальные игнорировать. Незнакомый вид - // слота и незнакомый `kem_id` уже пропускаются именно так — - // незнакомое критичное поле обязано вести себя так же. + // An unknown critical field makes this slot unusable, not the entire file. + // Do not interpret a partially understood key slot; skip it and allow another + // supported slot to serve the recipient. Optional fields follow the shared range rule. other => match unknown_tag_action(other) { UnknownTag::Refuse => unusable = true, UnknownTag::Ignore => {} @@ -1457,21 +1383,9 @@ mod tests { use super::*; use oc_policy::{Action, Policy}; - /// LENGTH TABLES ARE SILENT PRECISELY FOR THE MECHANISM THE BUILD CANNOT EXECUTE. - /// - /// Length and executability are DIFFERENT questions and must not be conflated: length - /// depends on the "version × mechanism" pair (P-256 is unknown to version 1, hybrids - /// to versions before four), while executability belongs to the build. Therefore - /// the check is not table equality but one implication: a mechanism is executable if and - /// only if AT LEAST ONE readable version defines BOTH lengths for it. - /// - /// Without this probe agreement relied on memory: adding a mechanism to - /// [`oc_crypto::seal::supports_kem`] but forgetting the table here produces - /// a slot the build can open but the parser silently skips - /// as foreign. The reverse omission is worse: a shape without execution support. - /// - /// Members come from parsing, not a manually remembered list, so a new registry - /// number enters automatically. + /// Executable KEMs must have both length entries in at least one readable version. + /// Versions define wire shapes; the build defines support. Enumerating registry + /// identifiers catches a new implementation whose parser length table was omitted. #[test] fn the_length_tables_are_silent_exactly_about_the_unexecutable_mechanism() { let all: Vec = (0u8..=255).filter_map(|v| KemAlg::from_u8(v).ok()).collect(); @@ -1911,23 +1825,12 @@ mod tests { ); } - /// THE ENTIRE RECORD IS EXCLUDED: TAG, LENGTH, AND VALUE, NOT JUST THE VALUE. - /// - /// The neighboring test above (`..._ignores_the_key_material_but_notices_everything_else`) - /// does NOT guard this, for a reason worth stating: it substitutes slot contents - /// and wrapped key WITHOUT CHANGING THEIR LENGTHS: `ct` stays 48 bytes, - /// while I-2 fixes `wrapped_cek` at 72 bytes. If code excluded - /// only the value and left tag and length, those remaining bytes would match - /// exactly in both headers, their hashes would agree, and the probe would remain silent. - /// Previously ONLY frozen vectors caught this, reporting "bytes differ at position - /// N", which does not identify the culprit. + /// Core hashing excludes each complete key-material record: tag, length and value. /// - /// Here the expected value is recomputed INDEPENDENTLY: header bytes - /// are concatenated around both record ranges. This is a second expression of rule - /// I-3, "`SHA-256("CC/v1/core-hash" ‖ 0x00 ‖ Header without these records)`", - /// and disagreement with the first means the exclusion shifted. Record 17 is covered - /// only this way: its length is constant, so no behavioral counterpart - /// like the probe below can exist for it. + /// Changing only same-length values cannot detect accidental retention of tag/length. + /// This independent construction concatenates header bytes around the full record + /// ranges and compares `SHA-256("CC/v1/core-hash" ‖ 0x00 ‖ Header without these records)`. + /// Record 17 has fixed length, so it especially needs this exclusion check. #[test] fn the_core_hash_cuts_whole_records_tag_and_length_included() { let bytes = sample().encode().unwrap(); diff --git a/crates/oc-format/src/lib.rs b/crates/oc-format/src/lib.rs index 8728ddd..c6de9e3 100644 --- a/crates/oc-format/src/lib.rs +++ b/crates/oc-format/src/lib.rs @@ -61,19 +61,10 @@ pub const MIN_CHUNK_SIZE: u32 = 4 * 1024; /// Upper bound on chunk size. pub const MAX_CHUNK_SIZE: u32 = 1024 * 1024; -/// The single definition of which chunk sizes are valid. +/// Shared chunk-size validation for parsers, assemblers and host input handling. /// -/// Deliberately public: the header assembler, the parser, and the code -/// accepting the user's command-line value must all check the size. -/// A condition written in three places diverges at the first boundary change, -/// and does so silently: a file accepted by one check ends up -/// rejected by another. -/// -/// Input validation is protection, not convenience: the value reaches -/// `SecretBuf::with_capacity`, allocating and zeroing a buffer, even before -/// the header is assembled. Without this check, `--chunk-size 4000000000` -/// would attempt a four-gigabyte allocation, and allocation failure in Rust means -/// process `abort`, not an error that can be shown to the user. +/// Validate before buffer allocation: an arbitrary u32 size can request gigabytes, +/// and allocation failure may abort rather than return an ordinary input error. pub fn check_chunk_size(size: u32) -> Result { if (MIN_CHUNK_SIZE..=MAX_CHUNK_SIZE).contains(&size) && size.is_power_of_two() { Ok(size) @@ -175,24 +166,10 @@ pub enum FormatError { /// Distinct from the format version: a class can be added without changing the version, /// and accepting an unknown class would mean reading under class-zero rules. UnsupportedClass { class: u8 }, - /// The FORMAT version itself is outside the range this code can read. - /// - /// Distinct from [`FormatError::ReaderTooOld`], not cosmetically. There the number - /// means client version; here it means format version. Previously both values - /// entered one structure, reporting "this file requires client version 999" - /// for a file declaring `container_version = 999` and `min_reader_version = 1`. - /// A format version in a field meaning client version is precisely the substitution - /// of meanings this repository forbids in bytes and must also forbid - /// in diagnostics. + /// Container format version outside this reader's supported range. /// - /// `first` and `max` bound the READABLE range, not the format's history. - /// The name `first` dates from when the lower bound matched the first - /// version ever created; decision R-1 (2026-09-19) separated them: versions 1–4 - /// are no longer readable, and the field carries - /// `header::MIN_READABLE_CONTAINER_VERSION`, rather than - /// `header::FIRST_CONTAINER_VERSION`. - /// Renaming the field here would require edits unrelated to that decision; - /// a false doc comment would cost more. + /// Distinct from [`FormatError::ReaderTooOld`], which concerns min_reader_version. + /// The `first` and `max` fields bound readable format versions, not historical ones. UnsupportedContainerVersion { version: u16, first: u16, max: u16 }, /// A lease document version this build does not recognize. /// @@ -341,19 +318,11 @@ impl<'a> Prologue<'a> { Ok(Self { header, signature, after_signature: sig_end as u64, declared_len }) } - /// Independent construction of the signing string, **only for the comparison test**. - /// - /// There is one production implementation: [`crate::verify::header_signing_transcript`]. - /// This one assembles the same bytes by hand with a hardcoded label literal instead of - /// `label::HEADER_SIG`, which is the point: the test - /// `both_ways_of_building_the_signing_string_agree` compares them and fails - /// if someone changes the label or field order in one place and forgets - /// the other. + /// Independent signing-transcript construction for comparison tests only. /// - /// Gated by `cfg(test)`. When public, it was a second source of truth for - /// signed bytes in the crate's production surface. If someone called it - /// instead of the real implementation, divergence would become invisible because both - /// sides would compute using the same copy. + /// Uses a literal label and manual field assembly to catch drift in the production + /// [`crate::verify::header_signing_transcript`]. Gated by cfg(test) so callers cannot + /// accidentally choose a second production definition of signed bytes. #[cfg(test)] pub(crate) fn signing_transcript(&self, suite_id: u8, out: &mut Vec) { out.clear(); diff --git a/crates/oc-format/src/tlv.rs b/crates/oc-format/src/tlv.rs index 6496acf..5e0e9e6 100644 --- a/crates/oc-format/src/tlv.rs +++ b/crates/oc-format/src/tlv.rs @@ -1,19 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 -//! Header encoding: fields with numeric tags, lengths, and values. +//! TLV fields with strictly increasing numeric tags. //! -//! The format is closed and third-party developers do not need to read it, so CBOR -//! would add only canonicalization questions and a large parser. Instead, -//! a minimal TLV has one strict rule: **tags are strictly -//! increasing**. This rule automatically provides everything other -//! formats introduce "canonical encoding" for: duplicate tags are impossible, -//! field reordering is impossible, and two different byte sequences cannot -//! mean the same thing. Checking takes one comparison per field. -//! -//! Critical and optional fields are separated by tag range. An unknown -//! critical tag means the file uses semantics this client does not -//! understand, so it must not be opened. Unknown optional tags are ignored. -//! Without this distinction, every new significant field in the next format version -//! would cause all old clients to reject files. +//! Ordering rejects duplicates and reordered fields. Each codec separately checks +//! field lengths and canonical values. Tag ranges distinguish unknown critical +//! fields, which require rejection, from optional fields that can be skipped. +//! Slot parsing applies the same rule with slot-level rather than file-level rejection. use crate::FormatError; use core::ops::Range; @@ -162,19 +153,11 @@ impl<'a> TlvReader<'a> { } } -/// Field writing with the same increasing-tag validation. -/// -/// Write-side validation is not redundant with read-side validation: it prevents -/// generation of a header that our own reader would reject. +/// Write fields while enforcing increasing tags, matching reader validation. /// -/// The buffer is **zeroizing**, not merely an extra precaution. -/// This writer also assembles private metadata: the real filename, -/// whose concealment is why the `private_meta` field exists and is encrypted -/// with a separate K5 key. An ordinary `Vec` would return that name to the allocator -/// intact, leaving it readable in freed heap memory. Header encryption would -/// then protect the file on disk but not the process that assembled it. -/// Zeroizing does not harm public header fields: the cost is one memset -/// when the writer is destroyed. +/// The self-wiping buffer also carries private metadata such as filenames. +/// Zeroizing wipes owned contents on drop; it does not make Vec growth safe for +/// secrets or prove erasure of copies made by callers. #[derive(Default)] pub struct TlvWriter { buf: Zeroizing>, diff --git a/crates/oc-format/src/verify.rs b/crates/oc-format/src/verify.rs index 64c091f..1dc0253 100644 --- a/crates/oc-format/src/verify.rs +++ b/crates/oc-format/src/verify.rs @@ -53,19 +53,11 @@ pub enum SignerTrust { Conflict, } -/// Store of pinned author keys. +/// Pinned author-key store, returning the trust verdict for an organization. /// -/// Returns the complete verdict rather than "is this key known": only the store's owner -/// can distinguish "first seen" from "a different key is known", -/// and this layer has no basis for deciding on its behalf. -/// -/// `org_id` is required in the query as a consequence of the format. The author's -/// name is deliberately absent from the header (§2, tag 5): a name claimed in -/// the header proves no more than the header itself. The only anchor -/// for pinning is `org_id`, the tenant field. Hence the guarantee's -/// boundary: this catches key substitution **for a known organization**, but not -/// an attacker claiming to be a new organization; they simply become -/// unknown, which is the honest state. +/// Only the store knows whether the key is new or differs from a known key. +/// Pinning can detect substitution for a known org_id; an unknown organization +/// remains unknown rather than gaining trust from a name in the header. pub trait TrustStore { /// What is known about the "organization, author key" pair. fn lookup(&self, org_id: &[u8], author_key: &[u8; 32]) -> SignerTrust; @@ -97,44 +89,25 @@ pub struct VerifiedHeader<'a> { pub trust: SignerTrust, } -/// Algorithm suite identifier included in the header signature (§5). -/// -/// There is no separate scalar `suite_id` field in the header: the suite is defined by three -/// identifiers within `SUITE`. This value is therefore derived from the -/// parsed suite, specifically from the **signature algorithm**, -/// the sole algorithm on which the signature being verified depends. When [`SigAlg`] -/// gains a second value, this byte prevents presenting one scheme's signature as -/// another's if their encodings can be made to collide. +/// Signature-suite identifier included in the header transcript (§5). /// -/// The AEAD and tree hash identifiers deliberately do not enter this byte: they are -/// within the header bytes, all covered by the signature. A second encoding -/// of the same thing is a second place that may diverge from the first, -/// causing silent rejection of a valid file. +/// Derived from the signature algorithm rather than a separate header field. +/// AEAD and tree-hash identifiers are already authenticated inside the header. /// /// [`SigAlg`]: oc_crypto::SigAlg pub fn suite_id(suite: &Suite) -> u8 { suite.sig as u8 } -/// Exact byte string covered by the author's signature (§5). +/// Author-signature transcript (§5), shared by packers and verifiers. /// /// ```text /// "CC/v1/header-sig" ‖ 0x00 ‖ u8(suite_id) ‖ Magic ‖ u32le(HeaderLen) ‖ Header /// ``` /// -/// [`Transcript::new`] itself inserts the zero byte after the label, so it must not -/// be added here: a second zero would produce a string absent from -/// the specification, making files incompatible with any other implementation. -/// -/// The header comes last without a length prefix: its length is already bound by -/// the preceding field. [`Transcript`] provides -/// [`Transcript::tail_after_declared_length`] for this case: it states that the missing -/// prefix is deliberate, not forgotten. -/// -/// Public because the packer must sign exactly the same string. -/// Two independent constructions, one for writing and one for reading, could diverge and yield -/// a file our writer signs but our reader rejects, requiring byte comparison -/// to find the cause. +/// [`Transcript::new`] inserts the label's zero separator; do not add another. +/// HeaderLen already binds the trailing header, so use +/// [`Transcript::tail_after_declared_length`] without a second length prefix. pub fn header_signing_transcript( header_bytes: &[u8], suite: &Suite, @@ -157,21 +130,14 @@ pub fn header_signing_transcript( /// Parse and verify the container prologue. /// -/// Sequence (see module documentation for its departure from §5.1): -/// 1. structural boundaries: magic, length limit, sufficient bytes; -/// 2. header decoding to obtain the author key and algorithm suite; -/// 3. Ed25519 signature verification over the domain-labeled transcript; -/// 4. version negotiation: `min_reader_version` and the `container_version` range; -/// 5. signer trust determination using the store. -/// -/// Step 4 precedes **any** substantive use of fields: a file -/// requiring a newer client must be rejected even if everything else -/// parsed and its signature matched. The author key and suite are read earlier, -/// at step 2, but are not "applied": they only determine what to use and -/// what to compute the signature over. +/// 1. Check magic, length limit and available bytes. +/// 2. Decode the header to obtain author key and suite. +/// 3. Verify Ed25519 over the domain-separated transcript. +/// 4. Check min_reader_version and readable container_version range. +/// 5. Ask the store for signer trust. /// -/// Success means "the signature matches", **not** "the signer is trustworthy": -/// trust is returned separately in [`VerifiedHeader::trust`]. +/// Version checks precede substantive field use. A valid signature does not +/// establish signer trust; that verdict is returned in [`VerifiedHeader::trust`]. pub fn verify_and_parse<'a>( buf: &'a [u8], trust_store: &dyn TrustStore, @@ -242,20 +208,10 @@ pub fn verify_and_parse<'a>( }) } -/// Parse the prologue **without** signature verification. +/// Parse without checking the signature, for decoder fuzzing only. /// -/// Exists for exactly one use: decoder fuzzing, which needs -/// access to deep branches without dealing with signatures. -/// -/// Gated by `cfg`, not merely an awkward name. A name and `#[doc(hidden)]` are -/// requests to a reviewer; bypassing signature verification is too costly to rely -/// on requests: a function available in a production build will eventually -/// be called there, from a debugging branch, a diagnostic utility, or -/// a "temporary look inside". Now the `cc-cli` build lacks this -/// symbol entirely, making it impossible to call even deliberately. -/// -/// The fuzzer enables `fuzzing` explicitly: `--cfg fuzzing` (cargo-fuzz sets -/// it automatically). +/// Available only with cfg(fuzzing), set by cargo-fuzz through `--cfg fuzzing`. +/// Production callers cannot use this signature-bypassing entry point. #[doc(hidden)] #[cfg(any(test, fuzzing))] pub fn parse_without_verifying_signature_for_fuzzing_only( @@ -725,23 +681,11 @@ mod tests { } } - /// The reader accepts exactly the readable range and rejects both adjacent values. - /// - /// Both boundaries, not just the upper one (§2.1 item 3). A version below the readable range is - /// more dangerous than one above: it arouses no suspicion, "it is just an old file", - /// although it means the same thing: semantics we do not support. - /// - /// FOUR IN THE REJECTION LIST IS A POSITIVE CONTROL FOR DECISION R-1, - /// not just another round number. Zero and `MAX + 1` were rejected before the decision: - /// testing them would also pass with the old boundary. What distinguishes the new behavior - /// is precisely the lower neighbor: version 4, readable yesterday and permanently retired today. - /// One appears for the same reason, but is weaker: it verifies that the lower - /// bound moved away from [`FIRST_CONTAINER_VERSION`], retained as a historical record. + /// Accept the full readable range and reject its two adjacent values. /// - /// Iterating the range, rather than only [`CONTAINER_VERSION`], maintains the second - /// property this stage exists for: the reader learns a version BEFORE - /// the writer starts producing it. Today the bounds coincide and the iteration - /// has one step; it becomes substantive again when the reader version is bumped. + /// The lower neighbor distinguishes retirement of an old format from the former + /// upper-bound-only rule. Historical FIRST_CONTAINER_VERSION is not the readable + /// lower bound; keep this test tied to the reader range as it grows. #[test] fn the_reader_accepts_all_released_versions_and_refuses_the_neighbours() { let signer = signer(7); diff --git a/crates/oc-format/tests/kat_policy.rs b/crates/oc-format/tests/kat_policy.rs index 9dfb418..9d3be8a 100644 --- a/crates/oc-format/tests/kat_policy.rs +++ b/crates/oc-format/tests/kat_policy.rs @@ -11,30 +11,12 @@ clippy::arithmetic_side_effects, clippy::disallowed_methods )] -//! Frozen header vectors: policy, `core_hash`, and the mutable region. +//! Frozen policy, core-hash and mutable-region vectors. //! -//! They live in `oc-format`, not `oc-crypto`, because policy encoding and the -//! hash transcript live here. The vector file loader is repeated (twenty lines), -//! deliberately: the file format is primitive precisely so anyone can parse it, -//! including a second implementation. A second parser for it in this -//! repository is therefore not duplicated logic. -//! -//! ## What is frozen and why -//! -//! Two vectors, both closing gaps from the second review round. -//! -//! `policy_value`: **the complete policy bytes**. Before R-8 the byte layouts of -//! `validity` and `network` variants (first-byte discriminant, little-endian fields, requirement -//! of no trailing bytes for fieldless variants) and the distinction "field absent" ↔ "field present but empty" -//! for `max_opens` existed ONLY in code, although §4 declares its numbers normative. -//! Assembling the same bytes from the document was impossible. -//! -//! `policy_hash`: **the transcript**. Before R-10, §4 merely said "over raw bytes", -//! specifying neither algorithm nor boundaries. The convention here is opposite to `core_hash`: -//! only the record VALUE is hashed, without the six tag and length bytes, while `core_hash` excludes -//! entire records. An implementation reasoning by analogy would obtain the wrong result, -//! appearing as "the slot does not open; the file is damaged": `policy_hash` enters the -//! associated data of every slot. +//! `policy_value` covers discriminants, little-endian fields, trailing-byte rejection +//! and absent/empty distinctions. `policy_hash` hashes the record value without +//! its six-byte tag/length prefix. It enters slot AAD, so its boundaries must match +//! across implementations. use std::collections::BTreeMap; use std::path::PathBuf; @@ -201,20 +183,10 @@ fn frozen_content_desc() -> ContentDesc { } } -/// `core_hash` and the entire mutable region, including its MAC. -/// -/// Both values have been recorded as outstanding work in `tests/kat/README.md` since vectors -/// first appeared, and both are completed before version 1 freezes. -/// -/// `header_bytes` is deliberately frozen with `core_hash`. The hash is computed over -/// a **byte range**, not a re-encoding of a parsed structure -/// (§5), so a vector lacking the bytes would test only half the claim: a second -/// implementation could not tell whether it diverged in hashing or header -/// encoding. +/// Compare `core_hash` with its frozen header bytes and the full mutable region. /// -/// The mutable region is frozen fully assembled: `ContentDescLen(4) ‖ body ‖ -/// MAC(32)`, exactly what is in the file. Those are the 32 MAC bytes -/// §1.2 warns must not be omitted from arithmetic. +/// Hash raw byte ranges, not a re-encoded structure. The region includes +/// `ContentDescLen(4) ‖ body ‖ MAC(32)`; the MAC contributes to layout arithmetic. #[test] fn the_header_core_hash_and_content_desc_match_their_frozen_vectors() { let v = load("header.kat"); diff --git a/crates/oc-format/tests/security_probe_hostile_input.rs b/crates/oc-format/tests/security_probe_hostile_input.rs index c1a6efb..ccfa421 100644 --- a/crates/oc-format/tests/security_probe_hostile_input.rs +++ b/crates/oc-format/tests/security_probe_hostile_input.rs @@ -12,23 +12,11 @@ clippy::disallowed_methods, clippy::disallowed_types, clippy::assertions_on_constants )] -//! Area 3: parsing hostile input. +//! Hostile-input probes for headers, policies and content descriptors. //! -//! `parser_robustness.rs` covers `TlvReader`, `Prologue`, and `Layout`, but does not -//! reach `Header::decode`, `policy_codec::decode`, or -//! `ContentDesc::decode_verified`. This file closes that gap and also compares -//! parsing with the specification where divergence has already occurred. -//! -//! Three probes here (CEK wrapper, mutable region MAC transcript, and -//! `suite` members) were written against an EARLIER `docs/format.md` revision and asserted -//! the opposite of the current specification. They were explicitly revised: -//! each comment records what the earlier revision required and why -//! it could not be implemented. None was silently adjusted to the code: -//! every claim was checked against the current §1.2, §2, and §3.3 text. -//! -//! The harness holds ITS OWN signing key and MAC key; otherwise deep branches -//! (`verify_and_parse` after the signature, `ContentDesc::decode_body` after the MAC) -//! are unreachable. This is exactly what `docs/format.md` §5.2 requires. +//! The harness signs and MACs inputs with its own test keys to reach parsing after +//! authentication. It checks the current format contracts for the CEK wrapper, +//! mutable-region MAC transcript and suite members. use std::panic::{AssertUnwindSafe, catch_unwind}; @@ -584,21 +572,9 @@ fn superseded_value_transcript( t } -/// REVISED OPENLY, not silently. The previous probe was named -/// `the_content_mac_follows_the_transcript_named_by_the_specification` and -/// required the reader to accept a region authenticated with a transcript over -/// PARSED field values. That transcript appeared in the former §1.2 revision; -/// the current one specifies `fixed(file_id) ‖ field(body)` over raw bytes and -/// explains why no alternative works. -/// -/// The previous probe's requirement was not merely obsolete; it contradicted two -/// other format promises. A value-based transcript would (1) require both -/// parties to know ALL body fields, so a version 2 optional tag would break the MAC -/// for a version 1 client, disabling the extension point of §2.1 item 3 -/// precisely where promised; (2) require a separate "footer present" -/// flag byte although §1.2 explicitly says "no footer" and "footer at offset 0" -/// are distinguishable from body bytes alone; (3) reproduce the JWS/XML-DSig canonicalization bug: -/// parse one thing, authenticate another. +/// Authenticate `fixed(file_id) ‖ field(body)` over raw content-body bytes. +/// A transcript of parsed values would lose unknown optional fields and change +/// the specified authentication boundary. #[test] fn the_content_mac_covers_the_raw_body_bytes_so_unknown_optional_fields_still_verify() { let key = mac_key(); @@ -740,21 +716,8 @@ fn header_without(header_bytes: &[u8], tag: u16) -> Vec { w.finish().to_vec() } -/// REVISED OPENLY, not silently. The previous probe was named -/// `a_header_containing_exactly_the_fields_of_the_specification_is_readable` and -/// required the opposite: a header WITHOUT tag 17 should parse, and tag 17 itself -/// should be optional (> 0x7FFF). The probe relied on a revision of -/// `docs/format.md` whose §2 table ended at tag 16. The current -/// §2 has row 17 stating `wrapped_cek`, bytes[72], **required**; -/// absence means rejection. -/// -/// Neither of the two edits could satisfy the former probe legitimately. -/// Moving the field to the optional range would let an unaware reader -/// SKIP the sole wrapped content key, then fail to open a single byte -/// while reporting file corruption: precisely the silent -/// divergence the tag ranges are separated to prevent. -/// Accepting a header without tag 17 would validate a container -/// for which no decryption key can be obtained. +/// Require critical tag 17, `wrapped_cek`, with its specified 72-byte value. +/// Without this field the reader cannot recover the content key. #[test] fn the_wrapped_cek_field_is_mandatory_and_stays_in_the_critical_tag_range() { let full = sample_header([0x01; 32]).encode().unwrap(); @@ -780,20 +743,9 @@ fn the_wrapped_cek_field_is_mandatory_and_stays_in_the_critical_tag_range() { ); } -/// REVISED OPENLY, not silently. The previous probe was named -/// `the_suite_map_accepts_the_members_the_specification_lists` and required -/// `suite` with `kdf_id`(4) and `kem_id`(5) members to parse: `suite` had this -/// former §2 description. Current §2 says "exactly three -/// members: sig_alg(1), aead_id(2), tree_hash_id(3). Neither kdf_id nor kem_id" and -/// gives each omitted member a paragraph of explanation. -/// -/// Satisfying the former probe would create a second source of truth -/// for KEM: §3.3 specifies `kem_id` PER SLOT, the reason slots exist; -/// the server slot and author-device slot already use different KEMs. A file-wide -/// `kem_id` would either forbid this coexistence or disagree with a slot, -/// causing two implementations to read the file using different KEMs. `kdf_id`, meanwhile, has -/// exactly one valid value: a critical field the reader can only -/// silently ignore is explicitly forbidden by the format. +/// Accept exactly `sig_alg`, `aead_id` and `tree_hash_id` suite members. +/// KEM is specified per slot; a file-wide KEM would conflict with mixed slots. +/// The format has no selectable KDF field. #[test] fn the_suite_map_holds_exactly_the_three_members_of_the_specification() { let base = sample_header([0x01; 32]).encode().unwrap(); @@ -840,31 +792,10 @@ fn the_suite_map_holds_exactly_the_three_members_of_the_specification() { #[test] fn an_unknown_critical_field_inside_a_key_slot_is_not_silently_dropped() { - // §2: «тег ≤ 0x7FFF критичен, и неизвестный такой тег означает отказ - // открывать файл». Правило сформулировано как свойство ДИАПАЗОНА тега, а не - // одного конкретного контейнера, и `Header::decode`, `decode_suite`, - // `decode_authority`, `policy_codec::decode` его соблюдают. - // - // ИСТОРИЯ. Проба заводилась, когда `decode_slot` отбрасывал ЛЮБОЙ незнакомый - // тег: слот с известным видом и KEM, но с добавленным в версии 2 критичным - // ограничением, открывался клиентом версии 1 так, будто ограничения нет. - // Затем маятник ушёл в другую крайность — отвергался весь ФАЙЛ, и точка - // расширения, ради которой слоты заведены, оказалась закрыта: любой будущий - // вид слота делал контейнер нечитаемым даже для клиента, у которого есть - // собственный открываемый слот. - // - // Нынешнее правило (пункт С-6): непригоден СЛОТ, а не файл. Он отдаётся как - // `Unknown` — то есть сохраняется целиком и не используется, ровно как слот - // незнакомого вида или с незнакомым `kem_id`. - // - // Тег берётся МАКСИМАЛЬНЫЙ критичный, а не первый свободный, и это третья - // редакция этой строки. Сначала здесь стоял тег 7 — и стал NONCE. Потом тег 8 - // как «первый свободный» — и стал CLAIM_COMMIT вместе с кодом-претензией. В - // оба раза проба продолжала проходить, но проверяла уже не диапазон тега, а - // длину известного поля: реестр вырос, а проба об этом не узнала. - // - // `CRIT_TAG_MAX` — верхняя граница критичного диапазона. Реестр растёт снизу, - // поэтому дойти до неё он может только вместе с решением, которое заметят. + // An unknown critical field makes this slot unusable, without rejecting + // other usable slots in the container. Preserve it as `Unknown`. + // Use `CRIT_TAG_MAX` rather than the next unassigned tag so registry growth + // does not silently turn the test into a known-field length check. const UNKNOWN_CRITICAL: u16 = oc_format::tlv::CRIT_TAG_MAX; let mut slot = TlvWriter::new(); @@ -927,22 +858,10 @@ fn an_optional_unknown_tag_inside_suite_and_authority_is_skipped() { } } -/// SERVER ADDRESSES ARE PRINTABLE ASCII, CHECKED BY BOTH SIDES. -/// -/// # Why content is checked, not merely length -/// -/// The product only prints `authority.urls` for the user -/// to compare the author's addresses with their intended destination. The person's -/// consent is the protection mechanism, but the displayed string is chosen by -/// the file's author, generally an adversary. -/// -/// The three samples below break precisely that comparison. All are valid UTF-8 -/// and passed the entire previous check: +/// Reject non-ASCII and control characters in server addresses on both paths. /// -/// * `ESC` starts a control sequence: an address printed below -/// can erase the line printed above; -/// * U+202E reverses order, displaying `moc.dab` as `bad.com`; -/// * Cyrillic `а` is indistinguishable from Latin `a` in any font. +/// The samples cover terminal escape sequences, bidi reordering and a Cyrillic +/// homoglyph. These can interfere with the user's comparison of displayed addresses. #[test] fn a_server_address_outside_printable_ascii_is_refused_on_both_sides() { let hostile = [ diff --git a/crates/oc-format/tests/skeptic_recheck_slot_tags.rs b/crates/oc-format/tests/skeptic_recheck_slot_tags.rs index 6716688..87fa1f5 100644 --- a/crates/oc-format/tests/skeptic_recheck_slot_tags.rs +++ b/crates/oc-format/tests/skeptic_recheck_slot_tags.rs @@ -120,49 +120,11 @@ fn an_unknown_critical_tag_inside_a_key_slot_is_not_dropped_in_silence() { } } -/// A slot using a mechanism whose shape version 1 does not define must be skipped, -/// not reject the entire container. +/// Skip P-256 slots in version 1, whose slot-shape table does not define them. /// -/// This is item R-3, concerning the §3.3 promise: "a slot with a known kind but unknown -/// `kem_id` is skipped rather than rejecting the file". It was impossible to satisfy -/// for a subtle reason. The fallback "unknown mechanism → `Unknown`" -/// came AFTER the field parsing loop, while `enc` length was checked INSIDE it, -/// unconditionally requiring 32 bytes; the fallback was never reached. Worse, -/// a second issue made it unreachable: `KemAlg::from_u8` recognizes 2 and 3, -/// so P-256 never counted as an "unknown mechanism" at all. -/// -/// A P-256 public key takes 33 or 65 bytes, neither fitting in 32. -/// A P-256 slot was therefore not "unknown" but **impossible**: any such -/// container was entirely rejected even beside our own, -/// fully openable slot. F-6 planned precisely P-256 through a TPM. -/// -/// ## This test must break, and its tempting fix is wrong -/// -/// It depends on version 1's P-256 shape being UNDEFINED. F-6 defines it: the -/// `spikes/tpm-ecdh` spike showed PCP exports only uncompressed public keys, so -/// the wire will carry `0x04 ‖ X ‖ Y`, precisely the 65 bytes constructed below. From then on -/// the slot will stop being `Unknown`, become parseable, and fail this test. -/// -/// The failure will be CORRECT; the obvious fix will not. Changing the expectation to -/// `Known` turns a R-3 test into a test that P-256 works; -/// the "unknown mechanism is skipped rather than breaking the file" branch loses all -/// coverage, and the next regression of the same kind passes silently. -/// -/// ## How this was actually resolved -/// -/// The prescription above, "move to `kem_id = 3`", was correct when shape -/// depended only on mechanism. Version 2 made it a function of the PAIR (version, -/// mechanism), opening a better option: test both claims rather than -/// replace one with the other. -/// -/// This test stays about P-256 and is pinned to **version 1**, where its shape is -/// undefined and will remain so forever: what a version cannot do is frozen -/// as well as what it can. The R-3 claim remains verbatim, gaining the additional -/// role of preventing retroactive semantics in version 1. -/// -/// The "mechanism whose shape NO version defines" branch is guarded by the neighboring -/// `kem_id = 3` test in a version 2 container, where P-256 already parses. -/// Together they cover what either alone would cover only intermittently. +/// Version and mechanism together determine the shape. This guards version 1 +/// semantics even though version 2 supports P-256. The neighboring RSA-OAEP test +/// checks an undefined mechanism shape in version 2. #[test] fn a_slot_whose_kem_shape_this_version_does_not_define_is_skipped_not_fatal() { // Два слота: чужой (P-256 с 65-байтным `enc`) и наш. Контейнер версии 1. @@ -293,21 +255,9 @@ fn a_claim_slot_cannot_smuggle_bytes_in_fields_it_does_not_use() { } } -/// A claim-code slot declares `kem_id = 1`, always; this concerns BYTES. -/// -/// §2 item 5 says verbatim: "A claim-code slot (`kind = 3`) declares -/// `kem_id = 1` **always**, with zeroed `enc` and `nonce` of length 32 and 24. -/// Without this statement, zeroed `enc` length would depend on an unrelated default -/// mechanism choice, silently changing `claim.cc` bytes." -/// -/// This statement had no check, and the omission was INVISIBLE: a slot with -/// `kem_id = 2` carries 65 zeros instead of 32; both pass the "all zero" check, -/// and each length is valid for its own mechanism. Thus the same -/// `claim.cc` could be issued in two byte forms, both accepted. -/// For a format declared frozen, this is precisely the danger §2 item 5 warns about. -/// -/// Worse, both sides accepted it: the writer produced what the reader should -/// reject. An empirical probe found this, not code inspection. +/// Claim slots always use `kem_id = 1`, zero `enc` of 32 bytes and zero nonce +/// of 24 bytes. A different KEM would create another representation of the claim +/// slot even if its encapsulation bytes were all zero. #[test] fn a_claim_slot_must_declare_the_first_mechanism() { let claim_with = |kem: KemAlg, enc_len: usize| { @@ -359,21 +309,10 @@ fn a_claim_slot_must_declare_the_first_mechanism() { ); } -/// A mechanism whose shape NO version defines is skipped in a version 2 -/// container, where P-256 already parses. -/// -/// Companion to the test above; without it coverage would be partial. That test is pinned to -/// version 1 and prevents retroactive semantics there; this one guards -/// the skip branch itself, ensuring it still works in the current version rather than only the past. +/// Skip an RSA-OAEP slot in version 2: its number is known but its shape is not. /// -/// `kem_id = 3` (RSA-OAEP) is chosen because its registry number is assigned but its shape -/// is undefined and unplanned: `KemAlg::from_u8` recognizes it, so the -/// "unknown number" branch cannot apply. The slot must be skipped specifically because -/// its shape is undefined: exactly the case that once broke an entire container. -/// -/// The `enc` length is deliberately unlike 32 or 65: RSA-OAEP encapsulation -/// occupies hundreds of bytes; checking another mechanism's shape with our own measure -/// is precisely what must not happen. +/// The nonstandard `enc` length must not be checked against X25519 or P-256 +/// lengths before the undefined-shape fallback. #[test] fn a_mechanism_no_version_defines_is_still_skipped_in_version_two() { let mut foreign = TlvWriter::new(); @@ -401,22 +340,11 @@ fn a_mechanism_no_version_defines_is_still_skipped_in_version_two() { ); } -/// A compressed P-256 point is rejected by the FORMAT; that claim belongs here. -/// -/// The check was claimed in the wrong place. An `oc-crypto` probe said "P-256 rejects -/// compressed form" while feeding `agree` thirty-three `0x04` bytes. Rejection came -/// from SEC1 parsing (`0x04` prefixes an UNcompressed point; neither form looks like that), -/// not from its form. The agreement layer does not have the claimed property: -/// `from_sec1_bytes` parses both forms, and both yield the same secret. -/// -/// The ban lives in the exact-length table (§3.3: `enc` for `kem_id = 2` is exactly 65 -/// bytes, uncompressed SEC1 point `0x04 ‖ X ‖ Y`), upheld by I-8. The specification's -/// reason concerns BYTES: two valid forms of one key would yield two different -/// slot records for one recipient, hence divergent `core_hash` and signatures -/// between two conforming implementations. +/// Reject compressed P-256 encapsulations at the format length boundary. /// -/// EXACTLY `BadFieldLength { tag: ENC }` is required: "any error" is precisely -/// the wording that let the previous probe pass for the wrong reason. +/// Low-level agreement accepts valid compressed SEC1 points; the wire requires +/// 65-byte uncompressed points. Require `BadFieldLength { tag: ENC }` to prove +/// this rejection comes from the intended check. #[test] fn a_compressed_p256_point_is_refused_by_the_exact_length_table() { // Слот P-256 версии 2, отличающийся от корректного ровно длиной `enc`. diff --git a/crates/oc-policy/src/lib.rs b/crates/oc-policy/src/lib.rs index 96eba34..45c006f 100644 --- a/crates/oc-policy/src/lib.rs +++ b/crates/oc-policy/src/lib.rs @@ -165,19 +165,11 @@ pub struct Policy { pub max_opens: Option, pub network: Network, pub min_binding: Binding, - /// Binding requirement PER ACTION: policy tag 7, format version 4. + /// Per-action binding requirement (policy tag 7, format version 4). /// - /// Viewing may use software binding, editing may not: an edit signature - /// cannot be stronger than the machine's binding level. Hence this field: - /// a policy-wide `min_binding` alone is insufficient. - /// - /// The effective requirement for an action is `max(min_binding, the entry here)`, - /// so this field can only TIGHTEN it. This is a condition of - /// I-10 monotonicity, not a convenience: per-action relaxation would turn `intersect` into - /// a function capable of changing `Deny` into `Allow`. - /// - /// An absent entry means "no separate requirement", not "weaker binding - /// allowed": default denial remains enforced through `min_binding`. + /// The effective requirement is max(min_binding, this action's entry), so an + /// entry can only tighten policy. Absence adds no requirement and cannot relax + /// the policy-wide minimum. pub action_binding: BTreeMap, pub watermark: bool, /// Action tags unknown to this client version. Their presence by itself @@ -350,54 +342,19 @@ pub struct LeaseFacts { /// declare itself attested; the server saw the verified chain, /// and it arrives under the server's signature. See [`effective_binding`]. pub attested: Option, - // Поля `max_offline_seconds` здесь намеренно НЕТ, хотя план Ф-7 перечисляет - // его среди полей защиты от отката. - // - // Оно уже существует и стоит в правильном месте: `Network::Lease` в политике - // АВТОРА, и `OfflineTooLong` по нему проверяется — с тех пор как у `protect` - // появился флаг `--offline-window`. До него окно всегда равнялось длине - // лизинга, и проверка была недостижима. - // - // Сервер, желающий ужесточить окно, сделает это через `intersect(автор, - // сервер)`. В будущем времени намеренно: функция написана и монотонна, но вне - // тестов её никто не зовёт, и серверной политики не существует. Второе поле с - // тем же смыслом в лизинге завело бы вопрос «какое из двух главнее», а вопрос - // этот решается пересечением, а не приоритетом, — это и остаётся в силе. /// Device hardware clock reading at issuance. /// - /// Protects against what neither the system clock nor the monotonic floor can prevent: - /// VM snapshot rollback restores both the state file and clock. - /// A TPM counter does not move backward or revert with a snapshot, provided the TPM - /// is real. - /// - /// `None` means the lease was issued to a device without hardware time. This is valid - /// (unavailable at software level), but provides no such protection. + /// An independent reading can detect restored software state only if the hardware + /// clock did not roll back with it. None is valid for devices without hardware + /// time and provides no independent hardware-clock protection. pub tpm_clock: Option, } -/// TPM hardware clock reading: reset counter and time within it. -/// -/// A pair, not one number, with **both components compared independently**: -/// a decrease in EITHER means rollback. -/// -/// # Why this type has NO ordering -/// -/// This once derived `PartialOrd, Ord`, with docs explaining lexicographic order through the model -/// "the clock resets when the platform resets, while the counter increases". **That model -/// is wrong**: under TPM 2.0, `clock` is nonvolatile and survives power -/// loss; only `TPM2_Clear` resets it, also resetting the counter. -/// A different value, `time`, resets on power-up; the product does not use it: -/// see `cc_keystore::clock`. +/// TPM reset counter and persistent clock reading. /// -/// This was more than a wording error. Lexicographic order accepted "higher counter, -/// lower clock" as a legitimate reboot, obtainable in precisely one way: -/// roll back a snapshot and reboot. The snapshot rollback check -/// allowed snapshot rollback. The hole was fixed but the `derive` remained, and `a.max(b)` -/// would silently restore it. -/// -/// Ordering was therefore REMOVED, not redefined. A redefined order would need -/// explanation to every reader; removing it makes the invalid comparison inexpressible. -/// Raise the floor componentwise; see the example in `cc_authority`. +/// Compare both components independently: a decrease in either is rollback. +/// There is no Ord/PartialOrd because lexicographic comparison would accept a +/// higher counter with a lower clock. Raise a stored floor componentwise. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TpmClock { /// Number of platform resets. Increases, never decreases. @@ -421,22 +378,10 @@ pub struct TpmClock { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Context { pub now: Timestamp, - /// Source of `now`. **The evaluator does NOT read this field.** - /// - /// Stated explicitly because silence here is unusually costly. The client - /// populates it honestly, and reviewers read it as an effective self-imposed limit: - /// "time is declared unreliable, so it is trusted less". No such - /// rule exists: `evaluate` never accesses the field, and decisions - /// for `Wall` and `ServerAsserted` coincide. + /// Source of now, currently informational: evaluate does not read this field. /// - /// There is no exploitable hole: the source is always `Wall` anyway, - /// the worst possible source, and rollback protection comes from the monotonic - /// floor plus TPM time, not this field. The danger is a reviewer spending less attention - /// because of a nonexistent guarantee. - /// - /// The field will gain a consumer with a rule that requires it, - /// such as "`HardwareAttested` requires time independent of the system clock". That - /// is a new policy rule, a separate decision rather than incidental work. + /// Wall and ServerAsserted therefore receive the same evaluation. Rollback checks + /// use the monotonic floor and hardware readings, not a trust rule based on this tag. pub time_source: TimeSource, /// Greatest time the client has ever seen. Time cannot move /// backward: `now` below this value means a rollback attempt. @@ -524,21 +469,10 @@ pub enum DenyReason { /// is a machine property, suggesting "lease without hardware time"; a read failure /// is a transient event, suggesting "retry". TpmClockUnreadable, - /// Hardware time advanced faster than system time, indicating system clock rollback. - /// - /// TPM time advances only while the machine is powered on, so it cannot outpace wall - /// time under legitimate conditions. Exceeding it means precisely one thing: - /// elapsed system time was shortened. + /// Hardware elapsed time exceeds the allowed wall-clock elapsed time. /// - /// Distinct from [`Self::Expired`], substantively. "Expired" is - /// an ordinary event; "hardware time outpaced system time" reports clock manipulation, - /// which the operator needs to know. Users may read both alike; - /// diagnosticians do not. - /// - /// The former name `ExpiredByHardwareClock` described the former check: comparing - /// hardware elapsed time with lease length. It rejected legitimate - /// users whenever readings were taken long before issuance and was - /// replaced; the name changed with it so the fields would not lie. + /// This reports a clock-consistency failure separately from ordinary lease expiry. + /// The check compares elapsed readings, not hardware time against lease duration. HardwareClockOutranWallClock { hardware_ms: u64, wall_ms: u64 }, NotYetValid, Expired, @@ -730,24 +664,9 @@ pub fn evaluate( DeviceClock::Absent => return Verdict::Deny(DenyReason::TpmClockMissing), DeviceClock::Unreadable => return Verdict::Deny(DenyReason::TpmClockUnreadable), }; - // Откатом считается уменьшение ЛЮБОЙ из двух величин, а не проигрыш в - // лексикографическом сравнении пары. - // - // Лексикографика стояла здесь под моделью «часы обнуляются при сбросе - // платформы, а счётчик при этом растёт», и модель была неверна. По TPM 2.0 - // `clock` энергонезависим и переживает выключение питания; обнуляет его - // только `TPM2_Clear`, который обнуляет и счётчик сбросов. Собственный - // производитель в этом репозитории говорит то же самое: `time` обнуляется - // при включении, `clock` — нет, и берётся именно `clock`, чтобы не - // объявлять откатом перезагрузку. - // - // Цена ошибки была не в словах. При лексикографике пара «счётчик больше, - // часы меньше» проходила как законная перезагрузка — а получить её можно - // ровно одним способом: откатить снапшот и перезагрузиться. Проверка, - // заведённая против отката снапшота, пропускала откат снапшота. - // - // Перезагрузка по-прежнему проходит: счётчик растёт, `clock` продолжает - // расти вместе с ним, и ни одна из величин не уменьшается. + // A decrease in either reset counter or persistent clock means rollback. + // Lexicographic comparison would hide a lower clock behind a higher counter. + // A reboot with both components nondecreasing remains valid. if presented.clock_ms < issued.clock_ms || presented.reset_count < issued.reset_count { return Verdict::Deny(DenyReason::TpmClockRollback { issued, presented }); } @@ -791,54 +710,14 @@ pub fn evaluate( Network::StrictOnline => {} } - // Срок, измеренный АППАРАТНЫМИ часами, а не системными. - // - // # Зачем вторая мера, когда есть первая - // - // Первая мера — `presented < issued` выше — ловит только случай, когда сами - // часы TPM пошли НАЗАД, то есть откат снапшота вместе с виртуальным TPM. На - // живой машине с настоящим TPM они идут вперёд всегда, и эта проверка молчит. - // - // А класс атаки, ради которого всё затевалось, выглядит иначе: противник - // копирует профиль, исчерпывает открытия, восстанавливает копию и отводит - // системные часы назад. Профиль возвращает пол монотонности, счётчики и - // планку номера — все три меры откатываются вместе с файлом. Часы TPM при - // этом идут вперёд, поэтому и первая мера молчит. - // - // Измерено: контекст «профиль восстановлен, системные часы отведены» давал - // `Allow` там, где живое состояние давало `Deny(LeaseRollback)`. - // - // # Что делает эта проверка - // - // Меряет прошедшее время величиной, которую восстановление профиля не - // трогает. `clock_ms` идёт нарастающим итогом и не обнуляется при - // перезагрузке — в отличие от `time`, — поэтому разность годится как мера - // элапса через выключения питания. - // - // # Почему это ДОБАВЛЯЕТСЯ к системной проверке, а не заменяет её - // - // Часы TPM идут только пока машина включена. Пролежав неделю выключенной, она - // покажет элапс в несколько минут — то есть аппаратная мера одна пропустила бы - // просроченный лизинг. Системная мера этот случай ловит. Отказ по ЛЮБОЙ из - // двух строго сильнее каждой по отдельности, и обе односторонние, так что - // сложение их монотонно. + // Compare hardware and wall elapsed time to detect a restored software clock. + // The hardware reading can remain ahead even when profile state is restored. + // Keep wall-clock expiry too: hardware time need not advance while powered off, + // so it cannot replace the lease's wall-clock deadline. if let (Some(issued), Some(presented)) = (lease.tpm_clock, ctx.device.tpm_clock.reading()) { - // Сравниваются ДВА ЭЛАПСА, а не элапс с длиной лизинга. - // - // Первая редакция сравнивала аппаратный элапс с разрешённым сроком — и это - // было неверно по существу. Такая проверка отказывает всякий раз, когда - // показания сняты задолго до выдачи, то есть законному пользователю, - // запустившему `cc keygen` вчера. Сервер теперь приводит показания к - // моменту выдачи, но сравнение всё равно должно быть другим, и вот почему. - // - // Настоящий инвариант: часы TPM идут НЕ БЫСТРЕЕ настенных, потому что идут - // только пока машина включена. Значит аппаратный элапс, превысивший - // системный, означает ровно одно — системные часы отвели назад. Это и есть - // признак, а длина лизинга здесь ни при чём: срок по системным часам - // проверяется отдельной строкой ниже. - // - // Заодно исчезает целый класс ложных отказов: спящая или выключенная - // машина даёт аппаратный элапс МЕНЬШЕ системного, то есть проходит. + // Compare hardware elapsed time with wall elapsed time, not lease duration. + // Excess hardware elapsed indicates inconsistent wall time; shorter hardware + // elapsed after sleep or power-off is permitted. Wall expiry is checked separately. let hardware_ms = presented.clock_ms.saturating_sub(issued.clock_ms); let wall_seconds = ctx.now.0.saturating_sub(lease.issued_at.0); // Отрицательный системный элапс означает, что часы отвели за момент @@ -1005,24 +884,12 @@ pub fn intersect(author: &Policy, server: &Policy) -> Policy { const IMPOSSIBLE: Validity = Validity::Window { not_before: Timestamp(i64::MAX), not_after: Timestamp(i64::MIN) }; -/// Intersect two validity periods without ever expanding access. -/// -/// `Always` means no restriction, so intersecting it returns the other -/// operand. Homogeneous pairs narrow naturally: windows use the later start -/// and earlier end; first-open durations use the shorter duration. +/// Intersect validity periods without widening either operand. /// -/// A heterogeneous pair (window and first-open duration) cannot express its conjunction -/// in one variant: the restrictions are independent, and `Validity` cannot carry -/// both simultaneously. Choosing either **expands** access relative to the other, -/// and `intersect` may never expand access: this underpins -/// the promise that even a compromised server cannot weaken -/// the author's policy. Hence denial here. -/// -/// This currently imposes no practical restriction: leases are not issued yet, -/// and a server with nothing to tighten sends `Always`. When leases -/// arrive (phase 1) and a heterogeneous pair is needed, the correct solution is -/// a variant carrying both restrictions. That changes policy format and thus -/// requires a separate decision and `docs/format.md` update, not a silent choice here. +/// Always adds no restriction. Two windows use the later start and earlier end; +/// first-open durations use the shorter duration. A window/duration conjunction +/// cannot fit the current single variant, so deny instead of dropping a constraint. +/// Representing both would require a policy-format decision. fn tighten_validity(author: Validity, server: Validity) -> Validity { match (author, server) { (Validity::Always, other) | (other, Validity::Always) => other, @@ -1286,19 +1153,9 @@ mod tests { assert!(evaluate(&policy, Some(&lease()), Action::View, &c).is_allowed()); } - /// A clock lower than at issuance means rollback. - /// - /// Both components are checked, EACH separately: decreasing either - /// means rollback. - /// - /// This used to say "lower time with a HIGHER counter is not rollback: - /// the platform rebooted and the clock restarted". The caption outlived - /// its own refutation: the test body below requires DENIAL for that pair and - /// explains why: honest hardware cannot produce it, and there is exactly - /// one way to obtain it: roll back a snapshot and reboot. - /// - /// Reboot passes not because the clock "restarted", but because - /// BOTH values increase: `clock` is nonvolatile. + /// Decreasing either hardware-clock component must reject as rollback. + /// A higher reset counter cannot excuse a lower persistent clock; both components + /// nondecreasing is the valid reboot control. #[test] fn hardware_clock_going_backwards_is_a_rollback_but_a_reboot_is_not() { let policy = viewable(); diff --git a/crates/oc-protocol/src/access.rs b/crates/oc-protocol/src/access.rs index 1ebe1c8..3e0e4e3 100644 --- a/crates/oc-protocol/src/access.rs +++ b/crates/oc-protocol/src/access.rs @@ -172,44 +172,11 @@ pub fn decision_transcript(body: &[u8]) -> oc_crypto::Transcript { t } -/// Whether a character is suitable for a note. +/// Check whether a character is safe for a displayed note. /// -/// # Why NOT printable ASCII, although server addresses use it -/// -/// The first revision restricted notes to printable ASCII using the same rule that -/// prevents display spoofing in server addresses. The rule was correct, its transfer -/// was not; a live run between two machines caught this, not a test: -/// the Russian note "test from the HUAWEI laptop" was rejected by the server. -/// -/// These are different things. A server address has a WIRE representation that is ASCII by -/// construction: non-Latin names travel over the network as punycode, so the restriction -/// excludes nothing legitimate. A note has no separate wire representation: it is a phrase from -/// one person to another, and people write in their own language. The example in -/// [`AskAccess::note`] ("this is Peter from accounting", in Russian) would itself fail validation: -/// the code prohibited precisely what the documentation used as an example. -/// -/// # What is excluded -/// -/// The same categories as for filenames (finding V-8), for the same reason: the danger lies in -/// CATEGORIES, not letters. The set and rationale for each category are in -/// [`oc_format::text`]; only the REACTION is local: reject. -/// -/// The first revision carried its own copy of the list, the repository's fourth, and it was -/// incomplete in precisely the same place as the other three: nine Trojan Source code points -/// without the direction marks themselves (ALM, LRM, RLM). The copy is gone; the list is shared. -/// -/// # Why newlines are NO LONGER allowed -/// -/// The first revision allowed them: "a phrase may span two lines, and a newline cannot shift -/// output; it adds a line rather than rewriting its neighbor". That reasoning is valid -/// for OVERWRITING but misses LIST SPOOFING. A note is printed as a request-list -/// item, and a newline moves text outside that item: a note -/// containing "No. 2" and "device:" adds a nonexistent request to the displayed -/// queue. -/// -/// Additionally, the only note consumer already replaced `\n` with a dot, -/// so a two-line note was NEVER displayed on two lines. The format -/// allowed something the display could not render. +/// Unicode letters are allowed. The excluded categories come from +/// [`oc_format::text`]; control and direction characters can spoof display order. +/// Newlines are rejected because they could make one queue entry look like several. fn note_char_is_safe(c: char) -> bool { !oc_format::text::is_display_unsafe(c) } @@ -217,22 +184,13 @@ fn note_char_is_safe(c: char) -> bool { /// Maximum note length. A phrase, not a letter. pub const MAX_NOTE: usize = 256; -/// The note rule shared across the crate. -/// -/// # Why the tag number is a parameter rather than an internal constant +/// Validate a note using the shared character rules. /// -/// Because notes occur elsewhere: an action EXECUTION request -/// ([`crate::action::ActionRequest`], stage 2) also carries one, with its own field number -/// in its own registry. Copying these ten lines elsewhere would recreate the repository's -/// familiar problem: one path fixed, its neighbor forgotten. The excluded-category -/// sets would diverge at the first change to -/// [`oc_format::text`]. A parameter costs less than a copy, and the rejection identifies -/// THE tag containing the note; otherwise the user would read a field number from -/// another document. +/// `tag` identifies the field in errors; access and action requests use different tags. /// /// # Errors -/// [`FormatError::BadFieldLength`]: the note exceeds [`MAX_NOTE`]; -/// [`FormatError::BadNoteChar`]: it contains a character from an excluded category. +/// [`FormatError::BadFieldLength`] if the note exceeds [`MAX_NOTE`]; +/// [`FormatError::BadNoteChar`] for an excluded character. pub(crate) fn check_note(note: &str, tag: u16) -> Result<(), FormatError> { if note.len() > MAX_NOTE { return Err(FormatError::BadFieldLength { tag, len: note.len() }); @@ -375,20 +333,13 @@ impl core::fmt::Display for QueueError { } } -/// Parse a queue: consecutive entries, each with its own length. -/// -/// Each entry has a length rather than one for the whole body because entries vary: -/// the note and public key are variable-length. There is deliberately no total entry counter: -/// it would be a second source of truth for their number and could diverge from the body. +/// Parse a queue of length-prefixed entries. /// -/// Lives here rather than in the caller because parsing has two hosts, `cc-cli` and -/// `cc-wasm`, and a second implementation of one layout would silently diverge. Before -/// the move there was one implementation in `cc_cli::decide`, inaccessible to a -/// `wasm32` module. +/// Notes and public keys vary in size. The body determines the entry count; +/// there is no separate counter to reconcile. /// /// # Errors -/// [`QueueError`] if the body is truncated, an entry cannot be parsed, or the entry count exceeds -/// the window. +/// [`QueueError`] for truncation, malformed entries or a count beyond the window. pub fn split_queue(mut rest: &[u8]) -> Result, QueueError> { let mut out: Vec = Vec::new(); while !rest.is_empty() { @@ -468,26 +419,9 @@ pub fn decode_decision(bytes: &[u8]) -> Result { tag::CT => ct = Some(f.value.to_vec()), tag::AUTHOR_KEY => author_key = Some(f.array::<32>()?), tag::SIGNATURE => signature = Some(f.array::<64>()?), - // ЕДИНСТВЕННЫЙ РАЗБОРЩИК КРЕЙТА, ОСТАВШИЙСЯ СТРОГИМ (решение - // 2026-09-21). Необязательного диапазона у решения автора нет, и - // это не недосмотр. - // - // Подпись решения проверяется НЕ по сырым байтам, а по телу, - // собранному заново из разобранной структуры ([`decision_body`], - // зовётся в `cc_authority::Authority::decide_access` и в - // `cc_cli::granted::verified_decision`). Пропущенный тег из такой - // сборки выпадает — значит посторонний дописал бы его к уже - // подписанному решению, и подпись СОШЛАСЬ БЫ. У всех остальных - // документов крейта подпись покрывает сырые байты, и потому там - // дописать нельзя; здесь можно, и цена этому — самое дорогое поле - // разговора, доля B. - // - // Расширения необязательный диапазон здесь не даёт и в обмен: поле, - // которое новая сборка внесёт в `decision_body`, старая всё равно - // отвергнет по подписи. То есть выбор стоял между «ничего не - // приобрели» и «ничего не приобрели, но подписанный документ стал - // ковким», а И-6 держит ровно обратное — одна подпись, один - // документ. + // Reject every unknown tag here: signature verification reconstructs the + // body with `decision_body`. Skipping a field would remove it from the + // transcript and allow changes that leave the signature valid. other => return Err(FormatError::UnknownCriticalField { tag: other }), } } @@ -695,13 +629,8 @@ mod tests { assert!(decode_decision(&encode_decision(&d).unwrap()).is_err(), "отказ с долей"); } - /// THE APPROVAL FLAG IS EXACTLY 0 OR 1, NOTHING IN BETWEEN. - /// - /// Before 2026-09-20 it was read as "nonzero byte": a decision with byte 5 - /// parsed as approval and re-encoded as one, so two different - /// byte sequences meant the same thing (I-7). This test specifically catches - /// returning to that interpretation; `is_err()` is insufficient here, so - /// the rejection CODE is checked too. + /// Accept approval flags only as 0 or 1. + /// Check the error variant to distinguish this boundary from another rejection. #[test] fn the_approval_flag_is_exactly_zero_or_one() { // Смещение байта одобрения ищется по заголовку поля: тег 8, длина 1. diff --git a/crates/oc-protocol/src/action.rs b/crates/oc-protocol/src/action.rs index 40ee986..3d7050f 100644 --- a/crates/oc-protocol/src/action.rs +++ b/crates/oc-protocol/src/action.rs @@ -722,15 +722,10 @@ fn components(path: &str) -> impl Iterator { path.split('/').filter(|part| !part.is_empty()) } -/// Tree path: relative, no `..`, no `.`, no empty components. +/// Validate a relative tree path with no `.`, `..` or empty components. /// -/// # Why the character set is so narrow -/// -/// Because this path reaches the filesystem, and whatever it cannot express -/// the door can never do. There is no fallback: a path the -/// grant cannot encode is a path on which no action will execute. -/// Forbidding backslash and colon removes Windows drives, NTFS streams and -/// UNC paths at once; forbidding `..` prevents escaping the tree. +/// Reject backslashes and colons to exclude Windows drives, UNC paths and NTFS +/// streams. These lexical checks are part of the door's filesystem boundary. fn check_tree_path(text: &str, tag: u16) -> Result<(), FormatError> { if text.is_empty() || text.len() > MAX_ACTION_PATH { return Err(FormatError::BadFieldLength { tag, len: text.len() }); diff --git a/crates/oc-protocol/src/activation.rs b/crates/oc-protocol/src/activation.rs index 7a9bcfd..b0319f5 100644 --- a/crates/oc-protocol/src/activation.rs +++ b/crates/oc-protocol/src/activation.rs @@ -45,22 +45,11 @@ pub mod req_tag { /// Hardware clock reading: `u32le` reset_count ‖ `u64le` clock_ms ‖ /// `i64le` sampling time. Optional — absent on a machine without a TPM. pub const DEVICE_CLOCK: u16 = 8; - /// Operation identity (K28, `docs/protocol.md` §9.10). 32 bytes. - /// Optional — requests without it execute as before. - /// - /// # Why critical (≤ `0x7FFF`) - /// - /// Because the field changes the MEANING of retry: the server must return - /// the stored outcome rather than execute again. An unaware server that - /// silently skipped the field would retry by issuing a second grant — exactly - /// what the client sent the field to prevent. An unknown critical field fails parsing, - /// so an older server does NOT execute the request at all; the client learns - /// this through refusal without echo ([`super::grant_tag::OPERATION_ID`]). - /// - /// Activation-document parsing does not recognize the optional range anyway - /// (any unknown tag is rejected); this choice records not this build's parser - /// behavior but the number's meaning in the registry: a field without whose - /// meaning a request must not execute. + /// Operation identity (K28, protocol §9.10), 32 bytes; optional. + /// + /// This is a critical tag because it changes retry semantics: the server + /// returns the stored outcome instead of executing again. A server that does + /// not understand it must reject the request rather than issue another grant. pub const OPERATION_ID: u16 = 9; } @@ -198,20 +187,14 @@ pub enum Request { Requests { file_id: [u8; 16] }, /// Collect the author's decision, if already made. Collect(CollectReq), - /// File-event subscription: server keeps the connection and announces events itself. - /// - /// A subscription TAILS THE JOURNAL: `since` is the first record number - /// the subscriber has not seen (zero means from the beginning); the server first - /// sends everything from it, then live events. Disconnection thus loses nothing: - /// resubscribe with the last number and receive what was missed. Up to - /// [`MAX_WATCH_FILES`] files per subscription: one connection per client, - /// not per file. - /// - /// Requires no proof of possession, like `Requests`: notifications reveal - /// nothing absent from the public queue and journal; decisions - /// are still retrieved through `Collect` with proof. This request does NOT end - /// the conversation: notifications continue while the pipe lives or until the server - /// closes the subscription on expiry. + /// Subscribe to file events on this connection. + /// + /// `since` is the first unseen journal record; zero starts at the beginning. + /// The server sends the backlog, then live events for up to [`MAX_WATCH_FILES`] + /// files. Reconnect with the next unseen record number to resume. + /// Notifications expose public queue/journal data and need no possession proof; + /// decisions still require `Collect`. The conversation lasts until disconnect + /// or subscription expiry. Watch { files: Vec<[u8; 16]>, since: u64 }, /// Subscription to “all the author's files” (`docs/protocol.md` §9.9, B5). /// @@ -470,29 +453,15 @@ pub enum Response { ReplicaAck(Vec), /// Grant or link accepted and recorded. No content — the fact matters. ChainStored, - /// Holder's chain and, optionally, this server's lease-signing key. - /// - /// `documents`: grant and links in order, each preceded by a `u32 le` length - /// ([`join_chain`], [`split_chain`]). Opaque body rather than - /// parsed documents, for the same reason as the request queue: - /// their consumer parses them; a second parser in the envelope - /// would diverge. Link signatures cover bytes — they must not be rebuilt - /// in transit. - /// - /// `lease_verify_key` is `authority.lease_verify_key`, used by the door to - /// verify an ACTION lease (`crate::action::ActionLease`, stage 2, §4.3). - /// Usually the door obtains it from any grant file's header, where the author's - /// signature pins it; a door granted ONLY ACTIONS has no files, - /// so nowhere to obtain it. Optional because the stage-one server - /// did not send it, and a stage-two reader must read older - /// responses too; it adds no trust — the server names its own - /// public key, and a door with at least one grant file must - /// compare that value with the header. - /// - /// `action_grant`: this chain's `crate::action::ActionGrant` bytes, if - /// present. Signed with the author key and independently verified by the door; - /// without it, the door does not know its rules and cannot deny BEFORE networking - /// (see [`chain_tag::ACTION_GRANT`]). + /// Holder chain with optional lease-signing key and action grant. + /// + /// `documents` is an ordered grant/link stream with `u32le` lengths + /// ([`join_chain`], [`split_chain`]). Preserve the signed document bytes. + /// `lease_verify_key` supports action-only holders without a file header. + /// It is the server's claim, not a trust anchor; compare it with a signed + /// header when one is available. + /// `action_grant`, when present, requires separate author-key verification + /// before the door uses its rules for local checks. Chain { documents: Vec, lease_verify_key: Option<[u8; 32]>, @@ -616,19 +585,9 @@ const KIND_ASKED: u8 = 11; const KIND_DECIDED: u8 = 12; const KIND_WAITING: u8 = 13; const KIND_ACCEPTED: u8 = 14; -/// The author queries their file's queue. -/// -/// # Why in the COMMON registry, not the caller's +/// Author query for a file's request queue. /// -/// The first author-side revision defined this kind LOCALLY in `cc-cli`, because -/// the common envelope was unavailable. It built without warnings; -/// both sides passed their own tests — yet in a live run the server -/// answered “unknown critical field 15.” -/// -/// One socket must have ONE kind registry: the first byte chooses parsing -/// before the document's owner is known. Caught by an end-to-end run, not a unit -/// test — each side verified itself while diverging from the -/// other. +/// The message kind belongs to the shared registry used by both endpoints. const KIND_REQUESTS: u8 = 15; /// Its response: queue as consecutive `u32le length ‖ document`. const KIND_QUEUE: u8 = 16; @@ -753,22 +712,12 @@ pub mod chain_tag { /// the stage-one server did not send it; readers must accept responses /// without it (I-7). pub const LEASE_VERIFY_KEY: u16 = 0x8001; - /// This chain's complete author-signed ACTION grant. - /// OPTIONAL (tag > `0x7FFF`): the chain may have no action grant - /// at all; stage-one readers must accept responses without it (I-7). - /// - /// # Why the door needs it - /// - /// Otherwise the door does not know its permissions; “you were not granted this” - /// would come only from the server, AFTER the conversation. - /// The stage 2 specification (§5, step 2) requires the reverse: validate arguments - /// locally and return refusal with words BEFORE networking. Holder rules reside - /// in the signed action grant and optional link tag - /// (`agent::link_tag::ACTIONS`); the door receives links in the document stream, - /// and the root action grant here. - /// - /// This adds no trust: bytes arrive signed by the author key, and the - /// door verifies them itself (`agent::verify_grant_chain_with_actions`). + /// Complete author-signed action grant for this chain. + /// + /// Optional tag (`> 0x7FFF`); chains without action grants remain valid. + /// The door verifies these bytes using the author key, then combines the root + /// rules with link restrictions for local checks before networking + /// ([`crate::agent::verify_grant_chain_with_actions`]). pub const ACTION_GRANT: u16 = 0x8002; } @@ -851,9 +800,8 @@ fn decode_view_request(body: &[u8]) -> Result<(u64, u64), FormatError> { } const NOTICE_HEARTBEAT: u8 = 0; -/// Kinds 1–3 of the first revision (“request,” “decision,” “revocation”) were discarded -/// with it on 2026-09-02: notifications became journal records, making separate -/// wire kinds unnecessary — `event` distinguishes them. +/// Journal-event notification kind. Values 1–3 are retired; `event` identifies +/// the journal record type. const NOTICE_EVENT: u8 = 4; /// Event body: number (8) ‖ kind (1) ‖ file (16) ‖ has-fingerprint (1) ‖ /// fingerprint (32, zeros if absent). Fixed layout, exact length. diff --git a/crates/oc-protocol/src/agent.rs b/crates/oc-protocol/src/agent.rs index edf5503..b8e9a1a 100644 --- a/crates/oc-protocol/src/agent.rs +++ b/crates/oc-protocol/src/agent.rs @@ -249,26 +249,14 @@ pub fn decode_grant(bytes: &[u8], author_key: &[u8; 32]) -> Result Result { let (signature, body) = split_signature(bytes)?; let mut grant = decode_grant_body(body)?; @@ -661,16 +649,10 @@ fn check_delegation_shape(link: &Delegation) -> Result<(), FormatError> { Ok(()) } -/// Chain verification result: what the LAST link may access, and until when. +/// Verified access facts for the final link. /// -/// # Why ALL restrictions are returned instead of compared with each other -/// -/// Because no ordering relation exists on policies, and inventing one for -/// one check is inappropriate. Rather than proving "the link did not weaken its parent", the chain returns -/// the entire list, and the evaluator intersects it all: `oc_policy::intersect` -/// is monotonic only toward restriction, so an additional link can -/// TIGHTEN but cannot loosen permissions, whatever it contains. A check -/// that does not exist cannot be bypassed. +/// The caller must intersect all returned policies using `oc_policy::intersect`; +/// an extra restriction must never loosen an ancestor's permissions. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ChainFacts { pub grant_id: [u8; 16], @@ -785,27 +767,15 @@ pub fn verify_grant_chain( verify_grant_chain_with_actions(author_key, grant, links, None, now) } -/// The same, plus an ACTION GRANT (Agent Protocol, stage 2). -/// -/// # Why a separate function rather than a fifth argument to the old one -/// -/// Because the old call must work UNCHANGED: it has callers -/// in the server, door and stage 1 tests. Updating all of them -/// just to pass `None` would add noise obscuring the one place -/// where behavior changed. The old function remains exactly what it was -/// and delegates here: two routes to one check, not two checks. +/// Verify a grant chain with an optional action grant. /// -/// `actions` contains RAW [`crate::action::ActionGrant`] bytes signed with THE SAME -/// author key: actions have no container header, so the anchor is -/// the file grant, with its key likewise supplied externally. -/// -/// `None` means "nothing known about actions", and the facts then carry an empty -/// list, even when chain links DO carry action tags. There is nothing -/// to verify them against; unverified rules have no effect (I-10). This also -/// fulfills the promise that a stage 1 reader sees the old chain. +/// `actions` contains raw [`crate::action::ActionGrant`] bytes signed by the +/// same externally supplied author key as the file grant. +/// With `None`, the result has no action rules, even if links contain action +/// tags: those restrictions cannot be verified without the root action grant. /// /// # Errors -/// [`ChainRefusal`] with the link number where the chain broke. +/// [`ChainRefusal`] identifies the link where verification failed. pub fn verify_grant_chain_with_actions( author_key: &[u8; 32], grant: &[u8], @@ -924,20 +894,12 @@ pub fn verify_grant_chain_with_actions( Ok(facts) } -/// Child rules against parent rules: each must be no broader. -/// -/// # Why "any parent rule of the same kind", not "the first" +/// Check that each child rule narrows at least one parent rule of the same kind. /// -/// A parent may have multiple rules of one kind: two `tree.remove` -/// rules for two subtrees are ordinary. Requiring -/// matching ORDER would make verification depend on how the author -/// listed rules, rejecting legitimate narrowing for an arbitrary -/// reason. -/// -/// Rejection reason comes from the FIRST parent rule of the same kind: a rejection -/// without explanation passes any "rejected" test, but the user needs the -/// limiting constraint's name. If the parent has no such kind, the reason is -/// different and more precise: [`ChainRefusal::ActionNotGranted`]. +/// Multiple parent rules may cover different subtrees; their order does not +/// determine authorization. If none permits the child, report the first +/// matching rule's constraint, or [`ChainRefusal::ActionNotGranted`] when +/// no parent rule has that kind. fn narrow_actions( parents: &[crate::action::ActionRule], children: &[crate::action::ActionRule], @@ -1845,16 +1807,8 @@ mod tests { assert_eq!(facts.actions, rules); } - /// "ACTIONS ONLY" DOOR: A FILE GRANT WITH AN EMPTY FILE LIST IS VALID. - /// - /// Stage 2 specification (§4.1) relies on this and requests test confirmation: - /// a door needing only actions needs no files, yet its anchor - /// is still the file grant because an action has no container header - /// from which to obtain the author key. - /// - /// Until now, this was verified ONLY by code inspection (`check_entries` checks - /// the upper limit and says nothing about a lower one). "Accepted today" and "promised - /// to be accepted" differ: an accidental edit can change the former. + /// An action-only holder can use a file grant with an empty file list. + /// The verified grant still supplies the author-key anchor for action rules. #[test] fn a_file_grant_with_no_files_still_anchors_its_actions() { let mut g = grant(); diff --git a/crates/oc-protocol/src/attestation.rs b/crates/oc-protocol/src/attestation.rs index 09e5884..2de351f 100644 --- a/crates/oc-protocol/src/attestation.rs +++ b/crates/oc-protocol/src/attestation.rs @@ -1,29 +1,15 @@ // SPDX-License-Identifier: MPL-2.0 -//! Device key attestation documents on the wire (B6b, `docs/protocol.md` -//! §9.11.1). +//! Device key attestation documents (protocol §9.11.1, B6b). //! -//! # Three steps, and why not two +//! The Windows provider creates its attestation key together with evidence, +//! so the challenge must arrive first. The wire exchange is: +//! 1. Request a challenge and receive `Nonce`. +//! 2. Send `Evidence` (EK, attestation key and challenged attestation); receive credentials. +//! 3. Return the decrypted secret and receive the verdict. //! -//! The model in §9.11 has two verifier moves: credentials for the attestation -//! key's name under the EK, then the secret and the attestation. The Windows provider works differently: -//! the attestation key is created TOGETHER with the attestation (`KAST` wrapper, B6b measurement), and -//! the attestation needs the verifier's challenge beforehand. Hence three wire steps: -//! -//! 1. The device requests a challenge; the server responds with `Nonce`. -//! 2. The device sends complete `Evidence`: EK, attestation key and attestation -//! with that challenge, and receives credentials. -//! 3. The device returns the decrypted secret; the server delivers its verdict. -//! -//! All three requests follow proof of possession and are protected by the session MAC (K25): -//! attestation concerns the key whose possession was proved in this same -//! conversation, not a key merely named by the peer. -//! -//! # What is not verified here -//! -//! TPM structures and certificates within are bytes: the verifier parses them -//! (`cc_authority::attest`), strictly and with its own limits. This module handles only -//! the document layout and length caps, so that envelope parsing cannot allocate memory -//! based solely on the sender's claims. +//! All requests follow possession proof and use the session MAC (K25). +//! This codec checks layouts and length caps. TPM structures and certificates +//! remain opaque bytes for the attestation verifier to validate. use oc_format::tlv::{TlvReader, TlvWriter}; use oc_format::FormatError; diff --git a/crates/oc-protocol/src/attribute_rule.rs b/crates/oc-protocol/src/attribute_rule.rs index d45ccb5..a9645fc 100644 --- a/crates/oc-protocol/src/attribute_rule.rs +++ b/crates/oc-protocol/src/attribute_rule.rs @@ -1,39 +1,22 @@ // SPDX-License-Identifier: MPL-2.0 -//! File rule based on holder attributes: bytes (F-27, B4b). +//! File rules based on holder attributes (F-27, B4b). //! -//! # Why the rule lives here rather than only on the server -//! -//! Before B4b, the rule was a type in `cc-authority` and a codec in its -//! store: only the operator set it, using a command on the server machine, and -//! its bytes never left the state file. With a signed author order -//! (`order::Kind::SetRule`), those same bytes travel over the wire under a signature; -//! a document signed and verified by different parties must have a single -//! definition, located alongside the other documents. -//! -//! The layout matches the store's pre-move output byte for byte: -//! old server state files remain readable unchanged, and the state -//! fingerprint does not change on upgrade. The store calls this same codec. +//! The server store and signed `order::Kind::SetRule` use this shared codec. //! //! # Layout -//! -//! TLV, tags in ascending order, all critical: +//! TLV with ascending critical tags: //! //! | Tag | Field | Value | //! |---|---|---| -//! | 1 | lease lifetime cap | `i64le` seconds, positive; optional | -//! | 2 | issuance conditions | condition stream; always written, even when empty | +//! | 1 | lease lifetime cap | positive `i64le` seconds; optional | +//! | 2 | issuance conditions | condition stream; written even when empty | //! | 3 | restrictions | stream of `[condition, profile]` pairs; only when present | //! -//! A stream is consecutive `u32le length ‖ bytes`. A condition is `u8 mode ‖ stream [attribute, -//! value…]`. A profile uses the policy codec (`policy_codec`, container version). -//! -//! # What parsing rejects itself -//! -//! A name or value that is not an identifier; a condition without values; "at least" with -//! other than one value; an unknown mode; limits on conditions, values and -//! restrictions; a nonpositive lifetime cap; an empty restrictions stream: -//! the writer never produces one, so it cannot have come from the writer. The meaning of names -//! (whether an attribute exists in the dictionary) is checked by the server, which owns the dictionary. +//! Streams contain `u32le length ‖ bytes` entries. A condition is +//! `u8 mode ‖ stream [attribute, value…]`; profiles use `policy_codec`. +//! Parsing checks identifiers, modes, counts, a positive cap and nonempty +//! restrictions. Conditions require values; "at least" requires one value. +//! The server separately checks names against its attribute dictionary. use oc_policy::Policy; diff --git a/crates/oc-protocol/src/control.rs b/crates/oc-protocol/src/control.rs index 5525b04..bf762ea 100644 --- a/crates/oc-protocol/src/control.rs +++ b/crates/oc-protocol/src/control.rs @@ -1,26 +1,16 @@ // SPDX-License-Identifier: MPL-2.0 -//! Authority control documents (E2, B2; `docs/protocol.md` §9.16). +//! Authority control documents (protocol §9.16, E2/B2). //! -//! Three documents, one codec: +//! * [`Binding`] describes server identity, epoch/revision, keys, addresses, +//! durability, recovery, service state and control roster. Its embedded key +//! must match the externally trusted anchor used by [`Binding::open`]. +//! * [`ControlRequest`] carries scope, operation ID, expected revision, +//! lifetime and content. Multiple signatures cover the same intent body. +//! * [`Receipt`] records the outcome, revision and committed content, including +//! whether execution awaits replica acknowledgment. //! -//! * [`Binding`]: server binding: organization, stable identity, -//! epoch and revision, keys, addresses and TLS fingerprints, durability profile, -//! recovery mode, service state and control-key roster. -//! Signed with the lease-signing key pinned by the author in the header. -//! **The document itself creates no trust**: it is verified with a key known -//! to the verifier beforehand ([`Binding::open`]); the embedded key field must -//! match it. -//! * [`ControlRequest`]: controller intent: scope (organization, -//! identity, epoch), operation identity, expected revision, lifetime and exact -//! content. Multiple signatures are allowed, all over ONE body: a -//! quorum signs one intent, and signatures over different intents cannot -//! be combined. -//! * [`Receipt`]: server receipt: operation outcome, resulting -//! revision and precisely what was committed. Distinguishes acceptance from execution -//! and from deferral pending replica acknowledgment. -//! -//! Layouts are TLV with ascending tags (I-7), all fields critical. -//! Signature transcripts are `CC/v1/authority-binding`, `CC/v1/control-request`, +//! Layouts are TLV with ascending critical tags. Transcript labels are +//! `CC/v1/authority-binding`, `CC/v1/control-request`, and //! `CC/v1/operation-receipt` over the body. use oc_format::FormatError; @@ -388,24 +378,15 @@ impl Binding { Ok(out) } - /// Parse and verify using a key the verifier trusts BEFOREHAND. - /// - /// The binding's embedded key must match the anchor; otherwise the binding - /// would declare its own signing key trusted. - /// - /// # Why signature verification PRECEDES parsing + /// Verify with an externally trusted key, then parse the binding. /// - /// The same I-5 rationale as [`open_request`]: parsing unauthenticated bytes - /// returns distinguishable rejection codes for data nobody signed. The key - /// comes from OUTSIDE (`anchor`), so the body need not be parsed before verification; - /// [`Self::peek`] separates the signature itself and is called over already - /// authenticated bytes. Where no anchor exists, `peek` extracts the key, - /// with a known tradeoff: in [`verify_chain`], the new epoch binding is first - /// read with `peek` to obtain its own key, but trust comes not from that - /// signature; it comes from the roster's signature over the certificate carrying those bytes. + /// The embedded key must match `anchor`; a binding cannot establish its own + /// trust. Verification precedes TLV parsing so malformed unauthenticated + /// bodies do not produce parsing errors. In [`verify_chain`], `peek` finds a + /// new epoch's key; trust comes from the roster-signed transfer certificate. /// /// # Errors - /// [`FormatError`]: layout, signature or anchor mismatch. + /// [`FormatError`] for invalid layout, signature or anchor mismatch. pub fn open(bytes: &[u8], anchor: &[u8; KEY]) -> Result { let (sig, body) = split_signed(bytes)?; oc_crypto::sign::verify(anchor, &transcript(label::AUTHORITY_BINDING, body), &sig) @@ -829,38 +810,15 @@ fn split_request(bytes: &[u8]) -> Result<(Vec, &[u8]), FormatError> { Ok((signatures, rest)) } -/// Parse an intent and verify EVERY signature. -/// -/// The server determines who may sign from the binding roster; this only checks -/// that "the signature verifies with the named key". -/// -/// # Why signature verification PRECEDES parsing +/// Verify every intent signature, then parse the body. /// -/// The same I-5 rationale as [`crate::revocation::verify_signed`] and its neighbors: -/// parsing unauthenticated bytes is itself an oracle; distinguishable rejection codes -/// (`MissingField`, `UnknownCriticalField`, `BadFieldLength` with a tag number) -/// report on data nobody signed, distinguishing forgery -/// from truncation. Nothing here REQUIRES parsing before verification: verification keys -/// are in the ENVELOPE, which `split_request` separates before any TLV, -/// unlike the container header, whose signing key resides within the -/// signed material and cannot be obtained from outside. -/// -/// # Empty envelope -/// -/// Zero signatures would trivially pass the loop, allowing unauthenticated body parsing; -/// reordering lines within this function would not prevent that. -/// An EARLIER boundary must prevent it. It exists: `split_request` rejects -/// `count == 0` before reading signatures (test -/// `documents_beyond_the_size_limits_are_refused_before_parsing`), so an unsigned -/// document never reaches the loop, leaving nothing to improve here. -/// -/// The caller counts the quorum, how many signatures and WHOSE; both sites are known: -/// `quorum_met` in `cc-authority/src/control.rs` (using the current revision's roster), and -/// comparison with the previous epoch's `roster`/`threshold` in [`verify_chain`], reached by -/// `cc_cli::chain::check`. +/// Envelope keys authenticate bytes; the caller checks signer authorization +/// and quorum against the binding roster. `split_request` rejects empty +/// signature lists before this verification loop. Authentication precedes TLV +/// parsing to avoid detailed errors for unauthenticated bodies. /// /// # Errors -/// [`FormatError`]: layout or any failed signature. +/// [`FormatError`] for invalid layout or any failed signature. pub fn open_request(bytes: &[u8]) -> Result { let (signatures, body) = split_request(bytes)?; let t = transcript(label::CONTROL_REQUEST, body); @@ -1186,21 +1144,13 @@ impl Transfer { } } -/// Parse a transfer certificate and verify EVERY signature. -/// -/// The verifier determines who may sign from the previous binding's roster; -/// this only checks that "the signature verifies with the named key". +/// Verify every transfer signature, then parse the certificate. /// -/// # Why signature verification PRECEDES parsing -/// -/// The rationale and empty-envelope caveat are the same as [`open_request`], documented -/// there: both use the same envelope (`split_request`), yet these two -/// combinators already diverged once, precisely the "one path fixed, its neighbor -/// forgotten" problem captured by the test -/// `a_transfer_opens_only_while_every_signature_holds`. +/// The caller checks signer authorization against the previous binding's +/// roster. Authentication and empty-envelope handling follow [`open_request`]. /// /// # Errors -/// [`FormatError`]: layout or any failed signature. +/// [`FormatError`] for invalid layout or any failed signature. pub fn open_transfer(bytes: &[u8]) -> Result<(Transfer, Vec<[u8; KEY]>), FormatError> { let (signatures, body) = split_request(bytes)?; let t = transcript(label::AUTHORITY_TRANSFER, body); @@ -1510,21 +1460,9 @@ mod tests { assert!(Receipt::open(&bytes, &other.public_key()).is_err()); } - /// A TRANSFER CERTIFICATE OPENS ONLY WHILE EVERY SIGNATURE VERIFIES. - /// - /// # Why a separate test from the intent - /// - /// Because `open_transfer` and `open_request` are two DIFFERENT combinators, - /// and only the second was guarded. Removing `oc_crypto::sign::verify` from - /// `open_transfer` broke no tests: the certificate was parsed, - /// compared with itself (`transfer.body()? != body`) and declared open, - /// so a transfer of authority to a new epoch would be accepted without a signature - /// from the previous roster. CLAUDE.md describes precisely this problem as "one path - /// fixed, its neighbor forgotten". - /// - /// EVERY byte is tested, not three selected positions: the body does not authenticate - /// signature bytes or named-key bytes itself; their only guard - /// is that signature check. + /// Mutate every byte of the transfer's keys, signatures and body. + /// Each must be rejected; this guards `open_transfer` independently of + /// `open_request`, since they use separate verification combinators. #[test] fn a_transfer_opens_only_while_every_signature_holds() { let s = server(); @@ -1559,23 +1497,10 @@ mod tests { } } - /// AN UNPARSEABLE BODY IS REJECTED BY SIGNATURE VERIFICATION, NOT PARSING. - /// - /// The rationale is I-5 for signed documents: a distinguishable parsing rejection - /// for a body nobody signed is an oracle. The body here is deliberately - /// unparseable (unknown CRITICAL tag `0x7ABC`), so both - /// boundaries want to reject it, revealing which responds first. - /// - /// # Why the test would be blind without a positive control - /// - /// Because an invalid signature returns `BadHeaderSignature` even with - /// reversed ordering, for ANY parseable body. Only a pair distinguishes - /// the order: that same body with a VALID signature must yield - /// exactly [`FormatError::UnknownCriticalField`]. If both halves passed under - /// reversed ordering, the test would not be testing order. - /// - /// Both entry points intentionally share one test: their envelope is shared, yet they - /// have already diverged once. + /// Use the same malformed body (critical tag `0x7ABC`) with invalid and + /// valid signatures. The first must fail authentication; the second must + /// reach parsing and return `UnknownCriticalField`. This pair detects + /// reversed verification/parsing order at both entry points. #[test] fn an_unparsable_body_is_refused_by_the_signature_first() { let a = Ed25519Signer::from_seed(&[0x71; 32]); diff --git a/crates/oc-protocol/src/directory.rs b/crates/oc-protocol/src/directory.rs index cb5864d..3744924 100644 --- a/crates/oc-protocol/src/directory.rs +++ b/crates/oc-protocol/src/directory.rs @@ -1,22 +1,12 @@ // SPDX-License-Identifier: MPL-2.0 -//! Organization key directory: a record, its leaf and proof -//! (`docs/protocol.md` §9.14, D4). +//! Organization key-directory records and inclusion proofs (protocol §9.14, D4). //! -//! # What a record asserts +//! A record is the server's claim linking an organization/member to a key, +//! version and origin; it does not prove that person's control of the key. +//! Signed and witnessed journal heads let monitors detect conflicting views. //! -//! "The server for organization `tenant` identifies key `public` (mechanism `kem`, -//! K27 fingerprint `fpr`) as the key of member `name`, version `version`, -//! origin `origin`." This is a SERVER assertion; by itself, it does not -//! prove that this person controls the key: the name is the operator's claim, not -//! cryptography. The directory journal serves a different purpose: it prevents the server from silently -//! showing different records to different people. The leaf is the record's hash; the head -//! is signed by the server and witness (`crate::witness`, kind `Directory`). -//! -//! # What is absent here -//! -//! Proof of ABSENCE: "there is no record for this name" is only the server's -//! claim. Completeness is checked by a monitor that reads the entire journal -//! (`DirectoryRecords`) and compares its root with the cosigned head. +//! There is no absence proof here. Completeness requires reading the full +//! `DirectoryRecords` journal and comparing its root with the cosigned head. use oc_format::FormatError; use oc_format::tlv::{TlvReader, TlvWriter}; @@ -395,20 +385,14 @@ impl Lookup { }) } - /// Verify an answer to "(organization, member)": the head signature, - /// the record's inclusion under that head, and that the record concerns the requested member. - /// - /// # Order is essential here + /// Verify the head signature, record inclusion and requested member. /// - /// Head signature first, record inclusion second, record parsing - /// only third. Record bytes are authenticated not directly by the head signature but by - /// the inclusion proof, and the leaf is computed from RAW bytes - /// ([`Record::leaf`]); no parsing is needed to check it. Parsing them - /// earlier would let [`LookupError::Malformed`] report on a record not present - /// in the journal at all (the same I-5 rationale as for signed documents). + /// Authenticate the head first, then prove inclusion of the raw record bytes + /// ([`Record::leaf`]), then parse. This avoids detailed parsing errors for + /// records not authenticated by the journal. /// /// # Errors - /// [`LookupError`]: which check failed. + /// [`LookupError`] identifies the failed check. pub fn verify(&self, server_key: &[u8; 32], tenant: &str, name: &str) -> Result { if !self.head.signed_by(Log::Directory, server_key) { return Err(LookupError::ServerSignature); diff --git a/crates/oc-protocol/src/lease.rs b/crates/oc-protocol/src/lease.rs index aafc26e..0e5d346 100644 --- a/crates/oc-protocol/src/lease.rs +++ b/crates/oc-protocol/src/lease.rs @@ -180,21 +180,11 @@ pub fn encode(lease: &Lease) -> Result, FormatError> { Ok(w.finish().to_vec()) } -/// Parse a lease body. **The result is NOT VERIFIED.** +/// Parse a lease body without verifying its signature. /// -/// No signature is checked here, so the returned [`Lease`] is not -/// evidence of anything: it is exactly as truthful as -/// the bytes supplied to it. Access decisions must not be based -/// on such a value. -/// -/// # What to call instead -/// -/// For EXTERNAL bytes, use [`verify_signed`]: it verifies the signature over the raw body -/// BEFORE parsing, enforcing the order that callers otherwise must remember -/// and might forget. This function remains public not to provide a choice of -/// convenience, but for one legitimate case: parsing one's OWN issuance, -/// where the reader is the same party that signed it and has nothing to verify -/// (`cc-authority`, replay of a stored response). +/// The result is untrusted and must not authorize access. For external +/// `signature ‖ body` bytes, use [`verify_signed`]. This parser also supports +/// replaying one's own stored issuance. pub fn decode(body: &[u8]) -> Result { let mut reader = TlvReader::new(body); @@ -338,29 +328,15 @@ pub fn verify( oc_crypto::sign::verify(lease_verify_key, &signing_transcript(body), signature) } -/// Verify the signature, then parse: in this order only. -/// -/// `bytes` is the complete document: `signature(64) ‖ body`. -/// -/// # Why a combinator while both halves remain available -/// -/// Because the CALLER was responsible for "signature → parsing", and the product -/// had to wrap these two calls itself (`cc_cli::lease`), although -/// neighboring documents, [`crate::revocation::verify_signed`] and -/// [`crate::order::verify_signed`], had that wrapper from the start. Parsing -/// unauthenticated bytes is itself an oracle: distinguishable error codes report on -/// something nobody signed (I-5 for documents). A single call prevents mistakes -/// in the ordering. -/// -/// The signature is checked over the RAW body bytes, not a reconstructed structure: -/// the module rule that eliminates a class of canonicalization errors. +/// Verify the signature over raw body bytes, then parse the lease. /// -/// The caller checks that `file_id` and the policy hash match the container: there is -/// no container here. +/// `bytes` is `signature(64) ‖ body`. Authentication precedes parsing to avoid +/// detailed errors for unauthenticated data. The caller must separately check +/// `file_id` and policy hash against the container. /// /// # Errors -/// [`FormatError::BadHeaderSignature`] for a short document or a -/// failed signature; otherwise parsing errors, as in [`decode`]. +/// [`FormatError::BadHeaderSignature`] for a short document or failed +/// signature; otherwise the parsing errors of [`decode`]. pub fn verify_signed(bytes: &[u8], lease_verify_key: &[u8; 32]) -> Result { let (signature, body) = bytes.split_at_checked(SIGNATURE_LEN).ok_or(FormatError::BadHeaderSignature)?; diff --git a/crates/oc-protocol/src/order.rs b/crates/oc-protocol/src/order.rs index 5717481..d39d8a1 100644 --- a/crates/oc-protocol/src/order.rs +++ b/crates/oc-protocol/src/order.rs @@ -1,31 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 -//! An author's order to the server: what to do with a file. +//! Author-signed orders to the server (protocol §9.3). //! -//! Before 2026-09-03, registration and revocation were operator `cca` commands on -//! the server machine, so who counted as the author depended on who had access to -//! that machine. This cannot work over the wire: `file_id` is public in the header, -//! and without a signature anyone receiving the file could register it under their own -//! terms or revoke it for everyone (`docs/protocol.md` §9.3). +//! Registration carries the container header: its verified signature supplies +//! the author key used to verify the order. Later file orders use the key +//! remembered at registration. The layout is `signature(64) ‖ TLV body`. //! -//! An order is a TLV document signed by the AUTHOR KEY, the same key that -//! signed the container header. Registration carries the entire header alongside it: -//! the server verifies the header signature, extracts the author key and uses it -//! to verify the order. The server learns the author key not from the requester's claims but -//! from a document the recipient cannot modify. Revocation is verified using -//! the key remembered during registration. -//! -//! The layout matches revocations and leases: `signature(64) ‖ body`. The signature -//! comes first so that a truncated document cannot parse "almost successfully". -//! -//! Issuance time is part of the body and signature: the server accepts only -//! fresh orders (allowing clock skew), so a document resurfacing -//! after lying somewhere is not executed. -//! -//! Freshness is insufficient, although this documentation formerly claimed otherwise: "both operations are idempotent, -//! replay does nothing". That described two kinds, registration and -//! revocation; there are now eight, and replaying a vote, roster or thaw changes -//! state. Order BETWEEN commands is maintained by the server using a per-kind barrier -//! (`docs/protocol.md` §11.11), not by this check. +//! Issuance time is signed; the server checks freshness with clock-skew +//! allowance. Freshness alone does not prevent replay or reordering: the +//! server also maintains per-kind barriers (protocol §11.11). //! There are more than two order kinds: author proof of life and heir appointment //! joined registration and revocation (`docs/protocol.md` §11). Layout, @@ -96,38 +78,15 @@ pub mod tag { pub const SILENCE_SECONDS: u16 = 11; /// `SetHeir` only: what to do after silence. pub const HEIR_MODE: u16 = 12; - /// `SetHeir{Open}` only: completed author decisions for heirs, as a stream. - /// - /// # Why a stream, and why the number was not retired + /// Completed heir decisions for `SetHeir{Open}`. /// - /// The tag was created for ONE decision. Multiple heirs were needed (customer decision - /// of 2026-09-05), so the contents became a stream of - /// `u32le length ‖ Decision`, repeated up to `MAX_HEIRS` times. - /// - /// The number was not repurposed for a different meaning; the same meaning was expressed - /// more precisely: "who gets what after silence". I-7 forbids the former, not - /// the latter. Retiring the number would be superstition rather than caution: no - /// server with an appointed heir has shipped, and protocol documents other than - /// leases are not frozen (`docs/protocol.md` §0). A single heir remains - /// a valid case: a one-entry stream. + /// A stream of `u32le length ‖ Decision` entries, up to `MAX_HEIRS`. + /// A single heir uses the same layout with one entry. pub const BEQUEST: u16 = 13; - /// Whose key verifies the signature when it cannot be obtained from the file. - /// - /// # Why the name changed but the number did not - /// - /// The tag began as `AUTHOR_KEY`, "whose presence is recorded" in proof of life - /// for all files. Approver voting revealed the field's broader - /// meaning: a roster member signs, not the author, and that key also cannot be - /// found from the file: the server remembers the AUTHOR's key. The name `author_key` - /// would then be a lie presented as truth, containing someone else's key. + /// Verification key when it cannot be obtained from the file. /// - /// The number deliberately remained unchanged. I-7 prohibits reusing numbers - /// with a DIFFERENT meaning; this meaning is and always was "the key - /// used to verify this document". Changing the number would retire it merely - /// to refine the wording. - /// - /// Required for `Alive` with zero `file_id` and for `ApproveDevice`; forbidden - /// everywhere else. + /// Required for `Alive` with zero `file_id` and for `ApproveDevice`; + /// forbidden for other orders. pub const SIGNER_KEY: u16 = 14; /// `SetCoauthors` only: lifetime of a proposal lacking enough signatures. /// @@ -777,24 +736,11 @@ pub fn peek(bytes: &[u8]) -> Result { decode(body) } -/// INTENT digest: what was ordered, without "when issued" or "who signed". -/// -/// # Why it exists -/// -/// Coauthor quorum collects signatures from DIFFERENT people on DIFFERENT machines, -/// each signing their own document: they type the same command, their client inserts its own -/// issuance time and key. If the server identified proposals by body hash, two -/// coauthors typing the same command in different seconds would create -/// two different proposals, and quorum would NEVER be reached. +/// Hash the order's intent, excluding issuance time and signer key. /// -/// Hence the rule: a proposal is identified by WHAT is ordered, not when it -/// was said. File, kind and every parameter must match; issuance time and -/// signer key are excluded from the digest, because they belong to the signature, -/// not the intent. -/// -/// It is computed using exactly the body encoder: a second serializer for -/// hashing would create a second definition of "the same order" and -/// would diverge on the first newly added line. +/// Coauthors may sign the same command at different times with different +/// keys. File, kind and parameters identify the shared quorum proposal. +/// The digest uses the body encoder to keep these fields in one encoding. /// /// # Errors /// [`FormatError`] if the order cannot be encoded. @@ -803,18 +749,9 @@ pub fn intent_digest(order: &Order) -> Result<[u8; 32], FormatError> { Ok(oc_crypto::sha256(&encode(&canonical)?)) } -/// Order kind from the BARE BODY, without a preceding signature. -/// -/// Parsing is strict, as everywhere: unknown kinds are not guessed. -/// -/// # Who calls this +/// Read the order kind from a body without its signature prefix. /// -/// Nobody today; saying so is more honest than omitting it. The function was -/// written for the coauthor proposal list, where kind came from the STORED -/// body; since F-20 item 15 (2026-09-04), the server stores no bodies at all: -/// a proposal is identified by intent (`intent_digest`), and kind is a record -/// field. The previous doc comment outlived that architectural change and -/// continued promising stored bodies. +/// This does not authenticate the body; unknown kinds are rejected. /// /// # Errors /// [`FormatError`] if the body cannot be parsed. diff --git a/crates/oc-protocol/src/replica.rs b/crates/oc-protocol/src/replica.rs index 1a12c88..d9cf710 100644 --- a/crates/oc-protocol/src/replica.rs +++ b/crates/oc-protocol/src/replica.rs @@ -1,25 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 -//! Server state replication: push and acknowledgment (E2, B4; -//! `docs/protocol.md` §9.17). +//! Server snapshot replication and acknowledgment (protocol §9.17, E2/B4). //! -//! # What it is and what it does not provide +//! `witnessed` waits for a signature acknowledging the exact snapshot before +//! success; `mirrored` sends the copy afterward and can lose the recent tail. +//! Durability against disk, machine or administrator loss depends on deployment. //! -//! The replica is a separate process that accepts server state snapshots and -//! acknowledges SPECIFIC bytes with its signature. The `witnessed` profile means -//! that the server does not return success until acknowledgment arrives; `mirrored` means -//! the copy is sent after success, with a nonzero RPO for the tail. -//! -//! What it does not provide: another machine. A replica in an adjacent process or container -//! survives server failure, but not loss of the disk or machine, nor an administrator. -//! The profile promises exactly that "acknowledged operations survive loss -//! of the SERVER"; both server and replica state this boundary. -//! -//! # Continuity -//! -//! A push carries the commit number and the PREVIOUS snapshot's fingerprint. The replica -//! accepts only a continuation of its own history: a gap yields `SnapshotRequired`, -//! the same number with a different fingerprint yields `HistoryConflict`. The former calls for -//! resending a snapshot; the latter is evidence of two histories for one server. +//! A push includes its commit number and previous snapshot fingerprint. +//! Gaps yield `SnapshotRequired`; a conflicting fingerprint at the same +//! number yields `HistoryConflict`. use oc_format::FormatError; use oc_format::tlv::{TlvReader, TlvWriter}; @@ -443,12 +431,9 @@ mod tests { } } - /// THE BOOTSTRAP SNAPSHOT FLAG IS EXACTLY 0 OR 1. - /// - /// Before 2026-09-20 it was read as "nonzero byte": 255 different bytes identically - /// disabled the history continuity check, and re-encoding reduced them - /// all to one (I-7). The forgery here IS SIGNED with the server key; otherwise - /// the test would pass on signature rejection and say nothing about parsing. + /// Accept snapshot flags only as 0 or 1. + /// Sign malformed bodies with the test server key to reach flag parsing + /// rather than fail authentication first. #[test] fn the_snapshot_flag_is_exactly_zero_or_one() { let server = Ed25519Signer::from_seed(&[0x24; 32]); diff --git a/crates/oc-protocol/src/revocation.rs b/crates/oc-protocol/src/revocation.rs index 5af25fc..9f0d03b 100644 --- a/crates/oc-protocol/src/revocation.rs +++ b/crates/oc-protocol/src/revocation.rs @@ -1,26 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 -//! Revocation: a server-signed "file revoked" document. +//! Server-signed revocation documents, distributable through any channel. //! -//! # Why a document when revocation already works -//! -//! Revocation works by the server ceasing to issue leases, so it reaches -//! only those who contact the server. A revocation document makes revocation DATA: -//! a document signed with the lease-signing key that a reader accepts -//! from anywhere: a subscription, a query, a file beside the container, or a -//! peer. The channel is no longer the only route (F-18, tier 3). -//! -//! It cannot be forged without the server key; spreading a genuine document means -//! spreading the truth. It is verified with the same key as a lease: the author -//! pinned it in the container header under their signature, so revocation introduces -//! no second trust root. -//! -//! # Layout -//! -//! Like a lease: `signature(64) ‖ body`, the body is TLV under I-7 and I-8; -//! the signature transcript uses label `"CC/v1/revocation"` from the shared registry in §3.6 -//! (the label was reserved earlier and had not been used before this date). -//! Revocation is final: the epoch in the document serves journals and reports, not -//! comparisons of "which revocation is newer". +//! Verify with the lease-signing key pinned in the signed container header. +//! The layout is `signature(64) ‖ TLV body`; the transcript label is +//! `CC/v1/revocation`. Revocation is final: its epoch identifies journal/report +//! entries, rather than choosing between newer and older revocations. use oc_crypto::CryptoError; diff --git a/crates/oc-protocol/src/standing.rs b/crates/oc-protocol/src/standing.rs index a5d0e2a..2eb3aec 100644 --- a/crates/oc-protocol/src/standing.rs +++ b/crates/oc-protocol/src/standing.rs @@ -1,25 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 -//! File standing on the server: what the author sees when asking "what is set there?". +//! Current server-side file standing for author queries. //! -//! # Why a separate document -//! -//! The author gives the server orders: revoke, appoint an heir, set -//! a limit. Before this document, there was no way to SEE which -//! orders currently applied. The only mirror was the journal, a history -//! of actions rather than their result: "heir appointed" and "heir appointed, then -//! removed" are different histories with the same empty outcome, and the author should not -//! have to distinguish them by reading the journal tail. -//! -//! This is especially true for the dead man's switch, whose result is irreversible: someone who appointed -//! an heir has the right to verify that the interval is what they intended and that -//! the event is still far away. -//! -//! # What is absent here -//! -//! Secrets. Everything here was already disclosed to the server by the author, and is visible to recipients -//! in the queue and journal. The response therefore requires no proof of possession, -//! like the request queue (`Requests`). The bequest is absent: it belongs to -//! the heir, not anyone who asks, and is delivered by `Collect` after the event. +//! This summarizes the result of orders rather than their journal history. +//! It contains public queue/journal information and needs no possession proof. +//! The heir's bequest is excluded; `Collect` delivers it after the event. use oc_format::FormatError; use oc_format::tlv::{TlvReader, TlvWriter}; @@ -60,12 +44,9 @@ pub mod tag { pub const FROZEN: u16 = 17; } -/// What the file specifies for the author's silence. +/// Action configured for the author's silence. /// -/// A separate enum rather than adjacent optional fields: "open, -/// but to whom is unknown" and "close, but with an heir" are states that cannot -/// occur, so there is no reason to make them representable. The same technique as on the server -/// (`cc_authority::AfterSilence`) and in the order (`order::HeirMode`). +/// The enum keeps heir recipients and closure mode mutually exclusive. #[derive(Debug, Clone, PartialEq, Eq)] pub enum HeirStanding { /// No heir appointed; silence does not close the file. @@ -90,12 +71,8 @@ pub struct Vote { /// A proposal awaiting signatures. /// -/// The first signer's body is carried as a TEMPLATE. The first revision omitted it: -/// "sign your own command, not someone else's bytes". That reasoning is valid: signing -/// someone else's body is wrong, and it is also stale. But a coauthor signing through the UI -/// must REPRODUCE the parameters, which the intent digest hides by construction. -/// The solution is a template: the client extracts parameters, inserts its own time and -/// signs with its own key. The same command, its own bytes. +/// The first signer's body is a parameter template. Coauthors reconstruct the +/// command with their own issuance time and sign their own body. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProposalStanding { /// INTENT digest: identifies the proposal. @@ -256,32 +233,15 @@ fn flatten(keys: &[[u8; 32]]) -> Vec { /// Vote record length: fingerprint, voter, decision, time. const VOTE_LEN: usize = 73; -/// Parse the standing strictly: exact lengths, mode consistent with fields, and -/// NO value has two representations. -/// -/// # What has been rejected since 2026-09-20, and why +/// Parse standing with exact lengths and consistent mode/fields. /// -/// The writer [`encode`] omits everything meaning "nothing": an empty roster, -/// a zero threshold, zero proposal lifetime and unfrozen standing are never -/// written. Parsing formerly accepted both omission and explicit encoding of the same meaning, -/// so each quantity had TWO wire representations, while -/// canonicality (I-7) requires exactly one. -/// -/// **Unfrozen standing remains valid**, and is the most common case, -/// but is expressed by ABSENCE of tag 17, not by `frozen = 0`. The same applies to -/// roster, threshold and lifetime: absence remains valid and reads as before; -/// only the second form, which no writer produces, is rejected. -/// -/// Roster and threshold are also checked against each other under the same rule as -/// `oc_format::header::check_coauthors` and [`crate::order`]: the threshold must be -/// achievable by the roster. The server never returns otherwise (`check_roster` rejects -/// such an order, and `Coauthors` such a header), so the check excludes no -/// reachable standing while preventing a manually crafted "quorum -/// that nobody can ever reach". +/// Canonical encoding omits an empty roster, zero threshold, zero proposal +/// lifetime and an unfrozen flag. Explicit empty/zero forms are rejected; +/// absence remains valid. The threshold must be achievable by the roster. /// /// # Errors -/// [`FormatError`] for an unknown tag, invalid length, a value the -/// writer never produces, or fields inconsistent with the mode. +/// [`FormatError`] for an unknown critical tag, invalid length, noncanonical +/// value or fields inconsistent with the mode. pub fn decode(bytes: &[u8]) -> Result { let mut file_id = None; let mut revoked = None; @@ -722,16 +682,8 @@ mod tests { assert!(decode(&w.finish()).is_err(), "событие без наследника принято"); } - /// VALUES NEVER PRODUCED BY THE WRITER ARE REJECTED (2026-09-20). - /// - /// Each quantity had TWO wire representations: omission - /// (no tag) and an explicit encoding of the same meaning. Nobody ever - /// produces the latter, yet parsing accepted it: two different byte sequences - /// meant the same thing, which I-7 forbids. - /// - /// The test enumerates them by name rather than checking that "something was rejected": - /// removing ANY one check must fail the test, and the message must - /// identify exactly which form passed. + /// Reject explicit empty/zero forms where canonical encoding omits the field. + /// Check each form separately so removing one validation fails this test. #[test] fn values_the_writer_never_writes_are_refused() { // Каждый случай — приписка к минимальному законному положению. diff --git a/crates/oc-protocol/src/unknown.rs b/crates/oc-protocol/src/unknown.rs index fac6a44..a0d8381 100644 --- a/crates/oc-protocol/src/unknown.rs +++ b/crates/oc-protocol/src/unknown.rs @@ -10,14 +10,10 @@ use core::ops::Range; use oc_format::FormatError; use oc_format::tlv::{FIELD_PREFIX_LEN, Field, UnknownTag, unknown_tag_action}; -/// Decision for an unknown tag: reject critical, skip optional. -/// -/// The error variant is the one the crate returned for EVERY unknown tag before -/// the decision of 2026-09-21: behavior for the critical range is entirely unchanged, -/// including the rejection text. +/// Reject an unknown critical tag; skip an optional one. /// /// # Errors -/// [`FormatError::UnknownCriticalField`]: a tag from the critical range. +/// [`FormatError::UnknownCriticalField`] for a critical tag. pub(crate) fn refuse_if_critical(tag: u16) -> Result<(), FormatError> { match unknown_tag_action(tag) { UnknownTag::Refuse => Err(FormatError::UnknownCriticalField { tag }), diff --git a/crates/oc-protocol/src/witness.rs b/crates/oc-protocol/src/witness.rs index a77af6a..96eba0e 100644 --- a/crates/oc-protocol/src/witness.rs +++ b/crates/oc-protocol/src/witness.rs @@ -1,26 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 -//! Witnessing a server journal head (`docs/protocol.md` §9.13, D3). +//! Witnessed server journal heads (protocol §9.13, D3). //! -//! # What this contains and why +//! A witness retains a head, verifies a new head's RFC 9162 consistency proof, +//! and cosigns the extension. This codec handles heads, proofs and signatures; +//! networking, persistence and clocks belong to the witness service. //! -//! A signed head (`size ‖ root`) proves only that the server -//! agrees with itself: rewriting the journal also rewrites its root. Value -//! comes from a copy of the head OUTSIDE the server, and someone able to compare a -//! new head with it without possessing the journal. That is the witness: it stores one head, -//! receives a new one from the server with a consistency proof -//! (RFC 9162), and signs it after the server if it extends -//! the old one. -//! -//! Only bytes and checks live here: a head, a view (head with proof), -//! a cosigned head, and the relationship between two heads. Networking, disk and clocks belong -//! to the witness (`cc_authority::witness`). -//! -//! # What this does NOT provide -//! -//! Independent public time or operator independence: a witness on the same -//! machine or under the same operator signs what it is shown and -//! rolls back with it. The witness detects rollback and forks -//! RELATIVE TO ITS OWN MEMORY, nothing more. +//! Rollback/fork detection is relative to the witness's retained state. +//! A witness does not establish public time or operator independence; a +//! colocated witness can lose or roll back its state with the server. use oc_format::FormatError; use oc_crypto::merkle::MerkleTree; diff --git a/crates/oc-protocol/tests/action_vectors.rs b/crates/oc-protocol/tests/action_vectors.rs index 4bd60eb..a17417c 100644 --- a/crates/oc-protocol/tests/action_vectors.rs +++ b/crates/oc-protocol/tests/action_vectors.rs @@ -14,26 +14,10 @@ clippy::disallowed_types )] -//! Frozen ACTION door vectors: bodies, transcripts and signatures. +//! Frozen action-grant and action-lease bodies, transcripts and signatures. //! -//! A separate file rather than a module: these tests need real signing keys, -//! meaning the complete `oc-crypto`, and read the vector file here too. -//! -//! # What is frozen, and why the SIGNATURE rather than only the body -//! -//! The same rationale as `agent_grant.kat`: the body means nothing by itself. -//! An action grant is an AUTHOR assertion, a lease a SERVER assertion; without -//! signatures both are mere sheets of paper. Freezing only bytes would leave the -//! transcript, WHAT is signed, unfrozen. Divergence there is silent: the signature -//! simply fails to verify, making it look as though "the server broke". -//! -//! # Why a separate LEASE vector in addition to the grant -//! -//! The server uses the same signing key for file leases and action -//! leases; ONLY the label separates the domains. The lease vector checks -//! that the label is present: a signature made in the grant domain will not match the frozen one. -//! -//! These vectors do not affect the container version: none of their bytes reside in `.cc`. +//! The lease vector also guards domain separation from file leases using the same +//! server key. These protocol documents are outside the `.cc` container version. use oc_crypto::sign::{Ed25519Signer, Signer as _}; use oc_protocol::action::{ diff --git a/crates/oc-protocol/tests/agent_vectors.rs b/crates/oc-protocol/tests/agent_vectors.rs index a78317d..f19e81f 100644 --- a/crates/oc-protocol/tests/agent_vectors.rs +++ b/crates/oc-protocol/tests/agent_vectors.rs @@ -14,20 +14,10 @@ clippy::disallowed_types )] -//! Frozen Agent Protocol vectors: bodies, transcripts and signatures. +//! Frozen agent-grant and delegation bodies, transcripts and signatures. //! -//! A separate file rather than a module: these tests need real signing keys, -//! meaning the complete `oc-crypto`, and read the vector file here too. -//! -//! # What is frozen, and why the SIGNATURE rather than only the body -//! -//! The body means nothing by itself: a grant is an AUTHOR assertion, -//! while delegation is a DOOR assertion. Freezing only body bytes would leave -//! the transcript, exactly WHAT is signed, unfrozen; divergence there -//! is silent: the signature simply fails to verify, making it look as though "the server -//! broke". The same rationale as `lease.kat` and `control.kat`. -//! -//! These vectors do not affect the container version: none of their bytes reside in `.cc`. +//! Including signatures checks transcript agreement as well as encoding. +//! These documents are outside the `.cc` container version. use oc_crypto::sign::{Ed25519Signer, Signer as _}; use oc_protocol::access::Blob; diff --git a/crates/oc-protocol/tests/control_vectors.rs b/crates/oc-protocol/tests/control_vectors.rs index 49af321..fae3d92 100644 --- a/crates/oc-protocol/tests/control_vectors.rs +++ b/crates/oc-protocol/tests/control_vectors.rs @@ -1,18 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 -//! Frozen control and replica document vectors (E2, B2/B4/B7). +//! Frozen control and replica document bodies, transcripts and signatures. //! -//! These documents arrive ENTIRELY FROM THE WIRE and must not change silently: -//! a wire-byte change requires a decision recorded in the specification, the same -//! rule as for the container. The vector is what makes a silent -//! change impossible. -//! -//! The signature is frozen together with the body, for the same reason as leases: -//! a body without a signature is a sheet of paper, while transcript divergence is silent -//! (the signature simply fails to verify, making it look as though "the server broke"). -//! -//! The generator is `print_vectors`, marked `#[ignore]`: it prints what later -//! resides in `tests/kat/control.kat`. Rerun ONLY together with -//! a recorded decision to change the bytes. +//! The ignored `print_vectors` tool regenerates `tests/kat/control.kat` only for a +//! recorded decision to change the wire bytes. #![allow( clippy::unwrap_used, diff --git a/crates/oc-protocol/tests/session_mac_table.rs b/crates/oc-protocol/tests/session_mac_table.rs index 623b16e..bba6c71 100644 --- a/crates/oc-protocol/tests/session_mac_table.rs +++ b/crates/oc-protocol/tests/session_mac_table.rs @@ -1,20 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 -//! Session MAC: authenticated-kind table and trailer separation, in the owning crate. +//! Check the shared session-MAC kind table and trailer separation. //! -//! Why a separate file when six tests in `cc-authority/tests/session_mac.rs` -//! already make the server reject tampered frames? Because BOTH quantities live -//! HERE, yet only the neighboring crate guarded them. Removing `KIND_ACTIVATE` -//! from [`is_session_sealed`] broke none of the 104 `oc-protocol` tests, -//! yet it removes activation's MAC entirely: the client uses the same table to decide -//! whether to append the trailer, and both peers stop requiring it together. The function's doc comment -//! explicitly calls it "one table for both ends of the wire"; such a -//! quantity must have a test where it is defined, not only where -//! it happened to be used. -//! -//! The test follows THE PATH: kind comes not from a constant but from the first byte of a frame -//! built by [`encode_request`], exactly as read by `cc_cli::activate` -//! (`seal_request`) and `cc_authority::serve` (`converse`). Comparing with the number 3 -//! would survive renaming a variant; frame construction would not. +//! Both peers use this table, so omitting a kind could make them agree to stop +//! protecting it. Derive kinds from encoded request frames to exercise the path +//! used by callers. // Литы сняты по названным причинам: `unwrap`/`panic` — словарь проверки, // индексирование — чтение первого байта кадра заведомо непустой длины. diff --git a/crates/oc-protocol/tests/unknown_tags.rs b/crates/oc-protocol/tests/unknown_tags.rs index bbf0979..32738ca 100644 --- a/crates/oc-protocol/tests/unknown_tags.rs +++ b/crates/oc-protocol/tests/unknown_tags.rs @@ -1,38 +1,12 @@ // SPDX-License-Identifier: MPL-2.0 -//! Tag-range rule for PROTOCOL DOCUMENTS (I-7, decision of 2026-09-21). +//! Protocol unknown-tag tests across the codec table. //! -//! # What is tested here +//! Reject unknown critical tags, skip optional tags, and enforce ordering even +//! for skipped entries. Known tags in these layouts precede the optional range, +//! so optional test fields are appended. //! -//! One rule across all fourteen parsers: an unknown tag > `0x7FFF` -//! is skipped; an unknown tag ≤ `0x7FFF` is rejected with the same error variant -//! used for every unknown tag before the decision; strict tag ordering -//! (I-7) also applies to skipped tags. -//! -//! # Why a table rather than twelve files -//! -//! Because there is ONE rule. Copying its test into one file per document would -//! create twelve independently editable places, exactly how this repository develops -//! the "one path fixed, its neighbor forgotten" problem. -//! Here a new document adds one table row and immediately receives all four -//! cases. -//! -//! # Why the unknown tag appears ONLY AT THE END -//! -//! Not laziness: no other position is possible. Tags strictly increase (I-7), and all -//! KNOWN protocol document tags lie in the critical range (1..=20 for the -//! richest document). An optional-range tag is therefore larger by construction -//! than every known tag and can only follow them. These documents have no middle -//! position where it could be inserted, unlike the -//! container header, where optional tag `0x8001` is already occupied. -//! -//! # Why signing happens AFTER the tag is added -//! -//! Case (i) must test PARSING. Signing a body without the tag and supplying one with -//! the tag would fail signature verification, letting the test pass without saying anything about -//! parsing. Thus signed documents have their body extended first, then -//! signed. The reverse order is separate case (iv), asserting -//! exactly what justifies the optional range: an outsider cannot append -//! a tag because the signature covers the raw body bytes. +//! Sign extended bodies to exercise parsing. Separately append fields after +//! signing to confirm raw-byte authentication detects changes. #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)] @@ -660,29 +634,11 @@ fn a_tag_appended_after_the_signature_breaks_the_signature() { assert!(checked >= 9, "подписанных документов в таблице стало меньше: {checked}"); } -/// SPECIAL CASE (a): UNAUTHENTICATED GREETING AND CONVERSATION TRANSCRIPT (K31). -/// -/// # The question this test answers -/// -/// The greeting and challenge travel over the wire BEFORE any authentication: neither -/// is signed, and the session MAC is not derived yet. An intermediary can therefore append -/// an optional tag to the greeting, and the range rule made such a -/// greeting PARSEABLE for the first time; formerly the server would reject it. The question: -/// is this harmless because the field is ignored? -/// -/// # Answer: harmless, but NOT because it is ignored -/// -/// It relies on the handshake transcript (K31, -/// `oc_crypto::kdf::handshake_transcript`, `docs/format.md`, "ECHO BOUND TO THE -/// CONVERSATION 2026-09-21"). It is computed over RAW FRAME BYTES: the device -/// uses bytes it SENT (`cc_cli::activate::greet`), the server bytes -/// it RECEIVED (`cc_authority::serve::step`, `Request::Hello` branch). -/// Editing a frame in transit makes these quantities differ, so the device's computed -/// proof-of-possession echo fails at the server. +/// Modify an optional greeting field before authentication. /// -/// Thus the parties hash DIFFERENT bytes precisely when an intermediary intervened, -/// and identical bytes when none did. That is what this test verifies rather than -/// merely retelling it. +/// The field can parse, but K31 hashes raw frames: the device hashes what it sent +/// and the server hashes what it received. Alteration must change the transcript +/// and make the possession-proof echo fail. #[test] fn a_tag_smuggled_into_the_unauthenticated_hello_parts_the_handshake_transcript() { let hello = activation::Request::Hello(activation::Hello { diff --git a/docs/review-2026-09-26.md b/docs/review-2026-09-26.md index a14d533..99dc29c 100644 --- a/docs/review-2026-09-26.md +++ b/docs/review-2026-09-26.md @@ -11,6 +11,13 @@ contracts. Corrected outdated hybrid support and claim-derivation documentation. Public policy digest comparison is no longer described as providing a general constant-time guarantee for secret inputs. +After publishing 0.0.5, a second comment-only pass shortened another 123 source +blocks, 40 test blocks and 11 short explanations. Wire tables, verification +contracts and safety limitations remain. It corrected stale `assemble()` return +documentation and KEM-dependent KDF field sizes. These later comment changes +are in this PR, not in the immutable registry 0.0.5 archives; executable source +lines and frozen fixtures are unchanged from release commit `1dd0d08`. + ## Reproduced defects and fixes - `mlkem_p256::Keypair` derived `Debug` printed the 64-byte ML-KEM seed through