Serialize OCSP Redis snapshots with CBOR

This commit is contained in:
woggioni committed 2026-10-28 20:54:19 +00:00
1 parent df98ada177
commit ef64c4a532
12 files changed
+191 -56

No files matched your search

+77 -25
View File
@@ -3,7 +3,7 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use sha1::{Digest as _, Sha1};
use sha2::Sha256;
use x509_cert::ext::pkix::CrlReason;
@@ -28,7 +28,7 @@ pub struct Snapshot {
pub entries: HashMap<Vec<u8>, StatusEntry>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
struct RawSnapshot {
schema: String,
issuer: RawIssuer,
@@ -36,7 +36,7 @@ struct RawSnapshot {
entries: Vec<RawEntry>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
struct RawIssuer {
#[serde(default)]
id: String,
@@ -44,7 +44,7 @@ struct RawIssuer {
sha256: String,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
struct RawEntry {
serial_hex: String,
status: String,
@@ -61,7 +61,9 @@ impl Snapshot {
expected_sha256: &str,
expected_ca_id: &str,
) -> Result<Self, SnapshotError> {
let raw: RawSnapshot = serde_json::from_slice(bytes)?;
cbor2::validate_slice(bytes).map_err(|error| SnapshotError::Cbor(error.to_string()))?;
let raw: RawSnapshot =
cbor2::from_slice(bytes).map_err(|error| SnapshotError::Cbor(error.to_string()))?;
if raw.schema != SCHEMA {
return Err(SnapshotError::Schema(raw.schema));
}
@@ -166,8 +168,8 @@ pub fn hash_sha256(bytes: &[u8]) -> Vec<u8> {
#[derive(Debug, thiserror::Error)]
pub enum SnapshotError {
#[error("invalid JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("invalid CBOR: {0}")]
Cbor(String),
#[error("unsupported snapshot schema {0:?}")]
Schema(String),
#[error("snapshot issuer ID {actual} does not match configured CA ID {expected}")]
@@ -184,27 +186,64 @@ pub enum SnapshotError {
DuplicateSerial(String),
}
#[cfg(test)]
pub(crate) fn test_body(sha: &str) -> Vec<u8> {
let raw = RawSnapshot {
schema: SCHEMA.to_string(),
issuer: RawIssuer {
id: "ca".to_string(),
sha256: sha.to_string(),
},
entries: vec![RawEntry {
serial_hex: "01".to_string(),
status: "valid".to_string(),
revoked_at: None,
reason: None,
}],
};
cbor2::to_vec(&raw).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
fn body(sha: &str) -> String {
format!(
r#"{{
"schema": "kca-ocsp-status/v2",
"issuer": {{"id": "ca", "sha256": "{sha}", "subject": "CN=CA"}},
"entries": [
{{"serial_hex": "01ab", "status": "valid", "not_after": "2027-01-01T00:00:00Z"}},
{{"serial_hex": "00ff", "status": "revoked", "revoked_at": "2026-02-03T04:05:06Z", "reason": "keyCompromise"}}
]
}}"#
)
fn raw_body(sha: &str, schema: &str) -> RawSnapshot {
RawSnapshot {
schema: schema.to_string(),
issuer: RawIssuer {
id: "ca".to_string(),
sha256: sha.to_string(),
},
entries: vec![
RawEntry {
serial_hex: "01ab".to_string(),
status: "valid".to_string(),
revoked_at: None,
reason: None,
},
RawEntry {
serial_hex: "00ff".to_string(),
status: "revoked".to_string(),
revoked_at: Some(
DateTime::parse_from_rfc3339("2026-02-03T04:05:06Z")
.unwrap()
.with_timezone(&Utc),
),
reason: Some("keyCompromise".to_string()),
},
],
}
}
fn body(sha: &str) -> Vec<u8> {
cbor2::to_vec(&raw_body(sha, SCHEMA)).unwrap()
}
#[test]
fn parses_statuses_and_reasons() {
let sha = sha256_hex(b"ca");
let snapshot = Snapshot::parse(body(&sha).as_bytes(), &sha, "ca").unwrap();
let snapshot = Snapshot::parse(&body(&sha), &sha, "ca").unwrap();
let good = snapshot.status_for_serial(&[0x01, 0xab]).unwrap();
assert_eq!(good.status, Status::Good);
let revoked = snapshot.status_for_serial(&[0xff]).unwrap();
@@ -216,15 +255,28 @@ mod tests {
#[test]
fn rejects_wrong_issuer_schema_and_duplicates() {
let sha = sha256_hex(b"ca");
assert!(Snapshot::parse(body(&sha).as_bytes(), "wrong", "ca").is_err());
assert!(Snapshot::parse(&body(&sha), "wrong", "ca").is_err());
assert!(matches!(
Snapshot::parse(body(&sha).as_bytes(), &sha, "other"),
Snapshot::parse(&body(&sha), &sha, "other"),
Err(SnapshotError::IssuerIdMismatch { .. })
));
let wrong_schema = body(&sha).replace(SCHEMA, "other");
assert!(Snapshot::parse(wrong_schema.as_bytes(), &sha, "ca").is_err());
let duplicate = body(&sha).replace("\"00ff\"", "\"01ab\"");
assert!(Snapshot::parse(duplicate.as_bytes(), &sha, "ca").is_err());
let wrong_schema = cbor2::to_vec(&raw_body(&sha, "other")).unwrap();
assert!(Snapshot::parse(&wrong_schema, &sha, "ca").is_err());
let mut duplicate = raw_body(&sha, SCHEMA);
duplicate.entries[1].serial_hex = duplicate.entries[0].serial_hex.clone();
let duplicate = cbor2::to_vec(&duplicate).unwrap();
assert!(Snapshot::parse(&duplicate, &sha, "ca").is_err());
}
#[test]
fn rejects_legacy_json() {
let sha = sha256_hex(b"ca");
let legacy =
br#"{"schema":"kca-ocsp-status/v2","issuer":{"id":"ca","sha256":"sha"},"entries":[]}"#;
assert!(matches!(
Snapshot::parse(legacy, &sha, "ca"),
Err(SnapshotError::Cbor(_))
));
}
#[test]