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;