Skip to content

Commit fd8fcce

Browse files
committed
Add log.v3.json endpoint and clean up handler, fixes #41
- Add an outer Router in the fetch handler to check the log parameter, instead of requiring each handler to check that the requested log path is in the allowlist - Change /metadata endpoint to /log.v3.json, dropping the witness_key field. The witness is just used to exercise the code path, and it's not expected that any clients would actually use the endpoint. - Disable bucket access via the Worker if monitoring_url is specified so clients must use the intended monitoring API endpoint. - Update MMD to 60 as that's the max allowed in Chrome's CT program for static CT logs. (Note that this is effectively zero in the current implementation.)
1 parent 5672029 commit fd8fcce

File tree

1 file changed

+100
-90
lines changed

1 file changed

+100
-90
lines changed

crates/ct_worker/src/frontend_worker.rs

Lines changed: 100 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use std::sync::LazyLock;
77

8-
use crate::{load_signing_key, load_witness_key, LookupKey, SequenceMetadata, CONFIG, ROOTS};
8+
use crate::{load_signing_key, LookupKey, SequenceMetadata, CONFIG, ROOTS};
99
use config::TemporalInterval;
1010
use futures_util::future::try_join_all;
1111
use generic_log_worker::{
@@ -24,17 +24,17 @@ use tlog_tiles::{LogEntry, PendingLogEntry};
2424
use worker::*;
2525

2626
// The Maximum Merge Delay (MMD) of a log indicates the maximum period of time
27-
// between when a SCT is issued and the corresponding entry is sequenced
28-
// in the log. For static CT logs, this is effectively zero since SCT issuance
29-
// happens only once the entry is sequenced. However, we can leave this value
30-
// in the metadata as the default (1 day).
31-
const MAX_MERGE_DELAY: usize = 86_400;
27+
// between when a SCT is issued and the corresponding entry is sequenced in the
28+
// log. For Azul-based logs, this is effectively zero since SCT issuance happens
29+
// only once the entry is sequenced. However, we can leave this value in the
30+
// metadata as the maximum allowed in Chrome's policy, 60 seconds.
31+
const MAX_MERGE_DELAY_SECS: usize = 60;
3232

3333
const UNKNOWN_LOG_MSG: &str = "unknown log";
3434

3535
#[serde_as]
3636
#[derive(Serialize)]
37-
struct MetadataResponse<'a> {
37+
struct LogV3JsonResponse<'a> {
3838
#[serde(skip_serializing_if = "Option::is_none")]
3939
description: &'a Option<String>,
4040
#[serde(skip_serializing_if = "Option::is_none")]
@@ -43,8 +43,6 @@ struct MetadataResponse<'a> {
4343
log_id: &'a [u8],
4444
#[serde_as(as = "Base64")]
4545
key: &'a [u8],
46-
#[serde_as(as = "Base64")]
47-
witness_key: &'a [u8],
4846
mmd: usize,
4947
submission_url: &'a str,
5048
monitoring_url: &'a str,
@@ -68,77 +66,101 @@ fn start() {
6866
/// Panics if there are issues parsing route parameters, which should never happen.
6967
#[event(fetch, respond_with_errors)]
7068
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
71-
let router = Router::new();
72-
router
73-
.get("/logs/:log/ct/v1/get-roots", |_req, ctx| {
74-
let _name = valid_log_name(&ctx)?;
75-
Response::from_json(&GetRootsResponse {
76-
certificates: x509_util::certs_to_bytes(&ROOTS.certs).unwrap(),
77-
})
78-
})
79-
.post_async("/logs/:log/ct/v1/add-chain", |req, ctx| async move {
80-
add_chain_or_pre_chain(req, &ctx.env, valid_log_name(&ctx)?, false).await
81-
})
82-
.post_async("/logs/:log/ct/v1/add-pre-chain", |req, ctx| async move {
83-
add_chain_or_pre_chain(req, &ctx.env, valid_log_name(&ctx)?, true).await
84-
})
85-
.get("/logs/:log/metadata", |_req, ctx| {
86-
let name = valid_log_name(&ctx)?;
87-
let params = &CONFIG.logs[name];
88-
let verifying_key = load_signing_key(&ctx.env, name)?.verifying_key();
89-
let log_id =
90-
&static_ct_api::log_id_from_key(verifying_key).map_err(|e| e.to_string())?;
91-
let key = verifying_key
92-
.to_public_key_der()
93-
.map_err(|e| e.to_string())?;
94-
let witness_key = load_witness_key(&ctx.env, name)?;
95-
let witness_key = witness_key
96-
.verifying_key()
97-
.to_public_key_der()
98-
.map_err(|e| e.to_string())?;
99-
Response::from_json(&MetadataResponse {
100-
description: &params.description,
101-
log_type: &params.log_type,
102-
log_id,
103-
key: key.as_bytes(),
104-
witness_key: witness_key.as_bytes(),
105-
submission_url: &params.submission_url,
106-
monitoring_url: if params.monitoring_url.is_empty() {
107-
&params.submission_url
69+
// Use an outer router as middleware to check that the log name is valid.
70+
Router::new()
71+
.or_else_any_method_async("/logs/:log/*route", |req, ctx| async move {
72+
let name = if let Some(name) = ctx.param("log") {
73+
if CONFIG.logs.contains_key(name) {
74+
&name.clone()
10875
} else {
109-
&params.monitoring_url
110-
},
111-
mmd: MAX_MERGE_DELAY,
112-
temporal_interval: &params.temporal_interval,
113-
})
114-
})
115-
.get_async("/logs/:log/metrics", |_req, ctx| async move {
116-
let name = valid_log_name(&ctx)?;
117-
let stub = get_durable_object_stub(
118-
&ctx.env,
119-
name,
120-
None,
121-
"SEQUENCER",
122-
CONFIG.logs[name].location_hint.as_deref(),
123-
)?;
124-
stub.fetch_with_str(&format!("http://fake_url.com{METRICS_ENDPOINT}"))
125-
.await
126-
})
127-
.get_async("/logs/:log/*key", |_req, ctx| async move {
128-
let name = valid_log_name(&ctx)?;
129-
let key = ctx.param("key").unwrap();
130-
131-
let bucket = load_public_bucket(&ctx.env, name)?;
132-
if let Some(obj) = bucket.get(key).execute().await? {
133-
Response::from_body(
134-
obj.body()
135-
.ok_or("R2 object missing body")?
136-
.response_body()?,
137-
)
138-
.map(|r| r.with_headers(headers_from_http_metadata(obj.http_metadata())))
76+
return Err(UNKNOWN_LOG_MSG.into());
77+
}
13978
} else {
140-
Response::error("Not found", 404)
141-
}
79+
return Err("missing 'log' route param".into());
80+
};
81+
82+
// Now that we've validated the log name, use an inner router to
83+
// handle the request.
84+
Router::with_data(name)
85+
.get("/logs/:log/ct/v1/get-roots", |_req, _ctx| {
86+
Response::from_json(&GetRootsResponse {
87+
certificates: x509_util::certs_to_bytes(&ROOTS.certs).unwrap(),
88+
})
89+
})
90+
.post_async("/logs/:log/ct/v1/add-chain", |req, ctx| async move {
91+
add_chain_or_pre_chain(req, &ctx.env, ctx.data, false).await
92+
})
93+
.post_async("/logs/:log/ct/v1/add-pre-chain", |req, ctx| async move {
94+
add_chain_or_pre_chain(req, &ctx.env, ctx.data, true).await
95+
})
96+
.get("/logs/:log/log.v3.json", |_req, ctx| {
97+
let name = ctx.data;
98+
let params = &CONFIG.logs[name];
99+
let verifying_key = load_signing_key(&ctx.env, name)?.verifying_key();
100+
let log_id = &static_ct_api::log_id_from_key(verifying_key)
101+
.map_err(|e| e.to_string())?;
102+
let key = verifying_key
103+
.to_public_key_der()
104+
.map_err(|e| e.to_string())?;
105+
Response::from_json(&LogV3JsonResponse {
106+
description: &params.description,
107+
log_type: &params.log_type,
108+
log_id,
109+
key: key.as_bytes(),
110+
submission_url: &params.submission_url,
111+
monitoring_url: if params.monitoring_url.is_empty() {
112+
&params.submission_url
113+
} else {
114+
&params.monitoring_url
115+
},
116+
mmd: MAX_MERGE_DELAY_SECS,
117+
temporal_interval: &params.temporal_interval,
118+
})
119+
})
120+
.get_async("/logs/:log/metrics", |_req, ctx| async move {
121+
let name = ctx.data;
122+
let stub = get_durable_object_stub(
123+
&ctx.env,
124+
name,
125+
None,
126+
"SEQUENCER",
127+
CONFIG.logs[name].location_hint.as_deref(),
128+
)?;
129+
stub.fetch_with_str(&format!("http://fake_url.com{METRICS_ENDPOINT}"))
130+
.await
131+
})
132+
.get_async("/logs/:log/*key", |_req, ctx| async move {
133+
let name = ctx.data;
134+
let key = ctx.param("key").unwrap();
135+
136+
// Enable direct access to the bucket via the Worker if
137+
// monitoring_url is unspecified.
138+
if CONFIG.logs[name].monitoring_url.is_empty() {
139+
let bucket = load_public_bucket(&ctx.env, name)?;
140+
if let Some(obj) = bucket.get(key).execute().await? {
141+
Response::from_body(
142+
obj.body()
143+
.ok_or("R2 object missing body")?
144+
.response_body()?,
145+
)
146+
.map(|r| {
147+
r.with_headers(headers_from_http_metadata(obj.http_metadata()))
148+
})
149+
} else {
150+
Response::error("Not found", 404)
151+
}
152+
} else {
153+
Response::error(
154+
format!(
155+
"Use {} for monitoring API",
156+
CONFIG.logs[name].monitoring_url
157+
),
158+
403,
159+
)
160+
}
161+
})
162+
.run(req, ctx.env)
163+
.await
142164
})
143165
.run(req, env)
144166
.await
@@ -269,18 +291,6 @@ async fn add_chain_or_pre_chain(
269291
Response::from_json(&sct)
270292
}
271293

272-
fn valid_log_name(ctx: &RouteContext<()>) -> Result<&str> {
273-
if let Some(name) = ctx.param("log") {
274-
if CONFIG.logs.contains_key(name) {
275-
Ok(name)
276-
} else {
277-
Err(UNKNOWN_LOG_MSG.into())
278-
}
279-
} else {
280-
Err("missing 'log' route param".into())
281-
}
282-
}
283-
284294
fn batcher_id_from_lookup_key(key: &LookupKey, num_batchers: u8) -> u8 {
285295
key[0] % num_batchers
286296
}

0 commit comments

Comments
 (0)