feat(api): Start on API requests with add-chain and boilerplate

This commit is contained in:
Tyler Beckman 2024-10-26 17:42:33 -06:00
parent 84aa99f261
commit 032a99f4fc
Signed by: Ty
GPG key ID: 2813440C772555A4
7 changed files with 125 additions and 5 deletions

View file

@ -18,5 +18,7 @@
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
}
},
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.features": "all"
}

1
Cargo.lock generated
View file

@ -189,7 +189,6 @@ dependencies = [
"num-traits",
"reqwest",
"serde",
"serde_json",
"sha2",
"tokio",
"x509-parser",

View file

@ -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"]

46
src/api/endpoints.rs Normal file
View file

@ -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://<log server>/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");

47
src/api/mod.rs Normal file
View file

@ -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<Self> {
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<String>
) -> reqwest::Result<AddChainResponse> {
self.inner_client
.request(
endpoints::ADD_CHAIN_ENDPOINT.0,
endpoints::ADD_CHAIN_ENDPOINT.1
)
.json(&AddChainRequest { chain })
.send()
.await?
.error_for_status()?
.json()
.await
}
}

21
src/api/responses.rs Normal file
View file

@ -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<String>
}
/// 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
}

View file

@ -10,5 +10,7 @@
//!
//! [RFC6926]: https://datatracker.ietf.org/doc/html/rfc6962
#[cfg(feature = "api")]
pub mod api;
pub mod merkle;
pub mod parsing;