Skip to content

Commit a0b49cc

Browse files
committed
privacy: make OpaquePointer.digest optional; add runtime validate() invariants for low-entropy class; update tests
1 parent 2ab91ac commit a0b49cc

File tree

4 files changed

+66
-4
lines changed

4 files changed

+66
-4
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/gatos-privacy/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ serde_json = { workspace = true }
99
blake3 = { workspace = true }
1010
hex = { workspace = true }
1111
anyhow = { workspace = true }
12-
12+
thiserror = "1"

crates/gatos-privacy/src/lib.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use serde_json::Value;
1717
pub struct OpaquePointer {
1818
pub kind: Kind,
1919
pub algo: Algo,
20-
pub digest: String,
20+
#[serde(skip_serializing_if = "Option::is_none")]
21+
pub digest: Option<String>,
2122
#[serde(skip_serializing_if = "Option::is_none")]
2223
pub ciphertext_digest: Option<String>,
2324
#[serde(skip_serializing_if = "Option::is_none")]
@@ -39,3 +40,44 @@ pub enum Kind {
3940
pub enum Algo {
4041
Blake3,
4142
}
43+
44+
impl OpaquePointer {
45+
/// Validate invariants beyond serde schema mapping.
46+
pub fn validate(&self) -> Result<(), PointerError> {
47+
let has_plain = self.digest.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
48+
let has_cipher = self
49+
.ciphertext_digest
50+
.as_ref()
51+
.map(|s| !s.is_empty())
52+
.unwrap_or(false);
53+
if !(has_plain || has_cipher) {
54+
return Err(PointerError::MissingDigest);
55+
}
56+
let low_entropy = self
57+
.extensions
58+
.as_ref()
59+
.and_then(|v| v.get("class"))
60+
.and_then(|c| c.as_str())
61+
.map(|s| s == "low-entropy")
62+
.unwrap_or(false);
63+
if low_entropy {
64+
if !has_cipher {
65+
return Err(PointerError::LowEntropyNeedsCiphertextDigest);
66+
}
67+
if has_plain {
68+
return Err(PointerError::LowEntropyForbidsPlainDigest);
69+
}
70+
}
71+
Ok(())
72+
}
73+
}
74+
75+
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
76+
pub enum PointerError {
77+
#[error("at least one of digest or ciphertext_digest is required")]
78+
MissingDigest,
79+
#[error("low-entropy class requires ciphertext_digest")]
80+
LowEntropyNeedsCiphertextDigest,
81+
#[error("low-entropy class forbids plaintext digest")]
82+
LowEntropyForbidsPlainDigest,
83+
}

crates/gatos-privacy/tests/pointer_schema.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ fn ciphertext_only_pointer_should_deserialize() {
1717
fn both_digests_allowed_when_not_low_entropy() {
1818
let json = read_example("privacy/opaque_pointer_min.json");
1919
let ptr: OpaquePointer = serde_json::from_str(&json).unwrap();
20-
let has_digest = !ptr.digest.is_empty();
20+
let has_digest = ptr.digest.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
2121
assert!(has_digest);
2222
assert!(ptr.ciphertext_digest.is_some());
2323
}
24-

0 commit comments

Comments
 (0)