From 5bd8f19b3cb86eea3147bfb9cdd919fe4d88d51a Mon Sep 17 00:00:00 2001 From: Jiale Zhang Date: Tue, 9 Dec 2025 16:30:05 +0800 Subject: [PATCH] RVDS: support ledger eventlog record and ethereumAdapter Signed-off-by: Jiale Zhang --- Cargo.lock | 405 +++++++++++++++++- rvds/Cargo.toml | 8 + rvds/docs/architecture.md | 12 +- rvds/docs/audit-guide.md | 77 ++++ rvds/docs/deployment.md | 4 + rvds/docs/eth-gateway.md | 72 ++++ rvds/docs/flow.md | 2 + rvds/docs/ledger-design.md | 107 +++++ rvds/src/bin/eth_gateway.rs | 158 +++++++ rvds/src/config.rs | 28 ++ rvds/src/ledger/eth_gateway.rs | 85 ++++ rvds/src/ledger/http.rs | 84 ++++ rvds/src/ledger/mod.rs | 58 +++ rvds/src/ledger/noop.rs | 30 ++ rvds/src/main.rs | 1 + rvds/src/models.rs | 16 + rvds/src/routes.rs | 7 +- rvds/src/state.rs | 34 +- .../extractor_modules/sample/mod.rs | 1 + .../extractors/extractor_modules/slsa/mod.rs | 10 +- rvps/src/reference_value.rs | 25 ++ 21 files changed, 1210 insertions(+), 14 deletions(-) create mode 100644 rvds/docs/audit-guide.md create mode 100644 rvds/docs/eth-gateway.md create mode 100644 rvds/docs/ledger-design.md create mode 100644 rvds/src/bin/eth_gateway.rs create mode 100644 rvds/src/ledger/eth_gateway.rs create mode 100644 rvds/src/ledger/http.rs create mode 100644 rvds/src/ledger/mod.rs create mode 100644 rvds/src/ledger/noop.rs diff --git a/Cargo.lock b/Cargo.lock index 1b44b3df..4e12abb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,6 +823,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2b_simd" version = "1.0.3" @@ -879,6 +891,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "byteorder" version = "1.5.0" @@ -1741,6 +1759,50 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror 1.0.69", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + [[package]] name = "eventlog" version = "0.1.0" @@ -1794,6 +1856,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -1850,6 +1924,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -2105,6 +2185,30 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + [[package]] name = "heck" version = "0.4.1" @@ -2540,6 +2644,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "1.0.3" @@ -2561,12 +2675,50 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + [[package]] name = "impl-more" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2717,6 +2869,21 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "jsonwebkey" version = "0.3.5" @@ -2857,6 +3024,15 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "kms" version = "0.1.0" @@ -3400,6 +3576,34 @@ dependencies = [ "sha2", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -3787,6 +3991,28 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3942,6 +4168,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -4243,6 +4475,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "ron" version = "0.7.1" @@ -4332,6 +4574,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4428,16 +4676,24 @@ version = "0.1.0" dependencies = [ "actix-web", "anyhow", + "async-trait", + "base64 0.21.7", "chrono", "env_logger 0.10.2", + "ethabi", "futures", + "hex", "log", + "rand", "reqwest 0.12.12", + "secp256k1", "serde", "serde_json", + "sha2", "thiserror 2.0.12", "tokio", "url", + "web3", ] [[package]] @@ -4635,6 +4891,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -4945,6 +5220,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shadow-rs" version = "0.19.0" @@ -5264,6 +5549,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5385,6 +5676,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -5511,8 +5811,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -5524,6 +5824,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -5533,11 +5842,32 @@ dependencies = [ "indexmap 2.7.1", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap 2.7.1", + "toml_datetime 0.7.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -5784,12 +6114,39 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" @@ -5836,7 +6193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -6090,6 +6447,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web3" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5388522c899d1e1c96a4c307e3797e0f697ba7c77dd8e0e625ecba9dd0342937" +dependencies = [ + "arrayvec", + "base64 0.21.7", + "bytes", + "derive_more", + "ethabi", + "ethereum-types", + "futures", + "futures-timer", + "headers", + "hex", + "idna 0.4.0", + "jsonrpc-core", + "log", + "once_cell", + "parking_lot 0.12.3", + "pin-project", + "reqwest 0.11.27", + "rlp", + "secp256k1", + "serde", + "serde_json", + "tiny-keccak", + "url", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -6375,6 +6763,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.14.0" diff --git a/rvds/Cargo.toml b/rvds/Cargo.toml index cf6f976d..3924a8bb 100644 --- a/rvds/Cargo.toml +++ b/rvds/Cargo.toml @@ -19,5 +19,13 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } url = "2" +sha2 = { workspace = true } +async-trait = { workspace = true } +hex = { workspace = true } +rand = "0.8" +web3 = { version = "0.19", default-features = false, features = ["http", "signing"] } +ethabi = "18" +secp256k1 = { version = "0.27", features = ["rand"] } +base64 = { workspace = true } diff --git a/rvds/docs/architecture.md b/rvds/docs/architecture.md index 6f204ba5..17a346a1 100644 --- a/rvds/docs/architecture.md +++ b/rvds/docs/architecture.md @@ -14,6 +14,7 @@ - **HTTP API(Actix-web)**:暴露 `/rvds/*` 接口,负责参数校验与响应封装。 - **订阅注册表(Subscriber Registry)**:用 `HashSet` 存储已注册的 Trustee 基址,持久化于 `data/rvds/subscribers.json`。 - **事件转发器(Forwarder)**:接收发布事件后,构造 RVPS 期望的 `message` 包裹并并发调用各 Trustee 的 `/api/rvps/register`。 +- **账本记录器(Ledger Recorder)**:对 `PublishEventRequest` 做规范化哈希,写入外部不可篡改账本(默认 noop,可配置 HTTP / 以太坊网关),返回记录凭据,并将审计凭据随 payload 一并下发。 - **配置与启动器(Config / Bootstrap)**:从环境变量加载监听地址、数据目录、下游调用超时等参数。 ## 数据模型 @@ -27,7 +28,14 @@ { "artifact_type": "rpm", "slsa_provenance": ["..."], // 多个 provenance(原文或 base64),数组形式 - "artifacts_download_url": ["https://...rpm"] + "artifacts_download_url": ["https://...rpm"], + "audit_proof": { // 可选,账本回执 + "backend": "ethereum-gateway", + "handle": "0x", + "event_hash": "", + "payload_hash": "", + "payload_b64": "" + } } ``` - **转发给 RVPS 的请求** @@ -45,7 +53,7 @@ - 返回:已新增的地址列表。 - `POST /rvds/rv-publish-event` - 功能:校验事件并转发到全部 Trustee。 - - 返回:每个 Trustee 的投递结果(成功/失败与错误信息)。 + - 返回:每个 Trustee 的投递结果(成功/失败与错误信息),以及可选的 ledger 记录凭据。 ## 工作流程 diff --git a/rvds/docs/audit-guide.md b/rvds/docs/audit-guide.md new file mode 100644 index 00000000..15dfd5d1 --- /dev/null +++ b/rvds/docs/audit-guide.md @@ -0,0 +1,77 @@ +# 第三方审计操作手册(RVDS + RVPS + 以太坊账本) + +本手册指导审计者仅凭 RVPS 返回的参考值(含审计字段)完成端到端验证,确认 RVDS 发布事件已被写入不可篡改账本(以太坊为例),并与 RVPS 内的参考值一致。账本侧仅写入摘要(hash),原文由 RVPS 的 `audit_proof.payload_b64` 提供。 + +## 前提信息 +- RVPS 查询到的 `ReferenceValue`,其中可选字段 `audit_proof`: + ```json + { + "name": "...", + "hash-value": [...], + "audit_proof": { + "backend": "ethereum-gateway", + "handle": "0x", + "event_hash": "", + "payload_hash": "", + "payload_b64": "" + } + } + ``` +- RVDS 账本记录:在 ledger_receipt 中会返回与 `audit_proof` 对应的字段。 +- 合约地址与链信息:`ETH_CONTRACT_ADDRESS`,链 ID,RPC/浏览器入口。 + +## 步骤 1:本地重算并校验摘要 +1. 从 RVPS 返回的 `audit_proof.payload_b64` 解码得到 canonical `PublishEventRequest` JSON。 +2. 本地计算: + - `sha256(payload)` → 对比 `payload_hash`。 + - `sha256(canonical payload)` → 对比 `event_hash`。 + 若不一致,审计失败。 + +## 步骤 2:链上验证交易与事件 +1. 取 `handle`(`tx_hash`),在浏览器或本地节点查询: + - 浏览器:输入 tx_hash。 + - 节点:`eth_getTransactionReceipt `. +2. 确认交易成功,找到来自合约 `RvdsEventLog` 的 `EventRecorded` 事件。 +3. 解码事件参数(浏览器通常自动解码): + - `eventHash` (bytes32) 应等于 `audit_proof.event_hash`。 + - 第二个参数存储的是 `payloadHash`(摘要上链),应等于 `audit_proof.payload_hash`。 + +## 步骤 3:对照 RVPS 参考值 +1. 从 `payload` 中的 `slsa_provenance` 解析 `subject[].digest`(和 RVPS 逻辑一致)。 +2. 确认解析出的制品哈希与 RVPS 返回的 `hash-value` 完全匹配。 +3. 如有多份参考值(多个 subject),逐一对应。 + +## 命令行示例 +假设已拿到 `tx_hash`、`event_hash`、`payload_hash`、`payload_b64`: + +```bash +# 1) 查询交易(需已配置 ETH_RPC_URL) +curl -s -X POST "$ETH_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc":"2.0", + "method":"eth_getTransactionReceipt", + "params":[""], + "id":1 + }' | jq . + +# 2) 解析 logs,确认事件里 eventHash == audit_proof.event_hash,payloadHash == audit_proof.payload_hash +# 3) 解码 RVPS 提供的 payload_b64 并重算 hash +echo "" | base64 -d > payload.json +PAYLOAD_HASH_LOCAL=$(sha256sum payload.json | awk '{print $1}') +echo "local payload hash: $PAYLOAD_HASH_LOCAL" +# 对比 audit_proof.payload_hash / event_hash +``` + +## 浏览器快速校验 +1. 打开区块浏览器,输入 `tx_hash`。 +2. 在 Logs/Events 中找到 `EventRecorded`: + - `eventHash` 对比 `audit_proof.event_hash`。 + - data/decoded payloadHash 对比 `audit_proof.payload_hash`。 + - 原文由 RVPS 的 `payload_b64` 提供,在本地解码后重算哈希比对。 + +## 注意事项与影响 +- 账本只写 hash,原文不上链;RVPS 存储 payload_b64 供审计端重算哈希。 +- `audit_proof` 可选,未开启账本时保持兼容(字段缺失不影响现有接口)。 +- 若使用其它账本(如 Rekor),在 `audit_proof.backend/handle` 写入对应证明,验证时使用相应工具;结构不变。 + diff --git a/rvds/docs/deployment.md b/rvds/docs/deployment.md index bb9c3870..6436d054 100644 --- a/rvds/docs/deployment.md +++ b/rvds/docs/deployment.md @@ -13,6 +13,9 @@ - `RVDS_LISTEN_ADDR`:监听地址,默认 `0.0.0.0:8090` - `RVDS_DATA_DIR`:订阅持久化目录,默认 `data/rvds` - `RVDS_FORWARD_TIMEOUT_SECS`:转发超时秒数,默认 `10` +- `RVDS_LEDGER_BACKEND`:`none`(默认)、`http`、`eth` +- `RVDS_LEDGER_HTTP_ENDPOINT` / `RVDS_LEDGER_HTTP_API_KEY`:账本网关(http)配置 +- `RVDS_LEDGER_ETH_GATEWAY` / `RVDS_LEDGER_ETH_GATEWAY_API_KEY`:以太坊网关配置 - `RUST_LOG`:日志等级,如 `info,rvds=debug` ## 源码构建运行 @@ -73,5 +76,6 @@ curl -k -X POST http://localhost:8090/rvds/rv-publish-event \ - 在 release workflow 中生成 `PublishEventRequest`,通过 `curl`/`gh api` 等 POST 到 RVDS。 - RVDS 会自动并发转发到已注册的 Trustee;失败结果会返回在响应中,供重试或告警。 +- 若配置了 ledger,RVDS 会在响应和下游 payload 中附带 `audit_proof`(含 event_hash/payload_hash/payload_b64、tx 句柄等),便于 RVPS/审计使用。 diff --git a/rvds/docs/eth-gateway.md b/rvds/docs/eth-gateway.md new file mode 100644 index 00000000..b5220ea6 --- /dev/null +++ b/rvds/docs/eth-gateway.md @@ -0,0 +1,72 @@ +# RVDS 以太坊网关使用指南 + +> 作用:提供一个具备签名与上链能力的 HTTP 网关,接受 RVDS 的事件摘要请求,调用链上合约 `record(bytes32 eventHash, string payloadHash)`,并返回真实交易哈希。 + +## 功能 + +- 端点:`POST /record` +- 请求: + ```json + { + "event_hash": "0x", // 32字节 + "payload": "canonical PublishEventRequest JSON" + } + ``` +- 响应: + ```json + { "tx_hash": "0x" } + ``` +- 内部逻辑: + - 对 payload 做 sha256,作为 `payloadHash`。 + - ABI 编码调用合约 `record(eventHash, payloadHash)`。 + - 使用私钥签名交易并通过 RPC 发送,返回 tx_hash。 + - 默认 gas 200000,gas_price 来自链上 `eth_gasPrice`。 + +## 启动 + +```bash +cd /root/design/trustee/rvds +cargo run --bin eth_gateway +``` + +必需环境变量: +- `ETH_RPC_URL`:以太坊 RPC 地址(可用公用节点或自建节点) +- `ETH_PRIVATE_KEY`:0x 前缀的私钥(用于签名) +- `ETH_CONTRACT_ADDRESS`:已部署的合约地址(示例见下) + +可选环境变量: +- `ETH_GATEWAY_LISTEN`:监听地址,默认 `0.0.0.0:8095` +- `ETH_CHAIN_ID`:链 ID,默认 `1` + +## 合约示例(Solidity 0.8.x) + +```solidity +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +contract RvdsEventLog { + event EventRecorded(bytes32 indexed eventHash, string payloadHash, address indexed sender, uint256 blockNumber); + + function record(bytes32 eventHash, string calldata payloadHash) external { + emit EventRecorded(eventHash, payloadHash, msg.sender, block.number); + } +} +``` + +部署后将合约地址写入 `ETH_CONTRACT_ADDRESS`。 + +## 与 RVDS 对接 + +在 RVDS 配置: +- `RVDS_LEDGER_BACKEND=eth` +- `RVDS_LEDGER_ETH_GATEWAY=http://:8095/record` +- `RVDS_LEDGER_ETH_GATEWAY_API_KEY`(当前未校验,可留空) + +RVDS 在 `/rvds/rv-publish-event` 时调用网关,`ledger_receipt.handle` 将包含真实链上 `tx_hash`。 + +## 生产注意 + +- 私钥务必放在可信环境,可优先使用 KMS/HSM 管理;当前示例从环境变量读取,仅适用于 PoC。 +- 请确认 gas 费、nonce 管理符合预期;当前实现使用链上 `gasPrice`,固定 gas=200000,可按需调整或改为 `eth_estimateGas`。 +- 如需鉴权,可在网关增加 API Key/Token 校验,并在 RVDS 设置 `RVDS_LEDGER_ETH_GATEWAY_API_KEY`。 + diff --git a/rvds/docs/flow.md b/rvds/docs/flow.md index 89627608..b8bc51bb 100644 --- a/rvds/docs/flow.md +++ b/rvds/docs/flow.md @@ -23,12 +23,14 @@ } ``` - 并发调用每个 Trustee 的 `https:///api/rvps/register`。 + - 同步将事件摘要(canonical + sha256)写入外部账本(若启用),返回 ledger 凭据,并把审计字段(`audit_proof`,含 event_hash/payload_hash/payload_b64)附在 payload 中下发。 4. **校验与入库** - Trustee Gateway 将请求转给 RVPS gRPC `RegisterReferenceValue`。 - RVPS 通过 `slsa` extractor: - 解析 `payload` 得到 `artifact_type/slsa_provenance[]/artifacts_download_url`。 - 针对每个 provenance(raw JSON 或 base64 JSON)解析 subject,支持多份 provenance。 - 抽取 `subject[].digest`(优先 `sha256`),生成 `ReferenceValue`(默认 12 个月有效)。 + - 将 `audit_proof` 落在 ReferenceValue(便于后续第三方审计)。 - 调用存储接口写入参考值。 5. **消费** - 上游(如 AS)调用 RVPS `query_reference_value` 获取可信哈希用于度量验证。 diff --git a/rvds/docs/ledger-design.md b/rvds/docs/ledger-design.md new file mode 100644 index 00000000..4dd53727 --- /dev/null +++ b/rvds/docs/ledger-design.md @@ -0,0 +1,107 @@ +# RVDS 发布事件不可篡改账本记录设计 + +## 背景与目标 + +- 发布事件需要“公开可审计、不可篡改”地留痕(典型载体:区块链 / 透明日志)。 +- RVDS 在收到 `PublishEventRequest` 时,除转发给 Trustee 外,还需将事件摘要写入外部账本,并返回记录凭据。 +- 设计需可扩展:不同场景可接入不同账本(链、透明日志、第三方网关)。 + +## 设计概览 + +- 新增 **Ledger Recorder** 组件,核心是 `LedgerAdapter` 抽象: + - `record_event(event, canonical_payload) -> LedgerReceipt` +- 内置实现:`NoopLedger`(默认)、`HttpLedger`(对接外部账本网关)、`EthGatewayLedger`(通过以太坊网关写入链上日志)。 +- 事件哈希: + - 使用 canonical JSON(对 `PublishEventRequest` 进行稳定序列化)后计算 `SHA256`,得到 `event_hash` 与 `payload_hash`。 + - 账本端仅写入摘要(hash),不携带原文;原始 payload 以 base64 形式保存在 RVPS 的 `audit_proof` 中,供审计者校验。 +- RVDS API 回包增强: + - `PublishResponse` 增加 `ledger_receipt`,便于 CI 在发布日志中附带证明。 + +## 账本适配层 + +### 接口 +```rust +pub trait LedgerAdapter { + async fn record_event(&self, event: &PublishEventRequest, canonical_payload: &str) -> Result; +} +``` + +### 内置实现 +- `NoopLedger`:不写外部账本,返回合成的 `event_hash`。 +- `HttpLedger`:POST `{event_hash, payload}` 到外部网关;网关可自行决定只上链 hash 或其它策略;`ledger_receipt` 会携带本地的 `payload_b64` 供审计者使用。 +- `EthGatewayLedger`:面向以太坊链,通过网关 POST `{event_hash, payload}`,网关签名调用合约时仅写入摘要(hash),返回 `tx_hash`;`ledger_receipt` 同样附带 `payload_b64`。 + +### 选择与配置 +- 环境变量: + - `RVDS_LEDGER_BACKEND`:`none`(默认)、`http` 或 `eth` + - `RVDS_LEDGER_HTTP_ENDPOINT`:后端网关地址(`http` 模式必填) + - `RVDS_LEDGER_HTTP_API_KEY`:可选鉴权 + - `RVDS_LEDGER_ETH_GATEWAY`:以太坊网关地址(`eth` 模式必填) + - `RVDS_LEDGER_ETH_GATEWAY_API_KEY`:以太坊网关鉴权(可选) +- 未来扩展: + - 可增加 `RekorAdapter`(Sigstore 透明日志)、`EthereumAdapter`(合约记录 `event_hash`),只需实现 `LedgerAdapter` 并在工厂中注册。 + +## 时序(含账本) + +1. CI 调用 `POST /rvds/rv-publish-event`。 +2. RVDS 规范化序列化 payload,计算 `event_hash`。 +3. Ledger Recorder 通过适配器写入外部账本,获得 `ledger_receipt`(可能为空)。 +4. RVDS 按原逻辑并发转发给各 Trustee 的 `/api/rvps/register`。 +5. RVDS 返回(示例): + ```json + { + "forwarded": [...], + "ledger_receipt": { + "backend": "http", + "handle": "", + "event_hash": "", + "payload_hash": "", + "payload_b64": "" + } + } + ``` +6. 可选:Trustee/RVPS 存储或透传 `ledger_receipt`,便于后续审计。 + +- ## 容错与安全 +- +- Ledger 失败策略:记录 warning 并继续转发(可根据需求改成强制失败)。 +- 哈希基于 canonical payload,避免字段顺序影响;业务如需更强规范,可固定字段排序/移除冗余。 +- 如需强制验证 ledger 成功,可在生产环境加开关:失败则拒绝发布。 +- +- ## 实现范围(本次) +- +- 新增适配层与配置,默认 `none`,支持 `http` 网关写入,以及 `eth`(以太坊网关)模式。 +- 在 `PublishResponse` 返回 `ledger_receipt`。 +- 文档同步:architecture、flow 以及本设计说明。 + +## 以太坊链设计(网关模式)与合约示例 + +- 模式:RVDS 通过 `EthGatewayLedger` 把 `{event_hash, payload}` POST 到以太坊网关;网关负责签名并调用链上合约时仅写入摘要(hash),返回 `tx_hash`。原文不链上存储,`payload_b64` 仅在 RVPS 中保存。 +- 网关接口建议: + - `POST /record` `{ "event_hash": "", "payload": "" }` + - 返回 `{ "tx_hash": "0x..." }` +- 合约示例(Solidity 0.8.x): + ```solidity + // SPDX-License-Identifier: Apache-2.0 + pragma solidity ^0.8.17; + + contract RvdsEventLog { + event EventRecorded(bytes32 indexed eventHash, string payloadHash, address indexed sender, uint256 blockNumber); + + function record(bytes32 eventHash, string calldata payloadHash) external { + emit EventRecorded(eventHash, payloadHash, msg.sender, block.number); + } + } + ``` +- 部署与网关步骤(概要): + 1. 选择链(主网/测试网)并配置 RPC。 + 2. 部署合约 `RvdsEventLog`,记录下合约地址。 + 3. 网关持有链上账户私钥,暴露 REST 接口 `/record`: + - 将 `event_hash`、`payloadHash`(可使用同样的 sha256/canonical)调用合约 `record`。 + - 返回 `tx_hash` 供 RVDS 作为 `handle`。 + 4. 在 RVDS 配置: + - `RVDS_LEDGER_BACKEND=eth` + - `RVDS_LEDGER_ETH_GATEWAY=https:///record` + - `RVDS_LEDGER_ETH_GATEWAY_API_KEY=`(若需要) + 5. 审计时:根据 `event_hash` 计算后,在链上事件日志中查找 `EventRecorded`,确认对应 `tx_hash` 与区块高度。 + diff --git a/rvds/src/bin/eth_gateway.rs b/rvds/src/bin/eth_gateway.rs new file mode 100644 index 00000000..09ad341b --- /dev/null +++ b/rvds/src/bin/eth_gateway.rs @@ -0,0 +1,158 @@ +use actix_web::{post, web, App, HttpResponse, HttpServer, Responder}; +use ethabi::{Function, Param, ParamType, StateMutability, Token}; +use hex::FromHex; +use secp256k1::SecretKey; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::env; +use web3::{ + transports::Http, + types::{Address, Bytes, TransactionParameters, H160, U256}, + Web3, +}; + +#[derive(Deserialize)] +struct RecordRequest { + event_hash: String, // hex string 0x... + payload: String, // canonical JSON string +} + +#[derive(Serialize)] +struct RecordResponse { + tx_hash: String, +} + +/// ABI for function record(bytes32,string) +fn record_function() -> Function { + Function { + name: "record".to_string(), + inputs: vec![ + Param { + name: "eventHash".to_string(), + kind: ParamType::FixedBytes(32), + internal_type: None, + }, + Param { + name: "payloadHash".to_string(), + kind: ParamType::String, + internal_type: None, + }, + ], + outputs: vec![], + constant: None, + state_mutability: StateMutability::NonPayable, + } +} + +#[post("/record")] +async fn record(body: web::Json, data: web::Data) -> impl Responder { + // Hash payload;链上仅写入摘要,原文不直接上链 + let payload_hash = hex::encode(Sha256::digest(body.payload.as_bytes())); + + // Decode event_hash + let event_bytes = match Vec::from_hex(body.event_hash.trim_start_matches("0x")) { + Ok(v) => v, + Err(e) => { + return HttpResponse::BadRequest().body(format!("invalid event_hash: {e}")); + } + }; + if event_bytes.len() != 32 { + return HttpResponse::BadRequest().body(format!( + "event_hash must be 32 bytes, got {}", + event_bytes.len() + )); + } + + // Build call data + let func = record_function(); + let call_data = match func.encode_input(&[ + Token::FixedBytes(event_bytes.clone()), + Token::String(payload_hash.clone()), // 仅上链哈希,降低 gas + ]) { + Ok(data) => data, + Err(e) => return HttpResponse::InternalServerError().body(format!("encode abi: {e}")), + }; + + // Prepare tx params + let web3 = &data.web3; + let from: H160 = data.from; + let nonce = match web3.eth().transaction_count(from, None).await { + Ok(n) => n, + Err(e) => return HttpResponse::InternalServerError().body(format!("nonce error: {e}")), + }; + + let gas_price = match web3.eth().gas_price().await { + Ok(p) => p, + Err(e) => return HttpResponse::InternalServerError().body(format!("gas_price error: {e}")), + }; + + let tx = TransactionParameters { + to: Some(data.contract), + gas_price: Some(gas_price), + gas: U256::from(200_000u64), + value: U256::zero(), + data: Bytes(call_data), + nonce: Some(nonce), + ..Default::default() + }; + + let signed = match web3.accounts().sign_transaction(tx, &data.sk).await { + Ok(s) => s, + Err(e) => return HttpResponse::InternalServerError().body(format!("sign error: {e}")), + }; + + let pending = match web3 + .eth() + .send_raw_transaction(signed.raw_transaction) + .await + { + Ok(p) => p, + Err(e) => return HttpResponse::InternalServerError().body(format!("send tx error: {e}")), + }; + + HttpResponse::Ok().json(RecordResponse { + tx_hash: format!("{:#x}", pending), + }) +} + +struct AppState { + web3: Web3, + sk: SecretKey, + from: Address, + contract: Address, +} + +#[actix_web::main] +pub async fn main() -> std::io::Result<()> { + env_logger::init(); + let listen = env::var("ETH_GATEWAY_LISTEN").unwrap_or_else(|_| "0.0.0.0:8095".to_string()); + let rpc = env::var("ETH_RPC_URL").expect("ETH_RPC_URL is required"); + let pk_hex = env::var("ETH_PRIVATE_KEY").expect("ETH_PRIVATE_KEY is required (0x...)"); + let contract_addr = env::var("ETH_CONTRACT_ADDRESS").expect("ETH_CONTRACT_ADDRESS required"); + let sk = SecretKey::from_slice( + &hex::decode(pk_hex.trim_start_matches("0x")).expect("invalid ETH_PRIVATE_KEY hex"), + ) + .expect("invalid ETH_PRIVATE_KEY"); + let from: Address = H160::from_slice( + &web3::signing::keccak256( + &secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk) + .serialize_uncompressed()[1..], + )[12..], + ); + let contract: Address = contract_addr.parse().expect("invalid ETH_CONTRACT_ADDRESS"); + let transport = web3::transports::Http::new(&rpc).expect("invalid ETH_RPC_URL"); + let web3 = Web3::new(transport); + + let state = web::Data::new(AppState { + web3, + sk, + from, + contract, + }); + + println!("Starting eth-gateway real on {listen}, from={from:?}, contract={contract:?}"); + HttpServer::new(move || App::new().app_data(state.clone()).service(record)) + .bind(listen)? + .run() + .await +} diff --git a/rvds/src/config.rs b/rvds/src/config.rs index 38d38a27..a56b6142 100644 --- a/rvds/src/config.rs +++ b/rvds/src/config.rs @@ -4,12 +4,27 @@ use std::time::Duration; use anyhow::{anyhow, Context, Result}; +#[derive(Clone, Debug)] +pub struct LedgerConfig { + /// Backend type: "none" (default) or "http" (external ledger gateway). + pub backend: String, + /// HTTP endpoint for the ledger gateway. + pub http_endpoint: Option, + /// Optional API key for the ledger gateway. + pub http_api_key: Option, + /// Ethereum gateway endpoint that relays tx to chain. + pub eth_gateway_endpoint: Option, + /// Optional API key for the ethereum gateway. + pub eth_gateway_api_key: Option, +} + /// Application level configuration loaded from environment variables. #[derive(Clone, Debug)] pub struct AppConfig { pub listen_addr: String, pub data_dir: PathBuf, pub request_timeout: Duration, + pub ledger: LedgerConfig, } impl AppConfig { @@ -36,10 +51,23 @@ impl AppConfig { )); } + let ledger_backend = env::var("RVDS_LEDGER_BACKEND").unwrap_or_else(|_| "none".to_string()); + let ledger_http_endpoint = env::var("RVDS_LEDGER_HTTP_ENDPOINT").ok(); + let ledger_http_api_key = env::var("RVDS_LEDGER_HTTP_API_KEY").ok(); + let eth_gateway_endpoint = env::var("RVDS_LEDGER_ETH_GATEWAY").ok(); + let eth_gateway_api_key = env::var("RVDS_LEDGER_ETH_GATEWAY_API_KEY").ok(); + Ok(Self { listen_addr, data_dir, request_timeout: Duration::from_secs(request_timeout_secs), + ledger: LedgerConfig { + backend: ledger_backend, + http_endpoint: ledger_http_endpoint, + http_api_key: ledger_http_api_key, + eth_gateway_endpoint, + eth_gateway_api_key, + }, }) } } diff --git a/rvds/src/ledger/eth_gateway.rs b/rvds/src/ledger/eth_gateway.rs new file mode 100644 index 00000000..66bc6ffd --- /dev/null +++ b/rvds/src/ledger/eth_gateway.rs @@ -0,0 +1,85 @@ +use anyhow::{Context, Result}; +use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD as B64; +use base64::Engine; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::time::Duration; + +use crate::config::LedgerConfig; +use crate::models::PublishEventRequest; + +use super::{LedgerAdapter, LedgerReceipt}; + +pub struct EthGatewayLedger { + client: Client, + endpoint: String, + api_key: Option, + timeout: Duration, +} + +impl EthGatewayLedger { + pub fn new(cfg: &LedgerConfig, client: Client) -> Result { + let endpoint = cfg + .eth_gateway_endpoint + .clone() + .context("RVDS_LEDGER_ETH_GATEWAY required for eth ledger")?; + Ok(Self { + client, + endpoint, + api_key: cfg.eth_gateway_api_key.clone(), + timeout: Duration::from_secs(60), + }) + } +} + +#[derive(Serialize)] +struct EthGatewayRequest<'a> { + event_hash: &'a str, + payload: &'a str, +} + +#[derive(Deserialize)] +struct EthGatewayResponse { + tx_hash: String, +} + +#[async_trait] +impl LedgerAdapter for EthGatewayLedger { + async fn record_event( + &self, + _event: &PublishEventRequest, + canonical_payload: &str, + ) -> Result { + let event_hash = hex::encode(Sha256::digest(canonical_payload.as_bytes())); + let payload_hash = hex::encode(Sha256::digest(canonical_payload.as_bytes())); + let payload_b64 = B64.encode(canonical_payload.as_bytes()); + let req_body = EthGatewayRequest { + event_hash: &event_hash, + payload: canonical_payload, + }; + + let mut req = self.client.post(&self.endpoint).json(&req_body); + if let Some(key) = &self.api_key { + req = req.header("Authorization", format!("Bearer {key}")); + } + + let resp = tokio::time::timeout(self.timeout, req.send()) + .await + .context("eth gateway timeout")? + .context("eth gateway request error")? + .error_for_status() + .context("eth gateway status")?; + + let parsed: EthGatewayResponse = resp.json().await.context("parse eth gateway response")?; + + Ok(LedgerReceipt { + backend: "ethereum-gateway".to_string(), + handle: parsed.tx_hash, + event_hash, + payload_hash: Some(payload_hash), + payload_b64: Some(payload_b64), + }) + } +} diff --git a/rvds/src/ledger/http.rs b/rvds/src/ledger/http.rs new file mode 100644 index 00000000..0b89c966 --- /dev/null +++ b/rvds/src/ledger/http.rs @@ -0,0 +1,84 @@ +use anyhow::{Context, Result}; +use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD as B64; +use base64::Engine; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::config::LedgerConfig; +use crate::models::PublishEventRequest; + +use super::{LedgerAdapter, LedgerReceipt}; + +/// Simple HTTP ledger: POST canonical payload hash to an external service that acts +/// as a gateway to a public immutable log (e.g., blockchain/transparent log). +pub struct HttpLedger { + client: Client, + endpoint: String, + api_key: Option, +} + +impl HttpLedger { + pub fn new(cfg: &LedgerConfig, client: Client) -> Result { + let endpoint = cfg + .http_endpoint + .clone() + .context("RVDS_LEDGER_HTTP_ENDPOINT required for http ledger")?; + Ok(Self { + client, + endpoint, + api_key: cfg.http_api_key.clone(), + }) + } +} + +#[derive(Serialize)] +struct HttpLedgerRequest<'a> { + event_hash: &'a str, + payload: &'a str, +} + +#[derive(Deserialize)] +struct HttpLedgerResponse { + handle: String, +} + +#[async_trait] +impl LedgerAdapter for HttpLedger { + async fn record_event( + &self, + _event: &PublishEventRequest, + canonical_payload: &str, + ) -> Result { + let event_hash = hex::encode(Sha256::digest(canonical_payload.as_bytes())); + let payload_hash = hex::encode(Sha256::digest(canonical_payload.as_bytes())); + let payload_b64 = B64.encode(canonical_payload.as_bytes()); + let req_body = HttpLedgerRequest { + event_hash: &event_hash, + payload: canonical_payload, + }; + + let mut req = self.client.post(&self.endpoint).json(&req_body); + if let Some(key) = &self.api_key { + req = req.header("Authorization", format!("Bearer {key}")); + } + + let resp = req + .send() + .await + .context("send ledger http request")? + .error_for_status() + .context("ledger http status")?; + + let parsed: HttpLedgerResponse = resp.json().await.context("parse ledger response")?; + + Ok(LedgerReceipt { + backend: "http".to_string(), + handle: parsed.handle, + event_hash: event_hash.clone(), + payload_hash: Some(payload_hash), + payload_b64: Some(payload_b64), + }) + } +} diff --git a/rvds/src/ledger/mod.rs b/rvds/src/ledger/mod.rs new file mode 100644 index 00000000..7772bda4 --- /dev/null +++ b/rvds/src/ledger/mod.rs @@ -0,0 +1,58 @@ +mod eth_gateway; +mod http; +mod noop; + +use anyhow::Result; +use log::warn; +use reqwest::Client; +use std::sync::Arc; + +use crate::config::LedgerConfig; +use crate::models::PublishEventRequest; + +pub use eth_gateway::EthGatewayLedger; +pub use http::HttpLedger; +pub use noop::NoopLedger; + +/// Receipt returned after persisting an event into an external immutable ledger. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct LedgerReceipt { + pub backend: String, + pub handle: String, + pub event_hash: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload_b64: Option, +} + +/// Ledger adapter trait to allow swapping implementations (none/http/eth...). +#[async_trait::async_trait] +pub trait LedgerAdapter: Send + Sync { + async fn record_event( + &self, + event: &PublishEventRequest, + canonical_payload: &str, + ) -> Result; +} + +/// Factory to build ledger adapter based on configuration. +pub fn build_ledger(cfg: &LedgerConfig, client: Client) -> Arc { + match cfg.backend.as_str() { + "http" => match HttpLedger::new(cfg, client) { + Ok(adp) => Arc::new(adp), + Err(e) => { + warn!("Http ledger init failed ({e:?}), falling back to noop."); + Arc::new(NoopLedger) + } + }, + "eth" => match EthGatewayLedger::new(cfg, client) { + Ok(adp) => Arc::new(adp), + Err(e) => { + warn!("Ethereum ledger init failed ({e:?}), falling back to noop."); + Arc::new(NoopLedger) + } + }, + "none" | _ => Arc::new(NoopLedger), + } +} diff --git a/rvds/src/ledger/noop.rs b/rvds/src/ledger/noop.rs new file mode 100644 index 00000000..e2996fa9 --- /dev/null +++ b/rvds/src/ledger/noop.rs @@ -0,0 +1,30 @@ +use anyhow::Result; +use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD as B64; +use base64::Engine; +use sha2::{Digest, Sha256}; + +use crate::models::PublishEventRequest; + +use super::{LedgerAdapter, LedgerReceipt}; + +/// No-op ledger used by default; returns a synthetic receipt. +pub struct NoopLedger; + +#[async_trait] +impl LedgerAdapter for NoopLedger { + async fn record_event( + &self, + _event: &PublishEventRequest, + canonical_payload: &str, + ) -> Result { + let event_hash = hex::encode(Sha256::digest(canonical_payload.as_bytes())); + Ok(LedgerReceipt { + backend: "none".to_string(), + handle: "noop".to_string(), + event_hash, + payload_hash: Some(hex::encode(Sha256::digest(canonical_payload.as_bytes()))), + payload_b64: Some(B64.encode(canonical_payload.as_bytes())), + }) + } +} diff --git a/rvds/src/main.rs b/rvds/src/main.rs index 561641d3..38bbbf5b 100644 --- a/rvds/src/main.rs +++ b/rvds/src/main.rs @@ -1,5 +1,6 @@ mod config; mod error; +mod ledger; mod models; mod routes; mod state; diff --git a/rvds/src/models.rs b/rvds/src/models.rs index b215dc55..fc09b131 100644 --- a/rvds/src/models.rs +++ b/rvds/src/models.rs @@ -1,6 +1,18 @@ use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; +/// Generic audit proof that can point to different ledger backends. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AuditProof { + pub backend: String, + pub handle: String, + pub event_hash: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload_b64: Option, +} + /// Register trustee subscription request body. #[derive(Debug, Deserialize)] pub struct SubscribeRequest { @@ -24,6 +36,9 @@ pub struct PublishEventRequest { /// A list of SLSA provenance documents (raw JSON or base64-encoded JSON). pub slsa_provenance: Vec, pub artifacts_download_url: Vec, + /// Optional audit proof (ledger) attached by RVDS. + #[serde(skip_serializing_if = "Option::is_none")] + pub audit_proof: Option, } impl PublishEventRequest { @@ -75,4 +90,5 @@ pub struct SubscribeResponse { #[derive(Debug, Serialize)] pub struct PublishResponse { pub forwarded: Vec, + pub ledger_receipt: Option, } diff --git a/rvds/src/routes.rs b/rvds/src/routes.rs index 706bb38d..c351178c 100644 --- a/rvds/src/routes.rs +++ b/rvds/src/routes.rs @@ -29,6 +29,9 @@ async fn rv_publish_event( state: web::Data, payload: web::Json, ) -> Result { - let results = state.forward_publish_event(payload.into_inner()).await?; - Ok(HttpResponse::Ok().json(PublishResponse { forwarded: results })) + let (results, receipt) = state.forward_publish_event(payload.into_inner()).await?; + Ok(HttpResponse::Ok().json(PublishResponse { + forwarded: results, + ledger_receipt: receipt, + })) } diff --git a/rvds/src/state.rs b/rvds/src/state.rs index 87d2b35e..5c49f18a 100644 --- a/rvds/src/state.rs +++ b/rvds/src/state.rs @@ -12,6 +12,7 @@ use url::Url; use crate::config::AppConfig; use crate::error::ApiError; +use crate::ledger::{build_ledger, LedgerAdapter, LedgerReceipt}; use crate::models::{ ForwardResult, PublishEventRequest, RvpsMessageEnvelope, RvpsRegisterRequest, SubscribeRequest, }; @@ -22,6 +23,7 @@ pub struct AppState { storage_path: PathBuf, http_client: Client, request_timeout: Duration, + ledger: std::sync::Arc, } impl AppState { @@ -38,12 +40,14 @@ impl AppState { .timeout(cfg.request_timeout) .build() .context("build reqwest client")?; + let ledger = build_ledger(&cfg.ledger, http_client.clone()); Ok(Self { subscribers: std::sync::Arc::new(RwLock::new(subscribers)), storage_path, http_client, request_timeout: cfg.request_timeout, + ledger, }) } @@ -79,8 +83,8 @@ impl AppState { /// Forward publish events to every registered trustee. pub async fn forward_publish_event( &self, - event: PublishEventRequest, - ) -> Result, ApiError> { + mut event: PublishEventRequest, + ) -> Result<(Vec, Option), ApiError> { event .validate() .map_err(|e| ApiError::Validation(e.to_string()))?; @@ -91,7 +95,7 @@ impl AppState { let message_envelope = RvpsMessageEnvelope { version: "0.1.0".to_string(), typ: "slsa".to_string(), - payload: payload_json, + payload: payload_json.clone(), }; let envelope_str = serde_json::to_string(&message_envelope) .map_err(|e| ApiError::Internal(format!("serialize envelope: {e}")))?; @@ -108,13 +112,35 @@ impl AppState { warn!("No trustee subscribers registered; skipping forward."); } + // Record in external ledger (if enabled). + let ledger_receipt = match self.ledger.record_event(&event, &payload_json).await { + Ok(r) => { + info!( + "Ledger recorded event_hash={} via {}", + r.event_hash, r.backend + ); + event.audit_proof = Some(crate::models::AuditProof { + backend: r.backend.clone(), + handle: r.handle.clone(), + event_hash: r.event_hash.clone(), + payload_hash: r.payload_hash.clone(), + payload_b64: r.payload_b64.clone(), + }); + Some(r) + } + Err(e) => { + warn!("Ledger recording failed: {e}"); + None + } + }; + // Dispatch webhooks concurrently to reduce tail latency. let futs = subscribers .into_iter() .map(|target| self.send_to_trustee(target, register_request.clone())); let results = join_all(futs).await; - Ok(results) + Ok((results, ledger_receipt)) } /// Persist subscriber registry to disk. diff --git a/rvps/src/extractors/extractor_modules/sample/mod.rs b/rvps/src/extractors/extractor_modules/sample/mod.rs index 23020ee8..70df3de2 100644 --- a/rvps/src/extractors/extractor_modules/sample/mod.rs +++ b/rvps/src/extractors/extractor_modules/sample/mod.rs @@ -62,6 +62,7 @@ impl Extractor for SampleExtractor { name: name.to_string(), expiration, hash_value: rvs, + audit_proof: None, }), None => { warn!("Expired time calculated overflowed for reference value of {name}."); diff --git a/rvps/src/extractors/extractor_modules/slsa/mod.rs b/rvps/src/extractors/extractor_modules/slsa/mod.rs index 43f7341a..37ee2318 100644 --- a/rvps/src/extractors/extractor_modules/slsa/mod.rs +++ b/rvps/src/extractors/extractor_modules/slsa/mod.rs @@ -8,7 +8,10 @@ use chrono::{Months, Timelike, Utc}; use serde::Deserialize; use tempfile::NamedTempFile; -use crate::{reference_value::REFERENCE_VALUE_VERSION, ReferenceValue}; +use crate::{ + reference_value::{AuditProof, REFERENCE_VALUE_VERSION}, + ReferenceValue, +}; use super::Extractor; @@ -18,6 +21,8 @@ struct RvdsPayload { slsa_provenance: Vec, #[allow(dead_code)] artifacts_download_url: Vec, + #[serde(default)] + audit_proof: Option, } #[derive(Debug, Deserialize)] @@ -236,7 +241,8 @@ impl Extractor for SlsaExtractor { let mut rv = ReferenceValue::new()? .set_version(REFERENCE_VALUE_VERSION) .set_name(&subject.name) - .set_expiration(expiration); + .set_expiration(expiration) + .set_audit_proof(envelope.audit_proof.clone()); for (alg, value) in subject.digest.iter() { rv = rv.add_hash_value(alg.to_string(), value.to_string()); diff --git a/rvps/src/reference_value.rs b/rvps/src/reference_value.rs index 1b19f6a3..2977171d 100644 --- a/rvps/src/reference_value.rs +++ b/rvps/src/reference_value.rs @@ -69,6 +69,24 @@ pub struct ReferenceValue { pub expiration: DateTime, #[serde(rename = "hash-value")] pub hash_value: Vec, + /// Optional audit proof pointing to immutable ledger evidence. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub audit_proof: Option, +} + +/// Minimal audit proof metadata kept alongside reference values. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct AuditProof { + pub backend: String, + pub handle: String, + pub event_hash: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub payload_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub payload_b64: Option, } /// Set the default version for ReferenceValue @@ -89,6 +107,7 @@ impl ReferenceValue { .with_nanosecond(0) .ok_or_else(|| anyhow!("set nanosecond failed."))?, hash_value: Vec::new(), + audit_proof: None, }) } @@ -124,6 +143,12 @@ impl ReferenceValue { self } + /// Set audit proof metadata. + pub fn set_audit_proof(mut self, proof: Option) -> Self { + self.audit_proof = proof; + self + } + /// Get hash value of the ReferenceValue. pub fn hash_values(&self) -> &Vec { &self.hash_value