Serialize OCSP Redis snapshots with CBOR
This commit is contained in:
1 parent
df98ada177
commit
ef64c4a532
12 files changed
+191
-56
No files matched your search
@@ -135,11 +135,11 @@ mod tests {
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::*;
|
||||
use crate::snapshot::sha256_hex;
|
||||
use crate::snapshot::{sha256_hex, test_body as body};
|
||||
use crate::store::{ObjectStore, StoreError, StoredSnapshot};
|
||||
|
||||
struct FakeStore {
|
||||
body: String,
|
||||
body: Vec<u8>,
|
||||
expires_at: SystemTime,
|
||||
calls: AtomicUsize,
|
||||
fail: AtomicBool,
|
||||
@@ -147,7 +147,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl FakeStore {
|
||||
fn new(body: String, lifetime: Duration) -> Self {
|
||||
fn new(body: Vec<u8>, lifetime: Duration) -> Self {
|
||||
Self {
|
||||
body,
|
||||
expires_at: SystemTime::now() + lifetime,
|
||||
@@ -157,7 +157,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn expired(body: String) -> Self {
|
||||
fn expired(body: Vec<u8>) -> Self {
|
||||
Self {
|
||||
body,
|
||||
expires_at: SystemTime::now() - Duration::from_secs(1),
|
||||
@@ -185,12 +185,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn body(sha: &str) -> String {
|
||||
format!(
|
||||
r#"{{"schema":"kca-ocsp-status/v2","issuer":{{"id":"ca","sha256":"{sha}"}},"entries":[{{"serial_hex":"01","status":"valid"}}]}}"#
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn single_flight_refreshes_once() {
|
||||
let sha = sha256_hex(b"ca");
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -133,13 +133,14 @@ mod tests {
|
||||
prefix: format!("ocsp:test:{}", std::process::id()),
|
||||
..Default::default()
|
||||
};
|
||||
let key = config.key("round-trip");
|
||||
let key = config.key("ca");
|
||||
let sha = crate::snapshot::sha256_hex(b"ca");
|
||||
let store = StatusStore::new(&config).await.unwrap();
|
||||
let client = redis::Client::open(config.url.as_str()).unwrap();
|
||||
let mut connection = client.get_connection_manager().await.unwrap();
|
||||
let _: () = redis::cmd("SET")
|
||||
.arg(&key)
|
||||
.arg("snapshot")
|
||||
.arg(crate::snapshot::test_body(&sha))
|
||||
.arg("EX")
|
||||
.arg(30)
|
||||
.query_async(&mut connection)
|
||||
@@ -149,7 +150,8 @@ mod tests {
|
||||
let stored = store.get(&key).await.unwrap();
|
||||
match stored {
|
||||
StoredSnapshot::Present { body, expires_at } => {
|
||||
assert_eq!(body, Bytes::from_static(b"snapshot"));
|
||||
let snapshot = crate::snapshot::Snapshot::parse(&body, &sha, "ca").unwrap();
|
||||
assert_eq!(snapshot.entries.len(), 1);
|
||||
assert!(expires_at > SystemTime::now());
|
||||
}
|
||||
StoredSnapshot::Missing => panic!("snapshot should be present"),
|
||||
|
||||
Reference in new issue
Block a user