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,
|
||||
"**/.DS_Store": 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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
@ -189,9 +211,9 @@ dependencies = [
|
|||
"num-traits",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"x509-parser",
|
||||
]
|
||||
|
||||
|
@ -1236,6 +1258,30 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.12"
|
||||
|
|
10
Cargo.toml
10
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,8 @@ 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"] }
|
||||
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
|
||||
|
||||
#[cfg(feature = "api")]
|
||||
pub mod api;
|
||||
pub mod merkle;
|
||||
pub mod parsing;
|
||||
|
|
Loading…
Reference in a new issue