Supply Chain Integrity, Transparency, and Trust (IETF SCITT)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Document inclusion_path accumulator ordering in ocaml-scitt

+142 -2
+7 -2
lib/scitt.ml
··· 256 256 if n = 0 then t.hash.digest "" else compute_root ~hash:t.hash t.hashes 0 n 257 257 258 258 (** RFC 9162 §2.1.3.1: PATH(m, D_n). Returns raw sibling hashes in 259 - leaf-to-root order. Uses an accumulator to avoid O(n) list concatenation. 260 - *) 259 + leaf-to-root order. 260 + 261 + The recursion walks top-down (root toward leaf), prepending each sibling 262 + hash to [acc]. The deepest level (nearest the leaf) is the last to 263 + prepend, so it ends up at the head — giving leaf-to-root order without a 264 + final [List.rev]. This avoids O(n) list concatenation ([@ [...]]) at each 265 + level. *) 261 266 let inclusion_path ~hash hashes off len idx = 262 267 let rec go off len idx acc = 263 268 if len <= 1 then acc
+135
test/test_scitt.ml
··· 953 953 | Ok _ -> Alcotest.fail "tampered vector should not verify" 954 954 | Error _ -> ()) 955 955 956 + (* RFC 9942 (COSE Merkle Tree Proofs) official inclusion receipt vector. 957 + Source: github.com/cose-wg/draft-ietf-cose-merkle-tree-proofs/examples/ 958 + We cannot verify the ES256 signature (signing key not published), but we 959 + verify: (1) the Merkle proof path matches our implementation for 960 + PATH(3, D_5), and (2) the path hashes produce the correct root. *) 961 + 962 + let rfc9942_inclusion_receipt_hex = 963 + "d2845840a40126044a746573742d6b65792d3119018b010fa101782868747470733a2f2f7472616e73706172656e63792d736572766963652e6578616d706c652e636f6da119018ca12081586a8305038358203d06455dd33da4e9bbd8090677a2d0955e6dffe4b92069605a468920d1198095582033a5211719e06238a191c7244a7633187da2c9aaa5bc6dec54e2cbb49825543458204d75742d9ea02f7767dcd554a7878ff22cdb208be9f3d35f7aa7700b57e741c0f6584034e81008fb0e4e521657668745450df608d0e5c015dffc5d607dd78236c2908f4e2b643817d9191d0abd7c074aff2b1bfda519ab3fab210ff2e0a1de275de6a7" 964 + 965 + let test_rfc9942_inclusion_proof () = 966 + (* The cose-wg vector has tree_size=5, leaf_index=3, 3 path hashes. 967 + The actual leaf data and signing key are not published, so we cannot 968 + verify the ES256 signature or match our root. We verify: 969 + 1. The COSE receipt decodes correctly per RFC 9942 structure 970 + 2. The proof fields (vds, tree_size, leaf_index, path) are correct 971 + 3. The path is self-consistent: walking verify_inclusion with a dummy 972 + leaf and the implied root succeeds *) 973 + let expected_path = 974 + List.map hex_to_raw 975 + [ 976 + "3d06455dd33da4e9bbd8090677a2d0955e6dffe4b92069605a468920d1198095"; 977 + "33a5211719e06238a191c7244a7633187da2c9aaa5bc6dec54e2cbb498255434"; 978 + "4d75742d9ea02f7767dcd554a7878ff22cdb208be9f3d35f7aa7700b57e741c0"; 979 + ] 980 + in 981 + (* Compute the root implied by this path and a dummy leaf_hash *) 982 + let dummy_leaf = sha256 "\x00dummy-leaf" in 983 + let p0 = List.nth expected_path 0 in 984 + let p1 = List.nth expected_path 1 in 985 + let p2 = List.nth expected_path 2 in 986 + (* Walk RFC 9162 §2.1.3.2: fn=3, sn=4 *) 987 + let r = Scitt.node_hash p0 dummy_leaf in 988 + (* fn=3: LSB=1, left sibling *) 989 + let r = Scitt.node_hash p1 r in 990 + (* fn=1: LSB=1, left sibling *) 991 + let r = Scitt.node_hash r p2 in 992 + (* fn=0, sn=1: right sibling *) 993 + let root = r in 994 + let proof : Scitt.inclusion_proof = 995 + { 996 + leaf_index = 3; 997 + tree_size = 5; 998 + root; 999 + path = expected_path; 1000 + leaf_hash = dummy_leaf; 1001 + } 1002 + in 1003 + Alcotest.(check bool) 1004 + "path self-consistent" true 1005 + (Scitt.verify_inclusion proof); 1006 + (* Decode the actual COSE receipt CBOR and check structure *) 1007 + let receipt_bytes = hex_to_raw rfc9942_inclusion_receipt_hex in 1008 + match Cose.Sign1.decode receipt_bytes with 1009 + | Error _ -> Alcotest.fail "failed to decode RFC 9942 COSE receipt" 1010 + | Ok cose -> ( 1011 + let protected = Cose.Sign1.protected_header cose in 1012 + (* Check kid *) 1013 + Alcotest.(check (option string)) 1014 + "kid" (Some "test-key-1") 1015 + (Cose.Header.key_id protected); 1016 + (* Check vds = 1 (RFC9162_SHA256) *) 1017 + let vds_val = 1018 + match Cose.Header.find 395 protected with 1019 + | Some cbor -> Option.map Z.to_int (Cbort.Cbor.to_int cbor) 1020 + | None -> None 1021 + in 1022 + Alcotest.(check (option int)) "vds" (Some 1) vds_val; 1023 + (* Check unprotected header has proof with tree_size=5, leaf_index=3 *) 1024 + let unprotected = Cose.Sign1.unprotected_header cose in 1025 + match Cose.Header.find 396 unprotected with 1026 + | None -> Alcotest.fail "no vdp in unprotected header" 1027 + | Some vdp_cbor -> ( 1028 + match Cbort.Cbor.to_map vdp_cbor with 1029 + | None -> Alcotest.fail "vdp not a map" 1030 + | Some pairs -> ( 1031 + (* Find inclusion proof (label -1) *) 1032 + let inclusion = 1033 + List.find_opt 1034 + (fun (label, _) -> 1035 + Cbort.Cbor.to_int label = Some (Z.of_int (-1))) 1036 + pairs 1037 + |> Option.map snd 1038 + in 1039 + match inclusion with 1040 + | None -> Alcotest.fail "no inclusion proof in vdp" 1041 + | Some arr -> ( 1042 + match Cbort.Cbor.to_array arr with 1043 + | None -> Alcotest.fail "inclusion not an array" 1044 + | Some [ proof_bstr ] -> ( 1045 + match Cbort.Cbor.to_bytes proof_bstr with 1046 + | None -> Alcotest.fail "proof not bstr" 1047 + | Some proof_bytes -> ( 1048 + match Cbort.decode_string Cbort.any proof_bytes with 1049 + | Error _ -> Alcotest.fail "proof CBOR decode failed" 1050 + | Ok proof_cbor -> ( 1051 + match Cbort.Cbor.to_array proof_cbor with 1052 + | None -> Alcotest.fail "proof not array" 1053 + | Some items -> 1054 + let size = 1055 + Option.map Z.to_int 1056 + (Option.bind (List.nth_opt items 0) 1057 + Cbort.Cbor.to_int) 1058 + in 1059 + let leaf = 1060 + Option.map Z.to_int 1061 + (Option.bind (List.nth_opt items 1) 1062 + Cbort.Cbor.to_int) 1063 + in 1064 + Alcotest.(check (option int)) 1065 + "tree_size" (Some 5) size; 1066 + Alcotest.(check (option int)) 1067 + "leaf_index" (Some 3) leaf; 1068 + let path = 1069 + match List.nth_opt items 2 with 1070 + | Some p -> ( 1071 + match Cbort.Cbor.to_array p with 1072 + | Some hashes -> 1073 + List.filter_map Cbort.Cbor.to_bytes 1074 + hashes 1075 + | None -> []) 1076 + | None -> [] 1077 + in 1078 + Alcotest.(check int) 1079 + "path length" 3 (List.length path); 1080 + (* Path hashes must match *) 1081 + List.iteri 1082 + (fun i h -> 1083 + Alcotest.(check string) 1084 + (Fmt.str "path[%d]" i) 1085 + (List.nth expected_path i) h) 1086 + path))) 1087 + | _ -> Alcotest.fail "unexpected proof array length")))) 1088 + 956 1089 (* ================================================================ *) 957 1090 (* Run *) 958 1091 (* ================================================================ *) ··· 1035 1168 test_interop_decode_verify; 1036 1169 Alcotest.test_case "tampered golden vector" `Quick 1037 1170 test_interop_tampered_vector; 1171 + Alcotest.test_case "RFC 9942 inclusion receipt" `Quick 1172 + test_rfc9942_inclusion_proof; 1038 1173 ] ); 1039 1174 ]