From 032a99f4fc7b2b583e8e52f45e88a8a036124a63 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sat, 26 Oct 2024 17:42:33 -0600 Subject: [PATCH 01/11] feat(api): Start on API requests with add-chain and boilerplate --- .vscode/settings.json | 4 +++- Cargo.lock | 1 - Cargo.toml | 9 ++++++--- src/api/endpoints.rs | 46 ++++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 47 +++++++++++++++++++++++++++++++++++++++++++ src/api/responses.rs | 21 +++++++++++++++++++ src/lib.rs | 2 ++ 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 src/api/endpoints.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/responses.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index f2ec98e..03f2751 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,7 @@ "**/CVS": true, "**/.DS_Store": true, "**/Thumbs.db": true - } + }, + "rust-analyzer.cargo.features": "all", + "rust-analyzer.check.features": "all" } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f750430..93e90a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,7 +189,6 @@ dependencies = [ "num-traits", "reqwest", "serde", - "serde_json", "sha2", "tokio", "x509-parser", diff --git a/Cargo.toml b/Cargo.toml index 99f848f..9a9fedf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,8 @@ anyhow = "1.0.90" der-parser = "9.0.0" nom = "7.1.3" num-traits = "0.2.19" -reqwest = { version = "0.12.8", features = ["json"] } -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.132" +reqwest = { version = "0.12.8", features = ["json"], optional = true } +serde = { version = "1.0.210", features = ["derive"], optional = true } sha2 = "0.10.8" x509-parser = "0.16.0" @@ -19,3 +18,7 @@ base64ct = "1.6.0" reqwest = { version = "0.12.8", features = ["json"] } serde = { version = "1.0.210", features = ["derive"] } tokio = { version = "1.41.0", features = ["rt-multi-thread", "macros"] } + +[features] +default-features = [] +api = ["dep:reqwest", "dep:serde"] diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs new file mode 100644 index 0000000..9eb55cc --- /dev/null +++ b/src/api/endpoints.rs @@ -0,0 +1,46 @@ +//! A module containing constants for each API endpoint, specifying the method +//! and path of each. This is primarily used for the [api module](`crate::api`) +//! internals, but could be used externally also. + +type Endpoint<'a> = (reqwest::Method, &'a str); + +/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.1 +/// ```txt +/// POST https:///ct/v1/add-chain +/// +/// Inputs: +/// +/// chain: An array of base64-encoded certificates. The first +/// element is the end-entity certificate; the second chains to the +/// first and so on to the last, which is either the root +/// certificate or a certificate that chains to a known root +/// certificate. +/// +/// Outputs: +/// +/// sct_version: The version of the SignedCertificateTimestamp +/// structure, in decimal. A compliant v1 implementation MUST NOT +/// expect this to be 0 (i.e., v1). +/// +/// id: The log ID, base64 encoded. Since log clients who request an +/// SCT for inclusion in TLS handshakes are not required to verify +/// it, we do not assume they know the ID of the log. +/// +/// timestamp: The SCT timestamp, in decimal. +/// +/// extensions: An opaque type for future expansion. It is likely +/// that not all participants will need to understand data in this +/// field. Logs should set this to the empty string. Clients +/// should decode the base64-encoded data and include it in the +/// SCT. +/// +/// signature: The SCT signature, base64 encoded. +/// +/// If the "sct_version" is not v1, then a v1 client may be unable to +/// verify the signature. It MUST NOT construe this as an error. (Note: +/// Log clients don't need to be able to verify this structure; only TLS +/// clients do. If we were to serve the structure as a binary blob, then +/// we could completely change it without requiring an upgrade to v1 +/// clients.) +/// ``` +pub const ADD_CHAIN_ENDPOINT: Endpoint = (reqwest::Method::POST, "/ct/v1/add-chain"); diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..b693120 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,47 @@ +use reqwest::Url; +use responses::{AddChainRequest, AddChainResponse}; + +pub mod endpoints; +pub mod responses; + +pub struct CtApiClient { + inner_client: reqwest::Client, + log_url: Url +} + +impl CtApiClient { + pub fn new(log_url: Url) -> reqwest::Result { + Ok(Self { + inner_client: reqwest::Client::builder().https_only(true).build()?, + log_url + }) + } + + /// Adds a chain to the CT log. + /// + /// See: [`endpoints::ADD_CHAIN_ENDPOINT`] + /// + /// ## Errors + /// + /// This may error if either the request failed (due to lack of internet or + /// invalid domain, for example), or if the CT log gave a 4xx/5xx response. + /// Specifically, compliant CT logs will reject chains that do not verify + /// sequentially from the first entry (end-user certificate) to the last + /// entry (a trusted root or an intermediate signed by the a trusted root). + pub async fn add_chain( + &self, + chain: Vec + ) -> reqwest::Result { + self.inner_client + .request( + endpoints::ADD_CHAIN_ENDPOINT.0, + endpoints::ADD_CHAIN_ENDPOINT.1 + ) + .json(&AddChainRequest { chain }) + .send() + .await? + .error_for_status()? + .json() + .await + } +} diff --git a/src/api/responses.rs b/src/api/responses.rs new file mode 100644 index 0000000..a885c16 --- /dev/null +++ b/src/api/responses.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +/// A request payload for adding a chain to a CT log +/// +/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] +#[derive(Debug, Serialize)] +pub struct AddChainRequest { + pub chain: Vec +} + +/// A response given when adding a chain to a CT log +/// +/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] +#[derive(Debug, Deserialize)] +pub struct AddChainResponse { + pub sct_version: u8, + pub log_id: String, + pub timestamp: u64, + pub extensions: String, + pub signature: String +} diff --git a/src/lib.rs b/src/lib.rs index 2723638..bc075fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,5 +10,7 @@ //! //! [RFC6926]: https://datatracker.ietf.org/doc/html/rfc6962 +#[cfg(feature = "api")] +pub mod api; pub mod merkle; pub mod parsing; -- 2.45.2 From ecbf630127469bc627c69aed8b43ae0716796985 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 00:21:35 -0600 Subject: [PATCH 02/11] fix(api): Fix URL logic --- src/api/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index b693120..08ce02e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -35,7 +35,7 @@ impl CtApiClient { self.inner_client .request( endpoints::ADD_CHAIN_ENDPOINT.0, - endpoints::ADD_CHAIN_ENDPOINT.1 + self.log_url.to_string() + endpoints::ADD_CHAIN_ENDPOINT.1 ) .json(&AddChainRequest { chain }) .send() -- 2.45.2 From 6f290c90e9c6dddf84388fa8d00ce6523c0be2a2 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 13:35:40 -0600 Subject: [PATCH 03/11] feat(api): Add add-pre-chain endpoint --- src/api/endpoints.rs | 16 ++++++++++++++++ src/api/mod.rs | 35 ++++++++++++++++++++++++++++++++++- src/api/responses.rs | 4 ++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 9eb55cc..1784c9b 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -44,3 +44,19 @@ type Endpoint<'a> = (reqwest::Method, &'a str); /// clients.) /// ``` pub const ADD_CHAIN_ENDPOINT: Endpoint = (reqwest::Method::POST, "/ct/v1/add-chain"); + +/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.2 +/// ```txt +/// POST https:///ct/v1/add-pre-chain +/// +/// Inputs: +/// +/// chain: An array of base64-encoded Precertificates. The first +/// element is the end-entity certificate; the second chains to the +/// first and so on to the last, which is either the root +/// certificate or a certificate that chains to a known root +/// certificate. +/// +/// Outputs are the same as in Section 4.1. +/// ``` +pub const ADD_PRE_CHAIN_ENDPOINT: Endpoint = (reqwest::Method::POST, "/ct/v1/add-pre-chain"); diff --git a/src/api/mod.rs b/src/api/mod.rs index 08ce02e..4b401b7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -17,7 +17,7 @@ impl CtApiClient { }) } - /// Adds a chain to the CT log. + /// Adds a standard x509 chain to the CT log. /// /// See: [`endpoints::ADD_CHAIN_ENDPOINT`] /// @@ -44,4 +44,37 @@ impl CtApiClient { .json() .await } + + /// Adds a precetificate chain to the CT log. This is largely the same as + /// [`CtApiClient::add_chain`], except is used specifically when the chain + /// starts with a precertificate rather than the final end-user certificate. + /// + /// See: [`endpoints::ADD_PRE_CHAIN_ENDPOINT`] + /// + /// ## Errors + /// + /// This may error if either the request failed (due to lack of internet or + /// invalid domain, for example), or if the CT log gave a 4xx/5xx response. + /// Specifically, compliant CT logs will reject chains that do not verify + /// properly. For precertificates this will happen is the first entry is not + /// a precertificate, or if the precertificate is not directly signed by + /// a. The CA certificate signing the real certificate + /// b. A special-purpose Precertificate Signing Certificate which is + /// directly signed by the CA certificate signing the real certificate. + pub async fn add_pre_chain( + &self, + chain: Vec + ) -> reqwest::Result { + self.inner_client + .request( + endpoints::ADD_PRE_CHAIN_ENDPOINT.0, + self.log_url.to_string() + endpoints::ADD_PRE_CHAIN_ENDPOINT.1 + ) + .json(&AddChainRequest { chain }) + .send() + .await? + .error_for_status()? + .json() + .await + } } diff --git a/src/api/responses.rs b/src/api/responses.rs index a885c16..4f02a3e 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; /// A request payload for adding a chain to a CT log /// -/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] +/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] or [`super::endpoints::ADD_PRE_CHAIN_ENDPOINT`] #[derive(Debug, Serialize)] pub struct AddChainRequest { pub chain: Vec @@ -10,7 +10,7 @@ pub struct AddChainRequest { /// A response given when adding a chain to a CT log /// -/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] +/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] or [`super::endpoints::ADD_PRE_CHAIN_ENDPOINT`] #[derive(Debug, Deserialize)] pub struct AddChainResponse { pub sct_version: u8, -- 2.45.2 From b718fe2792355c1d3bcff4c69d130785b625c340 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 14:25:08 -0600 Subject: [PATCH 04/11] chore: Rename endpoint consts and reformat --- src/api/endpoints.rs | 4 ++-- src/api/mod.rs | 12 ++++++------ src/api/responses.rs | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 1784c9b..2fddfe8 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -43,7 +43,7 @@ type Endpoint<'a> = (reqwest::Method, &'a str); /// we could completely change it without requiring an upgrade to v1 /// clients.) /// ``` -pub const ADD_CHAIN_ENDPOINT: Endpoint = (reqwest::Method::POST, "/ct/v1/add-chain"); +pub const ADD_CHAIN: Endpoint = (reqwest::Method::POST, "/ct/v1/add-chain"); /// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.2 /// ```txt @@ -59,4 +59,4 @@ pub const ADD_CHAIN_ENDPOINT: Endpoint = (reqwest::Method::POST, "/ct/v1/add-cha /// /// Outputs are the same as in Section 4.1. /// ``` -pub const ADD_PRE_CHAIN_ENDPOINT: Endpoint = (reqwest::Method::POST, "/ct/v1/add-pre-chain"); +pub const ADD_PRE_CHAIN: Endpoint = (reqwest::Method::POST, "/ct/v1/add-pre-chain"); diff --git a/src/api/mod.rs b/src/api/mod.rs index 4b401b7..5f97688 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -19,7 +19,7 @@ impl CtApiClient { /// Adds a standard x509 chain to the CT log. /// - /// See: [`endpoints::ADD_CHAIN_ENDPOINT`] + /// See: [`endpoints::ADD_CHAIN`] /// /// ## Errors /// @@ -34,8 +34,8 @@ impl CtApiClient { ) -> reqwest::Result { self.inner_client .request( - endpoints::ADD_CHAIN_ENDPOINT.0, - self.log_url.to_string() + endpoints::ADD_CHAIN_ENDPOINT.1 + endpoints::ADD_CHAIN.0, + self.log_url.to_string() + endpoints::ADD_CHAIN.1 ) .json(&AddChainRequest { chain }) .send() @@ -49,7 +49,7 @@ impl CtApiClient { /// [`CtApiClient::add_chain`], except is used specifically when the chain /// starts with a precertificate rather than the final end-user certificate. /// - /// See: [`endpoints::ADD_PRE_CHAIN_ENDPOINT`] + /// See: [`endpoints::ADD_PRE_CHAIN`] /// /// ## Errors /// @@ -67,8 +67,8 @@ impl CtApiClient { ) -> reqwest::Result { self.inner_client .request( - endpoints::ADD_PRE_CHAIN_ENDPOINT.0, - self.log_url.to_string() + endpoints::ADD_PRE_CHAIN_ENDPOINT.1 + endpoints::ADD_PRE_CHAIN.0, + self.log_url.to_string() + endpoints::ADD_PRE_CHAIN.1 ) .json(&AddChainRequest { chain }) .send() diff --git a/src/api/responses.rs b/src/api/responses.rs index 4f02a3e..b3a2c83 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -1,16 +1,18 @@ use serde::{Deserialize, Serialize}; /// A request payload for adding a chain to a CT log -/// -/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] or [`super::endpoints::ADD_PRE_CHAIN_ENDPOINT`] +/// +/// See: [`super::endpoints::ADD_CHAIN`] or +/// [`super::endpoints::ADD_PRE_CHAIN`] #[derive(Debug, Serialize)] pub struct AddChainRequest { - pub chain: Vec + pub chain: Vec } /// A response given when adding a chain to a CT log /// -/// See: [`super::endpoints::ADD_CHAIN_ENDPOINT`] or [`super::endpoints::ADD_PRE_CHAIN_ENDPOINT`] +/// See: [`super::endpoints::ADD_CHAIN`] or +/// [`super::endpoints::ADD_PRE_CHAIN`] #[derive(Debug, Deserialize)] pub struct AddChainResponse { pub sct_version: u8, -- 2.45.2 From d4762205dc4f93e18b9377f29b7180579325cdc1 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 14:25:08 -0600 Subject: [PATCH 05/11] feat(api): Add Get Signed Tree Head endpoint --- src/api/endpoints.rs | 18 ++++++++++++++++++ src/api/mod.rs | 23 ++++++++++++++++++++++- src/api/responses.rs | 11 +++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 2fddfe8..528d01d 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -60,3 +60,21 @@ pub const ADD_CHAIN: Endpoint = (reqwest::Method::POST, "/ct/v1/add-chain"); /// Outputs are the same as in Section 4.1. /// ``` pub const ADD_PRE_CHAIN: Endpoint = (reqwest::Method::POST, "/ct/v1/add-pre-chain"); + +/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.3 +/// ```txt +/// GET https:///ct/v1/get-sth +/// +/// No inputs. +/// +/// Outputs: +/// +/// tree_size: The size of the tree, in entries, in decimal. +/// +/// timestamp: The timestamp, in decimal. +/// +/// sha256_root_hash: The Merkle Tree Hash of the tree, in base64. +/// +/// tree_head_signature: A TreeHeadSignature for the above data. +/// ``` +pub const GET_STH: Endpoint = (reqwest::Method::GET, "/ct/v1/get-sth"); \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 5f97688..d60c367 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ use reqwest::Url; -use responses::{AddChainRequest, AddChainResponse}; +use responses::{AddChainRequest, AddChainResponse, GetSthResponse}; pub mod endpoints; pub mod responses; @@ -77,4 +77,25 @@ impl CtApiClient { .json() .await } + + /// /// Fetches the Signed Tree Head information for the current CT log tree + /// + /// See: [`endpoints::GET_STH`] + /// + /// ## Errors + /// + /// This may error if either the request failed (due to lack of internet or + /// invalid domain, for example). + pub async fn get_signed_tree_head(&self) -> reqwest::Result { + self.inner_client + .request( + endpoints::GET_STH.0, + self.log_url.to_string() + endpoints::GET_STH.1 + ) + .send() + .await? + .error_for_status()? + .json() + .await + } } diff --git a/src/api/responses.rs b/src/api/responses.rs index b3a2c83..2a831ec 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -21,3 +21,14 @@ pub struct AddChainResponse { pub extensions: String, pub signature: String } + +/// A response given when fetching the Signed Tree Head of a CT log +/// +/// See: [`super::endpoints::GET_STH`] +#[derive(Debug, Deserialize)] +pub struct GetSthResponse { + pub tree_size: u64, + pub timestamp: u64, + pub sha256_root_hash: String, + pub tree_head_signature: String +} -- 2.45.2 From 2358a22d38fa848b3c27f9bc25d32563be8dd648 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 14:48:08 -0600 Subject: [PATCH 06/11] chore: Reformat --- src/api/endpoints.rs | 2 +- src/api/mod.rs | 2 +- src/api/responses.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 528d01d..72eb23c 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -77,4 +77,4 @@ pub const ADD_PRE_CHAIN: Endpoint = (reqwest::Method::POST, "/ct/v1/add-pre-chai /// /// tree_head_signature: A TreeHeadSignature for the above data. /// ``` -pub const GET_STH: Endpoint = (reqwest::Method::GET, "/ct/v1/get-sth"); \ No newline at end of file +pub const GET_STH: Endpoint = (reqwest::Method::GET, "/ct/v1/get-sth"); diff --git a/src/api/mod.rs b/src/api/mod.rs index d60c367..e34d737 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -78,7 +78,7 @@ impl CtApiClient { .await } - /// /// Fetches the Signed Tree Head information for the current CT log tree + /// Fetches the Signed Tree Head information for the current CT log tree /// /// See: [`endpoints::GET_STH`] /// diff --git a/src/api/responses.rs b/src/api/responses.rs index 2a831ec..a36b683 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -23,7 +23,7 @@ pub struct AddChainResponse { } /// A response given when fetching the Signed Tree Head of a CT log -/// +/// /// See: [`super::endpoints::GET_STH`] #[derive(Debug, Deserialize)] pub struct GetSthResponse { -- 2.45.2 From 6d1ae370fa02f39ab9b9d601fd7a6bb45e6a9c02 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 14:55:34 -0600 Subject: [PATCH 07/11] feat(api): Get Signed Tree Head consistency proof --- src/api/endpoints.rs | 22 ++++++++++++++++++++++ src/api/mod.rs | 37 ++++++++++++++++++++++++++++++++++++- src/api/responses.rs | 9 +++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 72eb23c..f8cadf5 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -78,3 +78,25 @@ pub const ADD_PRE_CHAIN: Endpoint = (reqwest::Method::POST, "/ct/v1/add-pre-chai /// tree_head_signature: A TreeHeadSignature for the above data. /// ``` pub const GET_STH: Endpoint = (reqwest::Method::GET, "/ct/v1/get-sth"); + +/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.4 +/// ```txt +/// GET https:///ct/v1/get-sth-consistency +/// +/// Inputs: +/// +/// first: The tree_size of the first tree, in decimal. +/// +/// second: The tree_size of the second tree, in decimal. +/// +/// Both tree sizes must be from existing v1 STHs (Signed Tree Heads). +/// +/// Outputs: +/// +/// consistency: An array of Merkle Tree nodes, base64 encoded. +/// +/// Note that no signature is required on this data, as it is used to +/// verify an STH, which is signed. +/// ``` +pub const GET_STH_CONSISTENCY: Endpoint = + (reqwest::Method::GET, "/ct/v1/get-sth-consistency"); diff --git a/src/api/mod.rs b/src/api/mod.rs index e34d737..2ea8e44 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,10 @@ use reqwest::Url; -use responses::{AddChainRequest, AddChainResponse, GetSthResponse}; +use responses::{ + AddChainRequest, + AddChainResponse, + GetSthConsistencyResponse, + GetSthResponse +}; pub mod endpoints; pub mod responses; @@ -98,4 +103,34 @@ impl CtApiClient { .json() .await } + + /// Fetches the Signed Tree Head consistency proof for a specified start + /// tree_size and end tree_size. + /// + /// See: [`endpoints::GET_STH_CONSISTENCY`] + /// + /// ## Errors + /// + /// This may error if either the request failed (due to lack of internet or + /// invalid domain, for example), or if the CT log gave a 4xx/5xx response. + /// The CT log may error the response if your first and second tree sizes + /// are invalid, for example if the second is smaller than the first or if + /// the tree has never contained a size specified in `first` or `second`. + pub async fn get_signed_tree_head_consistency( + &self, + first: u64, + second: u64 + ) -> reqwest::Result { + self.inner_client + .request( + endpoints::GET_STH_CONSISTENCY.0, + self.log_url.to_string() + endpoints::GET_STH_CONSISTENCY.1 + ) + .query(&[("first", first), ("second", second)]) + .send() + .await? + .error_for_status()? + .json() + .await + } } diff --git a/src/api/responses.rs b/src/api/responses.rs index a36b683..de78f28 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -32,3 +32,12 @@ pub struct GetSthResponse { pub sha256_root_hash: String, pub tree_head_signature: String } + +/// A response given when fetching the Signed Tree Head consistency proof of a +/// CT log +/// +/// See: [`super::endpoints::GET_STH_CONSISTENCY`] +#[derive(Debug, Deserialize)] +pub struct GetSthConsistencyResponse { + pub consistency: Vec +} -- 2.45.2 From 505355189fad1a7ce5081f7a161a75ecdb2356c4 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 15:51:12 -0600 Subject: [PATCH 08/11] feat(api): Add Merkle Audit Proof by leaf hash endpoint --- src/api/endpoints.rs | 25 +++++++++++++++++++++++++ src/api/mod.rs | 33 +++++++++++++++++++++++++++++++++ src/api/responses.rs | 10 ++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index f8cadf5..31895c2 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -100,3 +100,28 @@ pub const GET_STH: Endpoint = (reqwest::Method::GET, "/ct/v1/get-sth"); /// ``` pub const GET_STH_CONSISTENCY: Endpoint = (reqwest::Method::GET, "/ct/v1/get-sth-consistency"); + +/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.4 +/// ```txt +/// GET https:///ct/v1/get-proof-by-hash +/// +/// Inputs: +/// +/// hash: A base64-encoded v1 leaf hash. +/// +/// tree_size: The tree_size of the tree on which to base the proof, +/// in decimal. +/// +/// The "hash" must be calculated as defined in Section 3.4. The +/// "tree_size" must designate an existing v1 STH. +/// +/// Outputs: +/// +/// leaf_index: The 0-based index of the end entity corresponding to +/// the "hash" parameter. +/// +/// audit_path: An array of base64-encoded Merkle Tree nodes proving +/// the inclusion of the chosen certificate. +/// ``` +pub const GET_PROOF_BY_HASH: Endpoint = + (reqwest::Method::GET, "/ct/v1/get-proof-by-hash"); diff --git a/src/api/mod.rs b/src/api/mod.rs index 2ea8e44..8f158a3 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,6 +2,7 @@ use reqwest::Url; use responses::{ AddChainRequest, AddChainResponse, + GetProofByHashResponse, GetSthConsistencyResponse, GetSthResponse }; @@ -133,4 +134,36 @@ impl CtApiClient { .json() .await } + + /// Fetches a single merkle audit proof for a specific leaf node by hash + /// from the CT log. The response both includes the index of the hashed + /// leaf node and the list of Merkle Tree nodes required to verify proof of + /// existence. + /// + /// See: [`endpoints::GET_PROOF_BY_HASH`] + /// + /// ## Errors + /// + /// This may error if either the request failed (due to lack of internet or + /// invalid domain, for example), or if the CT log gave a 4xx/5xx response. + /// The CT log may error the response if your provided hash was not a hash + /// of a valid node at the tree size specified. + pub async fn get_merkle_audit_proof_by_hash( + &self, + hash: &str, + tree_size: u64 + ) -> reqwest::Result { + self.inner_client + .request( + endpoints::GET_PROOF_BY_HASH.0, + self.log_url.to_string() + endpoints::GET_PROOF_BY_HASH.1 + ) + .query(&[("hash", hash)]) + .query(&[("tree_size", tree_size)]) + .send() + .await? + .error_for_status()? + .json() + .await + } } diff --git a/src/api/responses.rs b/src/api/responses.rs index de78f28..f8d7429 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -41,3 +41,13 @@ pub struct GetSthResponse { pub struct GetSthConsistencyResponse { pub consistency: Vec } + +/// A response given when fetching the Merkle Audit Proof from a CT log merkle +/// leaf. +/// +/// See: [`super::endpoints::GET_PROOF_BY_HASH`] +#[derive(Debug, Deserialize)] +pub struct GetProofByHashResponse { + pub leaf_index: u64, + pub audit_path: Vec +} -- 2.45.2 From f526f859a5b15cc19c4bde882c620fa06f21f8ea Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 16:03:40 -0600 Subject: [PATCH 09/11] docs(api): List outputs in CtApiClient methods --- src/api/mod.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 8f158a3..d153df8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -23,7 +23,9 @@ impl CtApiClient { }) } - /// Adds a standard x509 chain to the CT log. + /// Adds a standard x509 chain to the CT log. The log will then return + /// information needed to construct a valid SCT entry, including timestamp, + /// CT log signature, sct version, and any log operator extensions. /// /// See: [`endpoints::ADD_CHAIN`] /// @@ -54,6 +56,7 @@ impl CtApiClient { /// Adds a precetificate chain to the CT log. This is largely the same as /// [`CtApiClient::add_chain`], except is used specifically when the chain /// starts with a precertificate rather than the final end-user certificate. + /// Response data is exactly the same as [`CtApiClient::add_chain`]. /// /// See: [`endpoints::ADD_PRE_CHAIN`] /// @@ -84,7 +87,11 @@ impl CtApiClient { .await } - /// Fetches the Signed Tree Head information for the current CT log tree + /// Fetches the Signed Tree Head information for the current CT log tree. + /// The response contains the tree size, timestamp, root hash, and a + /// signature of all of the above from the CT log. This data can be used to + /// verify the integrity of the tree at any point, and verify inclusion of a + /// leaf in the tree. /// /// See: [`endpoints::GET_STH`] /// @@ -106,7 +113,9 @@ impl CtApiClient { } /// Fetches the Signed Tree Head consistency proof for a specified start - /// tree_size and end tree_size. + /// tree_size and end tree_size. This consistency proof is simply a list of + /// each merkle tree node necessary to verify the append-only nature of the + /// log between the specified first and second tree sizes. /// /// See: [`endpoints::GET_STH_CONSISTENCY`] /// @@ -138,7 +147,7 @@ impl CtApiClient { /// Fetches a single merkle audit proof for a specific leaf node by hash /// from the CT log. The response both includes the index of the hashed /// leaf node and the list of Merkle Tree nodes required to verify proof of - /// existence. + /// existence of your specified node in the full tree. /// /// See: [`endpoints::GET_PROOF_BY_HASH`] /// -- 2.45.2 From 1c2b59e9f8b4df20bd88550c19271c56eb3ced92 Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 16:11:47 -0600 Subject: [PATCH 10/11] feat(api): Allow re-use of existing reqwest client in CtApiClient struct --- src/api/mod.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index d153df8..69204c3 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use reqwest::Url; use responses::{ AddChainRequest, @@ -11,18 +13,25 @@ pub mod endpoints; pub mod responses; pub struct CtApiClient { - inner_client: reqwest::Client, + inner_client: Arc, log_url: Url } impl CtApiClient { pub fn new(log_url: Url) -> reqwest::Result { Ok(Self { - inner_client: reqwest::Client::builder().https_only(true).build()?, + inner_client: Arc::new(reqwest::Client::builder().https_only(true).build()?), log_url }) } + pub fn new_with_client(log_url: Url, inner_client: Arc) -> Self { + Self { + inner_client, + log_url + } + } + /// Adds a standard x509 chain to the CT log. The log will then return /// information needed to construct a valid SCT entry, including timestamp, /// CT log signature, sct version, and any log operator extensions. -- 2.45.2 From 7e51c915bc6b47b1c75901d1bca0a167efc347bd Mon Sep 17 00:00:00 2001 From: Tyler Beckman Date: Sun, 27 Oct 2024 16:10:19 -0600 Subject: [PATCH 11/11] docs(api): Fill in missing doc comments --- src/api/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index 69204c3..67c0968 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -12,12 +12,28 @@ use responses::{ pub mod endpoints; pub mod responses; +/// An API client used for interfacing with a specific CT log. This can be +/// constructed with [`CtApiClient::new`] to automatically create an inner +/// [`reqwest::Client`], or an [`Arc`] of one can be passed manually with +/// [`CtApiClient::new_with_client`] to re-use an existing client between code. +/// Re-using clients if one is already created is recommended, as it allows +/// connection-pools to be re-used and will have less overhead between requests. pub struct CtApiClient { inner_client: Arc, log_url: Url } impl CtApiClient { + /// Creates a new [`CtApiClient`] given a specific log URL. This log URL can + /// contain a subpath if the specific log uses one. Anything besides a + /// scheme, host information (ip/port), and a standard path is not supported + /// and will likely cause requests to fail. + /// + /// ## Errors + /// + /// As this automatically constructs a [`reqwest::Client`], this will error + /// if the client fails to be created for whatever reason, usually due to it + /// being unable to find TLS configuration and root store for the platform. pub fn new(log_url: Url) -> reqwest::Result { Ok(Self { inner_client: Arc::new(reqwest::Client::builder().https_only(true).build()?), @@ -25,6 +41,11 @@ impl CtApiClient { }) } + /// Creates a new [`CtApiClient`] given a specific log URL and + /// [`reqwest::Client`]. This log URL can contain a subpath if the specific + /// log uses one. Anything besides a scheme, host information (ip/port), + /// and a standard path is not supported and will likely cause requests to + /// fail. pub fn new_with_client(log_url: Url, inner_client: Arc) -> Self { Self { inner_client, -- 2.45.2