use serde_json::{json, Value as Json};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OcspPurpose {
ClaimSigner,
CawgIdentity { assertion_label: String },
}
impl OcspPurpose {
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::ClaimSigner => "claim_signer",
Self::CawgIdentity { .. } => "cawg_identity",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NetworkNeed {
RemoteManifest { uri: String },
Ocsp {
purpose: OcspPurpose,
responder_url: String,
request_der: Vec<u8>,
certificate_sha256_hex: String,
},
DidDocument { did: String, url: String },
ExternalData {
uri: String,
assertion_label: String,
},
}
impl NetworkNeed {
pub(crate) fn to_json(&self) -> Json {
match self {
Self::RemoteManifest { uri } => json!({
"kind": "remote_manifest",
"uri": uri,
}),
Self::Ocsp {
purpose,
responder_url,
certificate_sha256_hex,
..
} => {
let mut value = json!({
"kind": "ocsp",
"purpose": purpose.as_str(),
"responder_url": responder_url,
"certificate_sha256": certificate_sha256_hex,
});
if let OcspPurpose::CawgIdentity { assertion_label } = purpose {
value["assertion_label"] = json!(assertion_label);
}
value
}
Self::DidDocument { did, url } => json!({
"kind": "did_document",
"did": did,
"url": url,
}),
Self::ExternalData {
uri,
assertion_label,
} => json!({
"kind": "external_data",
"uri": uri,
"assertion_label": assertion_label,
}),
}
}
}
pub(super) fn push_unique(needs: &mut Vec<NetworkNeed>, need: NetworkNeed) {
const MAX_NETWORK_NEEDS: usize = 64;
if needs.len() >= MAX_NETWORK_NEEDS || needs.contains(&need) {
return;
}
needs.push(need);
}
pub(super) fn did_web_url(did: &str) -> Option<String> {
let identifier = did.strip_prefix("did:web:")?;
let identifier = identifier.split('#').next().unwrap_or_default();
let identifier = identifier.split('?').next().unwrap_or_default();
if identifier.is_empty() {
return None;
}
let mut segments = identifier.split(':');
let host = segments.next()?.replace("%3A", ":").replace("%3a", ":");
if host.is_empty() || host.contains('/') {
return None;
}
let path: Vec<&str> = segments.filter(|segment| !segment.is_empty()).collect();
if path.is_empty() {
return Some(format!("https://{host}/.well-known/did.json"));
}
Some(format!("https://{host}/{}/did.json", path.join("/")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_host_resolves_to_the_well_known_document() {
assert_eq!(
did_web_url("did:web:example.com").as_deref(),
Some("https://example.com/.well-known/did.json")
);
}
#[test]
fn a_port_and_path_travel_into_the_url() {
assert_eq!(
did_web_url("did:web:example.com%3A8443:user:alice").as_deref(),
Some("https://example.com:8443/user/alice/did.json")
);
}
#[test]
fn a_fragment_names_a_key_inside_the_same_document() {
assert_eq!(
did_web_url("did:web:example.com:issuers:1#key-0").as_deref(),
Some("https://example.com/issuers/1/did.json")
);
}
#[test]
fn a_non_web_did_has_no_url() {
assert_eq!(did_web_url("did:key:z6Mk"), None);
assert_eq!(did_web_url("did:web:"), None);
}
#[test]
fn the_cawg_ocsp_shape_names_the_assertion_it_came_from() {
let need = NetworkNeed::Ocsp {
purpose: OcspPurpose::CawgIdentity {
assertion_label: "cawg.identity".into(),
},
responder_url: "http://ocsp.example/r".into(),
request_der: vec![0x30, 0x00],
certificate_sha256_hex: "ab".repeat(32),
};
assert_eq!(
need.to_json(),
json!({
"kind": "ocsp",
"purpose": "cawg_identity",
"assertion_label": "cawg.identity",
"responder_url": "http://ocsp.example/r",
"certificate_sha256": "ab".repeat(32),
})
);
}
#[test]
fn the_claim_signer_ocsp_shape_carries_no_assertion_label() {
let need = NetworkNeed::Ocsp {
purpose: OcspPurpose::ClaimSigner,
responder_url: "http://ocsp.example/r".into(),
request_der: Vec::new(),
certificate_sha256_hex: "cd".repeat(32),
};
assert_eq!(
need.to_json(),
json!({
"kind": "ocsp",
"purpose": "claim_signer",
"responder_url": "http://ocsp.example/r",
"certificate_sha256": "cd".repeat(32),
})
);
}
#[test]
fn the_same_need_is_never_recorded_twice() {
let mut needs = Vec::new();
for _ in 0..3 {
push_unique(
&mut needs,
NetworkNeed::RemoteManifest {
uri: "https://example.com/store.c2pa".into(),
},
);
}
push_unique(
&mut needs,
NetworkNeed::RemoteManifest {
uri: "https://other.example/store.c2pa".into(),
},
);
assert_eq!(needs.len(), 2);
}
}