Skip to content

Commit ced760a

Browse files
mtc_worker: extract add_entry helpers with tests
1 parent dfabc17 commit ced760a

File tree

1 file changed

+114
-43
lines changed

1 file changed

+114
-43
lines changed

crates/mtc_worker/src/frontend_worker.rs

Lines changed: 114 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -277,35 +277,71 @@ async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
277277
response
278278
}
279279

280+
/// Builds the issuer RDN with the trust anchor ID.
281+
fn build_issuer_rdn(log_id: &str) -> std::result::Result<RdnSequence, String> {
282+
let utf8_value = Utf8StringRef::new(log_id).map_err(|e| e.to_string())?;
283+
let any_value = Any::new(Tag::Utf8String, utf8_value.as_bytes()).map_err(|e| e.to_string())?;
284+
285+
let attr = AttributeTypeAndValue {
286+
oid: ID_RDNA_TRUSTANCHOR_ID,
287+
value: any_value,
288+
};
289+
290+
let rdn = RelativeDistinguishedName(
291+
SetOfVec::from_iter([attr]).expect("single attribute should always succeed"),
292+
);
293+
294+
Ok(RdnSequence::from(vec![rdn]))
295+
}
296+
297+
fn build_validity(now_millis: u64, max_lifetime_secs: u64) -> std::result::Result<Validity, String> {
298+
let now = Duration::from_millis(now_millis);
299+
let not_before = UtcTime::from_unix_duration(now).map_err(|e| e.to_string())?;
300+
let not_after =
301+
UtcTime::from_unix_duration(now + Duration::from_secs(max_lifetime_secs)).map_err(|e| e.to_string())?;
302+
303+
Ok(Validity {
304+
not_before: Time::UtcTime(not_before),
305+
not_after: Time::UtcTime(not_after),
306+
})
307+
}
308+
309+
/// Returns the issuer cert for SCT validation. For multi-cert chains, that's
310+
/// chain[1]. For single-cert chains, we look it up from the roots pool.
311+
fn resolve_issuer_for_sct(
312+
chain: &[Vec<u8>],
313+
roots: &x509_util::CertPool,
314+
) -> std::result::Result<Vec<u8>, String> {
315+
use der::{Decode, Encode};
316+
use x509_cert::Certificate;
317+
318+
if chain.is_empty() {
319+
return Err("chain is empty".into());
320+
}
321+
322+
if chain.len() > 1 {
323+
return Ok(chain[1].clone());
324+
}
325+
326+
// Single-cert chain: look up issuer from roots pool
327+
let leaf = Certificate::from_der(&chain[0]).map_err(|e| format!("failed to parse leaf: {e}"))?;
328+
let issuer_dn = &leaf.tbs_certificate.issuer;
329+
330+
roots
331+
.find_by_subject(issuer_dn)
332+
.ok_or_else(|| format!("issuer not found in roots pool: {issuer_dn}"))?
333+
.to_der()
334+
.map_err(|e| format!("failed to encode issuer: {e}"))
335+
}
336+
280337
#[allow(clippy::too_many_lines)]
281338
async fn add_entry(mut req: Request, env: &Env, name: &str) -> Result<Response> {
282339
let params = &CONFIG.logs[name];
283340
let req: AddEntryRequest = req.json().await?;
284341

285-
let issuer = RdnSequence::from(vec![RelativeDistinguishedName(
286-
SetOfVec::from_iter([AttributeTypeAndValue {
287-
oid: ID_RDNA_TRUSTANCHOR_ID,
288-
value: Any::new(
289-
Tag::Utf8String,
290-
Utf8StringRef::new(&params.log_id)
291-
.map_err(|e| e.to_string())?
292-
.as_bytes(),
293-
)
294-
.map_err(|e| e.to_string())?,
295-
}])
296-
.unwrap(),
297-
)]);
298-
299-
let now = Duration::from_millis(now_millis());
300-
let mut validity = Validity {
301-
not_before: Time::UtcTime(UtcTime::from_unix_duration(now).map_err(|e| e.to_string())?),
302-
not_after: Time::UtcTime(
303-
UtcTime::from_unix_duration(
304-
now + Duration::from_secs(params.max_certificate_lifetime_secs as u64),
305-
)
306-
.map_err(|e| e.to_string())?,
307-
),
308-
};
342+
let issuer = build_issuer_rdn(&params.log_id).map_err(|e| e.to_string())?;
343+
let mut validity =
344+
build_validity(now_millis(), params.max_certificate_lifetime_secs as u64).map_err(|e| e.to_string())?;
309345

310346
let roots = load_roots(env, name).await?;
311347
let (pending_entry, found_root_idx) =
@@ -328,25 +364,7 @@ async fn add_entry(mut req: Request, env: &Env, name: &str) -> Result<Response>
328364

329365
// Get leaf and issuer DER for SCT validation
330366
let leaf_der = req.chain.first().ok_or("Chain is empty")?;
331-
let issuer_der: Vec<u8> = if req.chain.len() > 1 {
332-
// Issuer is in the chain
333-
req.chain[1].clone()
334-
} else {
335-
// Single-cert chain: look up issuer (root) from roots pool
336-
use der::{Decode, Encode};
337-
use x509_cert::Certificate;
338-
339-
let leaf = Certificate::from_der(leaf_der)
340-
.map_err(|e| format!("failed to parse leaf: {e}"))?;
341-
342-
let issuer_dn = &leaf.tbs_certificate.issuer;
343-
344-
roots
345-
.find_by_subject(issuer_dn)
346-
.ok_or_else(|| format!("issuer not found in roots pool: {issuer_dn}"))?
347-
.to_der()
348-
.map_err(|e| format!("failed to encode issuer {e}"))?
349-
};
367+
let issuer_der = resolve_issuer_for_sct(&req.chain, roots).map_err(|e| e.to_string())?;
350368

351369
let validation_time_secs = now_millis() / 1000;
352370

@@ -500,3 +518,56 @@ fn headers_from_http_metadata(meta: HttpMetadata) -> Headers {
500518
}
501519
h
502520
}
521+
522+
#[cfg(test)]
523+
mod tests {
524+
use super::*;
525+
use der::Encode;
526+
527+
#[test]
528+
fn test_build_issuer_rdn() {
529+
let rdn = build_issuer_rdn("test-log-id").unwrap();
530+
assert_eq!(rdn.0.len(), 1);
531+
532+
let attr = rdn.0[0].0.iter().next().unwrap();
533+
assert_eq!(attr.oid, ID_RDNA_TRUSTANCHOR_ID);
534+
535+
let encoded = attr.value.to_der().unwrap();
536+
assert_eq!(encoded[0], 0x0C); // UTF8String tag
537+
}
538+
539+
#[test]
540+
fn test_build_validity() {
541+
let now_ms = 1_700_000_000_000_u64; // Nov 2023
542+
let lifetime_secs = 86400_u64; // 1 day
543+
544+
let validity = build_validity(now_ms, lifetime_secs).unwrap();
545+
546+
assert_eq!(validity.not_before.to_unix_duration().as_secs(), now_ms / 1000);
547+
assert_eq!(validity.not_after.to_unix_duration().as_secs(), now_ms / 1000 + lifetime_secs);
548+
}
549+
550+
#[test]
551+
fn test_resolve_issuer_empty_chain() {
552+
let chain: Vec<Vec<u8>> = vec![];
553+
let roots = x509_util::CertPool::new(vec![]).unwrap();
554+
assert!(resolve_issuer_for_sct(&chain, &roots).is_err());
555+
}
556+
557+
#[test]
558+
fn test_resolve_issuer_multi_cert_chain() {
559+
let leaf = vec![1, 2, 3];
560+
let issuer = vec![4, 5, 6];
561+
let chain = vec![leaf, issuer.clone()];
562+
let roots = x509_util::CertPool::new(vec![]).unwrap();
563+
564+
assert_eq!(resolve_issuer_for_sct(&chain, &roots).unwrap(), issuer);
565+
}
566+
567+
#[test]
568+
fn test_resolve_issuer_invalid_der() {
569+
let chain = vec![vec![0xFF, 0xFF, 0xFF]];
570+
let roots = x509_util::CertPool::new(vec![]).unwrap();
571+
assert!(resolve_issuer_for_sct(&chain, &roots).is_err());
572+
}
573+
}

0 commit comments

Comments
 (0)