Compare commits
13 commits
84aa99f261
...
03f4151f66
Author | SHA1 | Date | |
---|---|---|---|
03f4151f66 | |||
70a83d4b9e | |||
7e51c915bc | |||
1c2b59e9f8 | |||
f526f859a5 | |||
505355189f | |||
6d1ae370fa | |||
2358a22d38 | |||
d4762205dc | |||
b718fe2792 | |||
6f290c90e9 | |||
ecbf630127 | |||
032a99f4fc |
7 changed files with 585 additions and 5 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -18,5 +18,7 @@
|
||||||
"**/CVS": true,
|
"**/CVS": true,
|
||||||
"**/.DS_Store": true,
|
"**/.DS_Store": true,
|
||||||
"**/Thumbs.db": true
|
"**/Thumbs.db": true
|
||||||
}
|
},
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
|
"rust-analyzer.check.features": "all"
|
||||||
}
|
}
|
48
Cargo.lock
generated
48
Cargo.lock
generated
|
@ -62,6 +62,28 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -189,9 +211,9 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"sha2",
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
"x509-parser",
|
"x509-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1236,6 +1258,30 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.12"
|
version = "0.7.12"
|
||||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -8,9 +8,8 @@ anyhow = "1.0.90"
|
||||||
der-parser = "9.0.0"
|
der-parser = "9.0.0"
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
reqwest = { version = "0.12.8", features = ["json"] }
|
reqwest = { version = "0.12.8", features = ["json"], optional = true }
|
||||||
serde = { version = "1.0.210", features = ["derive"] }
|
serde = { version = "1.0.210", features = ["derive"], optional = true }
|
||||||
serde_json = "1.0.132"
|
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
x509-parser = "0.16.0"
|
x509-parser = "0.16.0"
|
||||||
|
|
||||||
|
@ -19,3 +18,8 @@ base64ct = "1.6.0"
|
||||||
reqwest = { version = "0.12.8", features = ["json"] }
|
reqwest = { version = "0.12.8", features = ["json"] }
|
||||||
serde = { version = "1.0.210", features = ["derive"] }
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
tokio = { version = "1.41.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.41.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
tokio-test = "0.4.4"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default-features = []
|
||||||
|
api = ["dep:reqwest", "dep:serde"]
|
||||||
|
|
173
src/api/endpoints.rs
Normal file
173
src/api/endpoints.rs
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
//! 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 = (reqwest::Method::POST, "/ct/v1/add-chain");
|
||||||
|
|
||||||
|
/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.2
|
||||||
|
/// ```txt
|
||||||
|
/// POST https://<log server>/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 = (reqwest::Method::POST, "/ct/v1/add-pre-chain");
|
||||||
|
|
||||||
|
/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.3
|
||||||
|
/// ```txt
|
||||||
|
/// GET https://<log server>/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");
|
||||||
|
|
||||||
|
/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.4
|
||||||
|
/// ```txt
|
||||||
|
/// GET https://<log server>/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");
|
||||||
|
|
||||||
|
/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.4
|
||||||
|
/// ```txt
|
||||||
|
/// GET https://<log server>/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");
|
||||||
|
|
||||||
|
/// Reference: https://datatracker.ietf.org/doc/html/rfc6962#section-4.4
|
||||||
|
/// ```txt
|
||||||
|
/// GET https://<log server>/ct/v1/get-entries
|
||||||
|
///
|
||||||
|
/// Inputs:
|
||||||
|
///
|
||||||
|
/// start: 0-based index of first entry to retrieve, in decimal.
|
||||||
|
///
|
||||||
|
/// end: 0-based index of last entry to retrieve, in decimal.
|
||||||
|
///
|
||||||
|
/// Outputs:
|
||||||
|
///
|
||||||
|
/// entries: An array of objects, each consisting of
|
||||||
|
///
|
||||||
|
/// leaf_input: The base64-encoded MerkleTreeLeaf structure.
|
||||||
|
///
|
||||||
|
/// extra_data: The base64-encoded unsigned data pertaining to the
|
||||||
|
/// log entry. In the case of an X509ChainEntry, this is the
|
||||||
|
/// "certificate_chain". In the case of a PrecertChainEntry,
|
||||||
|
/// this is the whole "PrecertChainEntry".
|
||||||
|
///
|
||||||
|
/// Note that this message is not signed -- the retrieved data can be
|
||||||
|
/// verified by constructing the Merkle Tree Hash corresponding to a
|
||||||
|
/// retrieved STH. All leaves MUST be v1. However, a compliant v1
|
||||||
|
/// client MUST NOT construe an unrecognized MerkleLeafType or
|
||||||
|
/// LogEntryType value as an error. This means it may be unable to parse
|
||||||
|
/// some entries, but note that each client can inspect the entries it
|
||||||
|
/// does recognize as well as verify the integrity of the data by
|
||||||
|
/// treating unrecognized leaves as opaque input to the tree.
|
||||||
|
///
|
||||||
|
/// The "start" and "end" parameters SHOULD be within the range 0 <= x <
|
||||||
|
/// "tree_size" as returned by "get-sth" in Section 4.3.
|
||||||
|
///
|
||||||
|
/// Logs MAY honor requests where 0 <= "start" < "tree_size" and "end" >=
|
||||||
|
/// "tree_size" by returning a partial response covering only the valid
|
||||||
|
/// entries in the specified range. Note that the following restriction
|
||||||
|
/// may also apply:
|
||||||
|
///
|
||||||
|
/// Logs MAY restrict the number of entries that can be retrieved per
|
||||||
|
/// "get-entries" request. If a client requests more than the permitted
|
||||||
|
/// number of entries, the log SHALL return the maximum number of entries
|
||||||
|
/// permissible. These entries SHALL be sequential beginning with the
|
||||||
|
/// entry specified by "start".
|
||||||
|
/// ```
|
||||||
|
pub const GET_ENTRIES: Endpoint = (reqwest::Method::GET, "/ct/v1/get-entries");
|
285
src/api/mod.rs
Normal file
285
src/api/mod.rs
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use responses::{
|
||||||
|
AddChainRequest,
|
||||||
|
AddChainResponse,
|
||||||
|
GetEntriesResponse,
|
||||||
|
GetProofByHashResponse,
|
||||||
|
GetSthConsistencyResponse,
|
||||||
|
GetSthResponse
|
||||||
|
};
|
||||||
|
|
||||||
|
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<reqwest::Client>,
|
||||||
|
log_url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```
|
||||||
|
/// use ct::api::CtApiClient;
|
||||||
|
///
|
||||||
|
/// let client = CtApiClient::new("https://oak.ct.letsencrypt.org/2025h2")
|
||||||
|
/// .expect("Should construct properly");
|
||||||
|
///
|
||||||
|
/// // Use constructed client here
|
||||||
|
/// ```
|
||||||
|
pub fn new(log_url: &str) -> reqwest::Result<Self> {
|
||||||
|
Ok(Self::new_with_client(
|
||||||
|
log_url,
|
||||||
|
Arc::new(reqwest::Client::builder().https_only(true).build()?)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```
|
||||||
|
/// use std::sync::Arc;
|
||||||
|
///
|
||||||
|
/// use ct::api::CtApiClient;
|
||||||
|
///
|
||||||
|
/// let existing_client = reqwest::Client::new();
|
||||||
|
/// let client = CtApiClient::new_with_client(
|
||||||
|
/// "https://oak.ct.letsencrypt.org/2025h2",
|
||||||
|
/// Arc::new(existing_client)
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// // Use constructed client here
|
||||||
|
/// ```
|
||||||
|
pub fn new_with_client(log_url: &str, inner_client: Arc<reqwest::Client>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner_client,
|
||||||
|
log_url: log_url.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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`]
|
||||||
|
///
|
||||||
|
/// ## 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.0,
|
||||||
|
self.log_url.to_string() + endpoints::ADD_CHAIN.1
|
||||||
|
)
|
||||||
|
.json(&AddChainRequest { chain })
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.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.
|
||||||
|
/// Response data is exactly the same as [`CtApiClient::add_chain`].
|
||||||
|
///
|
||||||
|
/// See: [`endpoints::ADD_PRE_CHAIN`]
|
||||||
|
///
|
||||||
|
/// ## 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<String>
|
||||||
|
) -> reqwest::Result<AddChainResponse> {
|
||||||
|
self.inner_client
|
||||||
|
.request(
|
||||||
|
endpoints::ADD_PRE_CHAIN.0,
|
||||||
|
self.log_url.to_string() + endpoints::ADD_PRE_CHAIN.1
|
||||||
|
)
|
||||||
|
.json(&AddChainRequest { chain })
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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`]
|
||||||
|
///
|
||||||
|
/// ## 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<GetSthResponse> {
|
||||||
|
self.inner_client
|
||||||
|
.request(
|
||||||
|
endpoints::GET_STH.0,
|
||||||
|
self.log_url.to_string() + endpoints::GET_STH.1
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the Signed Tree Head consistency proof for a specified start
|
||||||
|
/// 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`]
|
||||||
|
///
|
||||||
|
/// ## 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<GetSthConsistencyResponse> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 of your specified node in the full tree.
|
||||||
|
///
|
||||||
|
/// 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<GetProofByHashResponse> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the CT log entries within the range `[start, end]` where both
|
||||||
|
/// parameters are 0-indexed. The response contains both the MerkleTreeLeaf
|
||||||
|
/// structure and full certificate chain of each entry. Logs may or may not
|
||||||
|
/// properly honor requests in which the start parameter is less than 0 or
|
||||||
|
/// the end parameter is greater than the current tree size, to try to
|
||||||
|
/// ensure these bounds are correct. In addition, there is no guarantee that
|
||||||
|
/// the resulting response will have the list of entries exactly the size
|
||||||
|
/// requested, as CT logs can enforce limits on how much data is returned at
|
||||||
|
/// once.
|
||||||
|
///
|
||||||
|
/// See: [`endpoints::GET_ENTRIES`]
|
||||||
|
///
|
||||||
|
/// ## 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 it doesn't allow invalid bounds and
|
||||||
|
/// the start and end parameters were specified incorrectly.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```
|
||||||
|
/// use ct::api::CtApiClient;
|
||||||
|
///
|
||||||
|
/// let client = CtApiClient::new("https://oak.ct.letsencrypt.org/2025h2")
|
||||||
|
/// .expect("Should construct properly");
|
||||||
|
///
|
||||||
|
/// tokio_test::block_on(async move {
|
||||||
|
/// let result = client
|
||||||
|
/// .get_log_entries(0, 9)
|
||||||
|
/// .await
|
||||||
|
/// .expect("Request should succeed");
|
||||||
|
/// assert_eq!(result.entries.len(), 10, "Log should return 10 entries");
|
||||||
|
/// })
|
||||||
|
/// ```
|
||||||
|
pub async fn get_log_entries(
|
||||||
|
&self,
|
||||||
|
start: u64,
|
||||||
|
end: u64
|
||||||
|
) -> reqwest::Result<GetEntriesResponse> {
|
||||||
|
self.inner_client
|
||||||
|
.request(
|
||||||
|
endpoints::GET_ENTRIES.0,
|
||||||
|
self.log_url.to_string() + endpoints::GET_ENTRIES.1
|
||||||
|
)
|
||||||
|
.query(&[("start", start), ("end", end)])
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
68
src/api/responses.rs
Normal file
68
src/api/responses.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A request payload for adding a chain to a CT log
|
||||||
|
///
|
||||||
|
/// See: [`super::endpoints::ADD_CHAIN`] or
|
||||||
|
/// [`super::endpoints::ADD_PRE_CHAIN`]
|
||||||
|
#[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`] or
|
||||||
|
/// [`super::endpoints::ADD_PRE_CHAIN`]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct AddChainResponse {
|
||||||
|
pub sct_version: u8,
|
||||||
|
pub log_id: String,
|
||||||
|
pub timestamp: u64,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A response given when fetching CT log entries within a range
|
||||||
|
///
|
||||||
|
/// See: [`super::endpoints::GET_ENTRIES`]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GetEntriesResponse {
|
||||||
|
pub entries: Vec<GetEntriesResponseEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A specific entry in a [`GetEntriesResponse`]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GetEntriesResponseEntry {
|
||||||
|
pub leaf_input: String,
|
||||||
|
pub extra_data: String
|
||||||
|
}
|
|
@ -10,5 +10,7 @@
|
||||||
//!
|
//!
|
||||||
//! [RFC6926]: https://datatracker.ietf.org/doc/html/rfc6962
|
//! [RFC6926]: https://datatracker.ietf.org/doc/html/rfc6962
|
||||||
|
|
||||||
|
#[cfg(feature = "api")]
|
||||||
|
pub mod api;
|
||||||
pub mod merkle;
|
pub mod merkle;
|
||||||
pub mod parsing;
|
pub mod parsing;
|
||||||
|
|
Loading…
Reference in a new issue