diff --git a/dag/gunbc/host_crypto_digest_seed_growth.dag b/dag/gunbc/host_crypto_digest_seed_growth.dag new file mode 100644 index 00000000000..3eca32cb3da --- /dev/null +++ b/dag/gunbc/host_crypto_digest_seed_growth.dag @@ -0,0 +1,33 @@ +module gunbc.host_crypto_digest_seed_growth + +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.seed_growth { SeedGrowthJustification } +import std.decl_ref { DeclarationRef, WholeDeclaration } + +// FORWARD-FREEZE RECEIPT for the host crypto digest seams in v1_interpreter, enumerated because +// gunbc.seed_growth_admission makes unenumerated hand growth in src/v1 a stop-line. +// +// TWO SEAMS, ONE RECEIPT. hmac_sha256_hex_tag (the HMAC-SHA-256 mint behind extdeps.crypto.mac +// mac_sign) landed with no receipt; it is enumerated here beside sha256_hex_of_text_digest, which this +// change adds, because they are one boundary: RustCrypto sha2 reached from the interpreter. +// +// WHY A HOST SEAM AND NOT THE PURE FOLD. extdeps.crypto.sha2 sha256_hex is the substrate's SHA-256 and +// stays its authority and oracle, but interpreted it costs ~200k eval steps per 64-octet block +// (measured with claim_batch on test.claim.sha256_host_known_answer_witness), and the fabric store +// door (gunbc.fabric_store_operation_admission) digests an approved intent on every protected write. +// +// NOT COUNTED BELOW, disclosed: the interpreter arms free_call.hmac_sha256_hex, free_call.hmac_sha256_ +// verify_hex and free_call.sha256_hex_of_text (macro arms inside eval_builtin_inner, not citable +// declarations). The generated v1_interpreter_dispatch_generated.rs and v1_compiler_infer_method.rs +// project gunbc.v1_interpreter_primitive_surface and v1.compiler.infer_method, whose authorities +// change with them. +data host_crypto_digest_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { + hand_authored_declarations: [ + DeclarationRef { module_path: "v1_compiler.v1_interpreter", decl_name: "sha256_hex_of_text_digest", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.v1_interpreter", decl_name: "hmac_sha256_hex_tag", field: WholeDeclaration } + ], + reason: "A cryptographic digest over a text's bytes is realized by the host's audited RustCrypto sha2 exactly as HMAC already is: the substrate's pure fold (extdeps.crypto.sha2) remains the authority and the differential oracle, and the host seam changes cost only -- ~200k interpreted eval steps per block becomes one native call on the store door's per-write path. Correctness is established by known answers shared with the grandfathered sha256_fips_witness (pure fold == NIST/RFC values) and by host-vs-known-answer claims over the padding boundaries.", + owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, + trigger: "Delete when the digest is realized by EMITTED code rather than the interpreter's host arm: a rt_function_registry bridge row for sha256_hex_of_text (and hmac_sha256_hex) with its v1_rt runtime body, admitted by the #12389 primitive-runtime-body gate, so the self-emitted store door and mac_sign call the emitted body. Sufficient means both digests run in an emitted crate with the same known-answer claims green; an interpreter-only arm does not satisfy it.", + current_boundary: "sha256_hex_of_text_digest returns hex(Sha256::digest(text.as_bytes())); hmac_sha256_hex_tag decodes a hex key and returns hex(HMAC-SHA-256(key, message)) or none for non-hex key material. Neither holds state, reads anything but its arguments, or makes a decision; every refusal and every use is in the .dag callers." +} diff --git a/dag/gunbc/primitive_egress/dispositions_text.dag b/dag/gunbc/primitive_egress/dispositions_text.dag index 131e97de5c7..a65ae0a5fc4 100644 --- a/dag/gunbc/primitive_egress/dispositions_text.dag +++ b/dag/gunbc/primitive_egress/dispositions_text.dag @@ -57,6 +57,7 @@ fn dispositions_text_rows() -> List { evidence_row(name: "is_xid_continue", category: Unicode, external_subject: NoExternalSubject, computation: computes(authority: authority_required(home: "extdeps.unicode (pinned UCD version; generated property tables are artifacts, never hand rows)"), inputs: "cp: Int", result: "Bool: XID_Continue per the pinned UCD; total", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "is_xid_continue: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), evidence_row(name: "is_emoji_ident", category: Unicode, external_subject: NoExternalSubject, computation: computes(authority: authority_required(home: "extdeps.unicode (pinned UCD version; generated property tables are artifacts, never hand rows)"), inputs: "cp: Int", result: "Bool: the identifier-admissible emoji property per the pinned UCD; total", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "is_emoji_ident: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), evidence_row(name: "hmac_sha256_hex", category: Crypto, external_subject: NoExternalSubject, computation: computes(authority: authority_required(home: "extdeps.crypto.mac (HMAC-SHA-256 over the ENCODING-0 byte substrate and the NUMERIC-BIT-0 words)"), inputs: "key_hex: String, message: String", result: "String?: the lowercase hex tag; Absent when key_hex is not hex -- a deterministic function of its inputs (RFC 2104 / FIPS 180-4)", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "hmac_sha256_hex: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), + evidence_row(name: "sha256_hex_of_text", category: Crypto, external_subject: NoExternalSubject, computation: computes(authority: authority_required(home: "std.primitives sha256_hex_of_text_contract (SHA-256 over the text's UTF-8 bytes; the pure extdeps.crypto.sha2 sha256_hex shares its known answers)"), inputs: "text: String", result: "String: the lowercase hex SHA-256 digest of the UTF-8 bytes; total (FIPS 180-4)", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "sha256_hex_of_text: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), evidence_row(name: "hmac_sha256_verify_hex", category: Crypto, external_subject: NoExternalSubject, computation: computes(authority: authority_required(home: "extdeps.crypto.mac (HMAC-SHA-256 over the ENCODING-0 byte substrate and the NUMERIC-BIT-0 words)"), inputs: "key_hex: String, message: String, tag_hex: String", result: "Bool: constant-time tag equality; the comparison is part of the modeled contract (hmac_sha256_verify_hex_contract)", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "hmac_sha256_verify_hex: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), evidence_row(name: "contiguous_loop_elementwise_kernel", category: NumericBit, external_subject: NoExternalSubject, computation: computes(authority: declared_authority(module_path: "std.primitives", decl_name: "contiguous_loop_elementwise_kernel_contract"), inputs: "xs: List, f: fn(Int) -> Int", result: "List: elementwise map over a contiguous carrier; total", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "contiguous_loop_elementwise_kernel: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), evidence_row(name: "contiguous_loop_elementwise_float_kernel", category: NumericBit, external_subject: NoExternalSubject, computation: computes(authority: declared_authority(module_path: "std.primitives", decl_name: "contiguous_loop_elementwise_float_kernel_contract"), inputs: "xs: List, f: fn(Float) -> Float", result: "List: elementwise map; total", body: DagBodyRequired), refusals: RefusalsTyped, realization: NoNativeRealizationClaim, compiler_query: NotACompilerQuery, conflict: NoMeaningConflict, located: "contiguous_loop_elementwise_float_kernel: the realization surfaces the census joins (registry row / contract row / algebra template / rt bridge / interpreter arm as present) and the authority named in the computation standing", next: "the owning lane lands the .dag body under the declared authority and switches one production consumer; the interpreter arm and bridge then delete for that consumer"), diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index bfd9db412ed..0f3628e703b 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -25,6 +25,7 @@ import gunbc.decl_facts_authored_string_attach_seed_growth { } import gunbc.keyed_declaration_read_seed_growth { keyed_declaration_read_seed_growth_justification } import gunbc.keyed_dependency_edge_read_seed_growth { keyed_dependency_edge_read_seed_growth_justification } +import gunbc.host_crypto_digest_seed_growth { host_crypto_digest_seed_growth_justification } import gunbc.census_memo_seed_growth { census_memo_seed_growth_justification } import gunbc.fabric_door_socket_seed_growth { fabric_door_socket_seed_growth_justification } import gunbc.kind_reflection_seed_growth { kind_reflection_seed_growth_justification } @@ -310,6 +311,7 @@ fn seed_growth_justification_roster() -> List { with_authored_string_literals_seed_growth_justification, keyed_declaration_read_seed_growth_justification, keyed_dependency_edge_read_seed_growth_justification, + host_crypto_digest_seed_growth_justification, cli_wire_host_seed_growth_justification, filesystem_create_new_seed_growth_justification, required_lane_judgment_seed_growth_justification, diff --git a/dag/gunbc/v1/v1_interpreter_primitive_surface.dag b/dag/gunbc/v1/v1_interpreter_primitive_surface.dag index 4a24531d53f..0f9ed177054 100644 --- a/dag/gunbc/v1/v1_interpreter_primitive_surface.dag +++ b/dag/gunbc/v1/v1_interpreter_primitive_surface.dag @@ -506,6 +506,15 @@ fn v1_interpreter_authored_roster_arms() -> List List { [ "hmac_sha256_verify_hex", "hmac_sha256_hex", + "sha256_hex_of_text", "count", "string_length", "code_point", @@ -723,6 +740,7 @@ fn primitive_contract_roster() -> List { string_length_contract, hmac_sha256_verify_hex_contract, hmac_sha256_hex_contract, + sha256_hex_of_text_contract, substring_contract, string_contains_contract, starts_with_contract, diff --git a/dag/test/claim/sha256_host_known_answer_witness_test.dag b/dag/test/claim/sha256_host_known_answer_witness_test.dag new file mode 100644 index 00000000000..006c8f5de37 --- /dev/null +++ b/dag/test/claim/sha256_host_known_answer_witness_test.dag @@ -0,0 +1,56 @@ +module test.claim.sha256_host_known_answer_witness + +import std.logic { Bool } +import std.types { String } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE HOST SHA-256 AGAINST KNOWN ANSWERS, one host call per claim. sha256_hex_of_text (the host +// seam, RustCrypto sha2) must equal the known answer for each text. For the empty, "abc" and NIST +// 448-bit texts the known answer is the FIPS 180-4 published value that test.claim.sha256_fips_witness_ +// test asserts of the pure fold (extdeps.crypto.sha2 sha256_hex), so host == pure holds transitively +// through the shared value. For the padding boundaries (55 octets, the last that fits one block with +// its length field; 56, which forces a second; 63/64/65, straddling the block edge; 130, three blocks) +// the known answers are computed by an independent implementation (Python hashlib); the pure fold is +// not asserted on those here, because interpreting it costs ~200k eval steps per block. The direct +// pure-vs-host comparison on all nine vectors ran as a local receipt (see the PR). +fn matches_known(text: String, known: String) -> Bool { + sha256_hex_of_text(text: text) == known +} + +test fn the_host_digest_of_the_empty_text_is_the_known_answer() -> Bool { + matches_known(text: "", known: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") +} + +test fn the_host_digest_of_abc_is_the_known_answer() -> Bool { + matches_known(text: "abc", known: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") +} + +test fn the_host_digest_of_a_55_octet_text_is_the_known_answer() -> Bool { + matches_known(text: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", known: "9f4390f8d30c2dd92ec9f095b65e2b9ae9b0a925a5258e241c9f1e910f734318") +} + +test fn the_host_digest_of_a_56_octet_text_is_the_known_answer() -> Bool { + matches_known(text: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", known: "b35439a4ac6f0948b6d6f9e3c6af0f5f590ce20f1bde7090ef7970686ec6738a") +} + +test fn the_host_digest_of_a_63_octet_text_is_the_known_answer() -> Bool { + matches_known(text: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", known: "7d3e74a05d7db15bce4ad9ec0658ea98e3f06eeecf16b4c6fff2da457ddc2f34") +} + +test fn the_host_digest_of_a_64_octet_text_is_the_known_answer() -> Bool { + matches_known(text: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", known: "ffe054fe7ae0cb6dc65c3af9b61d5209f439851db43d0ba5997337df154668eb") +} + +test fn the_host_digest_of_a_65_octet_text_is_the_known_answer() -> Bool { + matches_known(text: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", known: "635361c48bb9eab14198e76ea8ab7f1a41685d6ad62aa9146d301d4f17eb0ae0") +} + +test fn the_host_digest_of_a_130_octet_three_block_text_is_the_known_answer() -> Bool { + matches_known(text: "gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-gunbc-abcd", known: "3de7e15aa1b453dc3833d796643b52713e80cdff8bf8738fece1c2d4a352a052") +} + +test fn the_host_digest_of_the_nist_448_bit_text_is_the_known_answer() -> Bool { + matches_known(text: "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", known: "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1") +} diff --git a/src/v1/04_method.dag b/src/v1/04_method.dag index dfe679744ce..f6d6edfeecd 100644 --- a/src/v1/04_method.dag +++ b/src/v1/04_method.dag @@ -294,6 +294,7 @@ data builtin_function_registry: Map = { "count": derived_signature_and_return(names: ["xs"], method: "count"), "hmac_sha256_verify_hex": BuiltinSignature { params: [BuiltinParam { name: "key_hex", ty: NamedTemplate { name: "String" } }, BuiltinParam { name: "message", ty: NamedTemplate { name: "String" } }, BuiltinParam { name: "tag_hex", ty: NamedTemplate { name: "String" } }], returns: bool_type }, "hmac_sha256_hex": BuiltinSignature { params: [BuiltinParam { name: "key_hex", ty: NamedTemplate { name: "String" } }, BuiltinParam { name: "message", ty: NamedTemplate { name: "String" } }], returns: with_optional_cardinality(n: string_type) }, + "sha256_hex_of_text": BuiltinSignature { params: [BuiltinParam { name: "text", ty: NamedTemplate { name: "String" } }], returns: string_type }, "string_length": BuiltinSignature { params: [BuiltinParam { name: "s", ty: NamedTemplate { name: "String" } }], returns: int_type }, "code_point": BuiltinSignature { params: [BuiltinParam { name: "c", ty: NamedTemplate { name: "String" } }], returns: int_type }, "to_int": derived_signature(names: ["s"], method: "to_int", returns: int_type), diff --git a/src/v1/stage0/src/v1_compiler_infer_method.rs b/src/v1/stage0/src/v1_compiler_infer_method.rs index 0057cbf29ae..3b50a27b6c3 100644 --- a/src/v1/stage0/src/v1_compiler_infer_method.rs +++ b/src/v1/stage0/src/v1_compiler_infer_method.rs @@ -338,6 +338,15 @@ pub fn builtin_function_registry() -> Rc>> }), })]), returns: crate::v1_std_core::with_optional_cardinality(string_type()), + })); + __m.insert("sha256_hex_of_text".to_string(), Rc::new(BuiltinSignature { + params: Rc::new(vec![Rc::new(BuiltinParam { + name: "text".to_string(), + ty: Rc::new(AlgebraTypeTemplate::NamedTemplate { + name: "String".to_string(), + }), + })]), + returns: string_type(), })); __m.insert("string_length".to_string(), Rc::new(BuiltinSignature { params: Rc::new(vec![Rc::new(BuiltinParam { diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index d276f89973e..ee87c4db609 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -21220,6 +21220,14 @@ macro_rules! v1_builtin_arms { Ok(Some(Value::Bool(mac.verify_slice(&tag).is_ok()))) }, + // SHA-256 OF A TEXT: total, no refusal -- every text has a digest (std.primitives + // sha256_hex_of_text_contract). + arm "free_call.sha256_hex_of_text" { "sha256_hex_of_text" } => { + Ok(Some(str_value(sha256_hex_of_text_digest( + expect_value_str($positional.first().copied(), "sha256_hex_of_text text")?.as_str(), + )))) + }, + // ISSUANCE, THE KEY HOLDER'S OWN OPERATION, and a second primitive rather than a // widening of verify: the verify arm above deliberately yields one bit, so a verifier // is never handed a computed tag to compare in variable time. Minting is the only @@ -23977,6 +23985,14 @@ fn expect_string(val: &Value, context: &str) -> InterpResult { } } +// SHA-256 of the text's UTF-8 bytes, lowercase hex: the host realization of the builtin +// sha256_hex_of_text (std.primitives sha256_hex_of_text_contract), the same RustCrypto sha2 the HMAC +// seam below uses. The pure fold extdeps.crypto.sha2 sha256_hex shares its known answers. +fn sha256_hex_of_text_digest(text: &str) -> String { + use sha2::{Digest, Sha256}; + hex::encode(Sha256::digest(text.as_bytes())) +} + /// The `hmac_sha256_hex` builtin's computation: the lowercase hex HMAC-SHA256 tag of `message` /// under the hex-encoded key, or `None` when the key is not hex -- no key, no tag. fn hmac_sha256_hex_tag(key_hex: &str, message: &str) -> Option { diff --git a/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs b/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs index b50126e6aa3..b7d271a38b7 100644 --- a/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs +++ b/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs @@ -23,6 +23,7 @@ pub enum EvalBuiltinArm { FreeCallCount, FreeCallReverse, FreeCallHmacSha256VerifyHex, + FreeCallSha256HexOfText, FreeCallHmacSha256Hex, FreeCallStringLength, FreeCallSubstring, @@ -173,6 +174,7 @@ pub fn lookup_eval_builtin_inner(spelling: &str) -> Option { "count" => Some(EvalBuiltinArm::FreeCallCount), "reverse" => Some(EvalBuiltinArm::FreeCallReverse), "hmac_sha256_verify_hex" => Some(EvalBuiltinArm::FreeCallHmacSha256VerifyHex), + "sha256_hex_of_text" => Some(EvalBuiltinArm::FreeCallSha256HexOfText), "hmac_sha256_hex" => Some(EvalBuiltinArm::FreeCallHmacSha256Hex), "string_length" => Some(EvalBuiltinArm::FreeCallStringLength), "substring" => Some(EvalBuiltinArm::FreeCallSubstring), @@ -327,6 +329,7 @@ macro_rules! eval_builtin_inner_arm { ("free_call.count") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallCount }; ("free_call.reverse") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallReverse }; ("free_call.hmac_sha256_verify_hex") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallHmacSha256VerifyHex }; + ("free_call.sha256_hex_of_text") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallSha256HexOfText }; ("free_call.hmac_sha256_hex") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallHmacSha256Hex }; ("free_call.string_length") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallStringLength }; ("free_call.substring") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallSubstring };