Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 158 additions & 10 deletions cesr/src/serder/deserialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ pub fn deserialize_event(raw: &[u8]) -> Result<KeriEvent, SerderError> {
///
/// # Errors
///
/// Returns [`SerderError`] if JSON parsing fails, required fields are missing
/// or invalid, or the SAID does not verify.
/// Returns [`SerderError`] if JSON parsing fails, the version string is
/// malformed or inconsistent with the input length, required fields are
/// missing or invalid, or the SAID does not verify.
pub fn deserialize_inception(raw: &[u8]) -> Result<InceptionEvent, SerderError> {
validate_version_string(raw)?;
let val: Value = serde_json::from_slice(raw)?;
let digest_code = infer_digest_code(get_str(&val, "d")?)?;
let d_str = get_str(&val, "d")?;
Expand Down Expand Up @@ -110,9 +112,11 @@ pub fn deserialize_inception(raw: &[u8]) -> Result<InceptionEvent, SerderError>
///
/// # Errors
///
/// Returns [`SerderError`] if JSON parsing fails, required fields are missing
/// or invalid, or the SAID does not verify.
/// Returns [`SerderError`] if JSON parsing fails, the version string is
/// malformed or inconsistent with the input length, required fields are
/// missing or invalid, or the SAID does not verify.
pub fn deserialize_rotation(raw: &[u8]) -> Result<RotationEvent, SerderError> {
validate_version_string(raw)?;
let val: Value = serde_json::from_slice(raw)?;
let digest_code = infer_digest_code(get_str(&val, "d")?)?;

Expand Down Expand Up @@ -156,9 +160,11 @@ pub fn deserialize_rotation(raw: &[u8]) -> Result<RotationEvent, SerderError> {
///
/// # Errors
///
/// Returns [`SerderError`] if JSON parsing fails, required fields are missing
/// or invalid, or the SAID does not verify.
/// Returns [`SerderError`] if JSON parsing fails, the version string is
/// malformed or inconsistent with the input length, required fields are
/// missing or invalid, or the SAID does not verify.
pub fn deserialize_interaction(raw: &[u8]) -> Result<InteractionEvent, SerderError> {
validate_version_string(raw)?;
let val: Value = serde_json::from_slice(raw)?;
let digest_code = infer_digest_code(get_str(&val, "d")?)?;

Expand Down Expand Up @@ -186,9 +192,11 @@ pub fn deserialize_interaction(raw: &[u8]) -> Result<InteractionEvent, SerderErr
///
/// # Errors
///
/// Returns [`SerderError`] if JSON parsing fails, required fields are missing
/// or invalid, or the SAID does not verify.
/// Returns [`SerderError`] if JSON parsing fails, the version string is
/// malformed or inconsistent with the input length, required fields are
/// missing or invalid, or the SAID does not verify.
pub fn deserialize_delegated_inception(raw: &[u8]) -> Result<DelegatedInceptionEvent, SerderError> {
validate_version_string(raw)?;
let val: Value = serde_json::from_slice(raw)?;
let digest_code = infer_digest_code(get_str(&val, "d")?)?;
let d_str = get_str(&val, "d")?;
Expand Down Expand Up @@ -235,8 +243,9 @@ pub fn deserialize_delegated_inception(raw: &[u8]) -> Result<DelegatedInceptionE
///
/// # Errors
///
/// Returns [`SerderError`] if JSON parsing fails, required fields are missing
/// or invalid, or the SAID does not verify.
/// Returns [`SerderError`] if JSON parsing fails, the version string is
/// malformed or inconsistent with the input length, required fields are
/// missing or invalid, or the SAID does not verify.
pub fn deserialize_delegated_rotation(raw: &[u8]) -> Result<DelegatedRotationEvent, SerderError> {
let rotation = deserialize_rotation(raw)?;
Ok(DelegatedRotationEvent::new(rotation))
Expand Down Expand Up @@ -1298,6 +1307,145 @@ mod tests {
assert_eq!((n, d), (1, 1));
}

// -----------------------------------------------------------------------
// Version-string validation at every public entry point
// -----------------------------------------------------------------------

/// Insert one space after the first comma: the parsed JSON (and therefore
/// the SAID computed over its compact re-serialization) is unchanged, but
/// the raw length no longer matches the version-string size field.
fn whitespace_padded(raw: &[u8]) -> Vec<u8> {
let idx = raw
.iter()
.position(|b| *b == b',')
.expect("event JSON has a comma");
let mut padded = Vec::with_capacity(raw.len() + 1);
padded.extend_from_slice(&raw[..=idx]);
padded.push(b' ');
padded.extend_from_slice(&raw[idx + 1..]);
padded
}

fn probe_icp() -> InceptionEvent {
InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
)
}

fn probe_rot() -> RotationEvent {
RotationEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
vec![],
0,
vec![],
vec![],
)
}

#[test]
fn deserialize_inception_rejects_length_mismatched_raw() {
let raw = serialize_inception(&probe_icp()).unwrap();
let padded = whitespace_padded(raw.as_bytes());
// Precondition making this a real probe: the padded bytes are still
// valid JSON with an intact SAID — only the length lies.
assert!(serde_json::from_slice::<Value>(&padded).is_ok());
assert!(
matches!(
deserialize_inception(&padded),
Err(SerderError::InvalidVersionString(_))
),
"deserialize_inception must reject raw whose length contradicts its version string"
);
}

#[test]
fn deserialize_event_rejects_length_mismatched_raw() {
let raw = serialize_inception(&probe_icp()).unwrap();
let padded = whitespace_padded(raw.as_bytes());
assert!(
matches!(
deserialize_event(&padded),
Err(SerderError::InvalidVersionString(_))
),
"deserialize_event must keep rejecting length-mismatched raw"
);
}

#[test]
fn deserialize_rotation_rejects_length_mismatched_raw() {
let raw = serialize_rotation(&probe_rot()).unwrap();
assert!(
matches!(
deserialize_rotation(&whitespace_padded(raw.as_bytes())),
Err(SerderError::InvalidVersionString(_))
),
"deserialize_rotation must reject length-mismatched raw"
);
}

#[test]
fn deserialize_interaction_rejects_length_mismatched_raw() {
let event = InteractionEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![],
);
let raw = serialize_interaction(&event).unwrap();
assert!(
matches!(
deserialize_interaction(&whitespace_padded(raw.as_bytes())),
Err(SerderError::InvalidVersionString(_))
),
"deserialize_interaction must reject length-mismatched raw"
);
}

#[test]
fn deserialize_delegated_inception_rejects_length_mismatched_raw() {
let event = DelegatedInceptionEvent::new(probe_icp(), make_prefixer().into());
let raw = serialize_delegated_inception(&event).unwrap();
assert!(
matches!(
deserialize_delegated_inception(&whitespace_padded(raw.as_bytes())),
Err(SerderError::InvalidVersionString(_))
),
"deserialize_delegated_inception must reject length-mismatched raw"
);
}

#[test]
fn deserialize_delegated_rotation_rejects_length_mismatched_raw() {
let event = DelegatedRotationEvent::new(probe_rot());
let raw = serialize_delegated_rotation(&event).unwrap();
assert!(
matches!(
deserialize_delegated_rotation(&whitespace_padded(raw.as_bytes())),
Err(SerderError::InvalidVersionString(_))
),
"deserialize_delegated_rotation must reject length-mismatched raw"
);
}

// -----------------------------------------------------------------------
// Parse-failure routing: a malformed qb64 code is a parsing-domain error
// -----------------------------------------------------------------------
Expand Down
11 changes: 11 additions & 0 deletions cesr/src/serder/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ pub enum SerderError {
source: ParsingError,
},

/// A version-string field's value does not fit its fixed-width hex
/// encoding — rendering it anyway would widen the string and corrupt the
/// 17-byte frame.
#[error("version string field '{field}' exceeds its fixed-width capacity of {max}")]
VersionStringOverflow {
/// The version-string field that does not fit.
field: &'static str,
/// The largest value the field's fixed width can encode.
max: u32,
},

/// Digest computation failed.
#[error("digest error: {0}")]
DigestError(String),
Expand Down
15 changes: 10 additions & 5 deletions cesr/src/serder/serialize/dip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{SerializedEvent, matters_to_json_array, seal_to_json, tholder_to_jso
use crate::serder::error::SerderError;
use crate::serder::primitives::{identifier_to_qb64_string, sn_to_hex, to_qb64_string};
use crate::serder::said::{compute_digest, said_placeholder};
use crate::serder::version::VersionString;
use crate::serder::version::{VERSION_SIZE_MAX, VersionString};

/// Serialize a [`DelegatedInceptionEvent`] to canonical JSON with a computed SAID.
///
Expand Down Expand Up @@ -71,14 +71,19 @@ pub fn serialize_delegated_inception(
delegator: &delegator_qb64,
};

let phase1_vs = VersionString::keri_json_v1().to_str();
let phase1_vs = VersionString::keri_json_v1().to_str()?;
let phase1_json = build_dip_json(&phase1_vs, &placeholder, &fields)?;
let measured_len =
u32::try_from(phase1_json.len()).map_err(|e| SerderError::DigestError(e.to_string()))?;
let measured_len = u32::try_from(phase1_json.len())
.ok()
.filter(|len| *len <= VERSION_SIZE_MAX)
.ok_or(SerderError::VersionStringOverflow {
field: "size",
max: VERSION_SIZE_MAX,
})?;

let vs_with_size = VersionString::keri_json_v1()
.with_size(measured_len)
.to_str();
.to_str()?;
let phase2_json = build_dip_json(&vs_with_size, &placeholder, &fields)?;

let said = compute_digest(phase2_json.as_bytes(), digest_code)?;
Expand Down
15 changes: 10 additions & 5 deletions cesr/src/serder/serialize/drt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{SerializedEvent, matters_to_json_array, seal_to_json, tholder_to_jso
use crate::serder::error::SerderError;
use crate::serder::primitives::{identifier_to_qb64_string, sn_to_hex, to_qb64_string};
use crate::serder::said::{compute_digest, said_placeholder};
use crate::serder::version::VersionString;
use crate::serder::version::{VERSION_SIZE_MAX, VersionString};

/// Serialize a [`DelegatedRotationEvent`] to canonical JSON with a computed SAID.
///
Expand Down Expand Up @@ -67,14 +67,19 @@ pub fn serialize_delegated_rotation(
anchors: &anchors_value,
};

let phase1_vs = VersionString::keri_json_v1().to_str();
let phase1_vs = VersionString::keri_json_v1().to_str()?;
let phase1_json = build_drt_json(&phase1_vs, &placeholder, &fields)?;
let measured_len =
u32::try_from(phase1_json.len()).map_err(|e| SerderError::DigestError(e.to_string()))?;
let measured_len = u32::try_from(phase1_json.len())
.ok()
.filter(|len| *len <= VERSION_SIZE_MAX)
.ok_or(SerderError::VersionStringOverflow {
field: "size",
max: VERSION_SIZE_MAX,
})?;

let vs_with_size = VersionString::keri_json_v1()
.with_size(measured_len)
.to_str();
.to_str()?;
let phase2_json = build_drt_json(&vs_with_size, &placeholder, &fields)?;

let said = compute_digest(phase2_json.as_bytes(), digest_code)?;
Expand Down
15 changes: 10 additions & 5 deletions cesr/src/serder/serialize/icp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{SerializedEvent, matters_to_json_array, seal_to_json, tholder_to_jso
use crate::serder::error::SerderError;
use crate::serder::primitives::{sn_to_hex, to_qb64_string};
use crate::serder::said::{compute_digest, said_placeholder};
use crate::serder::version::VersionString;
use crate::serder::version::{VERSION_SIZE_MAX, VersionString};

/// Serialize an [`InceptionEvent`] to canonical JSON with a computed SAID.
///
Expand Down Expand Up @@ -64,15 +64,20 @@ pub fn serialize_inception(event: &InceptionEvent) -> Result<SerializedEvent, Se
};

// Phase 1: build JSON with placeholder SAIDs and zero size to measure length
let phase1_vs = VersionString::keri_json_v1().to_str();
let phase1_vs = VersionString::keri_json_v1().to_str()?;
let phase1_json = build_icp_json(&phase1_vs, &placeholder, &fields)?;
let measured_len =
u32::try_from(phase1_json.len()).map_err(|e| SerderError::DigestError(e.to_string()))?;
let measured_len = u32::try_from(phase1_json.len())
.ok()
.filter(|len| *len <= VERSION_SIZE_MAX)
.ok_or(SerderError::VersionStringOverflow {
field: "size",
max: VERSION_SIZE_MAX,
})?;

// Phase 2: rebuild with correct size in version string (same byte length)
let vs_with_size = VersionString::keri_json_v1()
.with_size(measured_len)
.to_str();
.to_str()?;
let phase2_json = build_icp_json(&vs_with_size, &placeholder, &fields)?;

// Phase 3: compute SAID over the correctly-sized JSON
Expand Down
40 changes: 35 additions & 5 deletions cesr/src/serder/serialize/ixn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{SerializedEvent, seal_to_json};
use crate::serder::error::SerderError;
use crate::serder::primitives::{identifier_to_qb64_string, sn_to_hex, to_qb64_string};
use crate::serder::said::{compute_digest, said_placeholder};
use crate::serder::version::VersionString;
use crate::serder::version::{VERSION_SIZE_MAX, VersionString};

/// Serialize an [`InteractionEvent`] to canonical JSON with a computed SAID.
///
Expand Down Expand Up @@ -46,15 +46,20 @@ pub fn serialize_interaction(event: &InteractionEvent) -> Result<SerializedEvent
};

// Phase 1: build JSON with placeholder SAID and zero size to measure length
let phase1_vs = VersionString::keri_json_v1().to_str();
let phase1_vs = VersionString::keri_json_v1().to_str()?;
let phase1_json = build_ixn_json(&phase1_vs, &placeholder, &fields)?;
let measured_len =
u32::try_from(phase1_json.len()).map_err(|e| SerderError::DigestError(e.to_string()))?;
let measured_len = u32::try_from(phase1_json.len())
.ok()
.filter(|len| *len <= VERSION_SIZE_MAX)
.ok_or(SerderError::VersionStringOverflow {
field: "size",
max: VERSION_SIZE_MAX,
})?;

// Phase 2: rebuild with correct size in version string (same byte length)
let vs_with_size = VersionString::keri_json_v1()
.with_size(measured_len)
.to_str();
.to_str()?;
let phase2_json = build_ixn_json(&vs_with_size, &placeholder, &fields)?;

// Phase 3: compute SAID over the correctly-sized JSON
Expand Down Expand Up @@ -153,6 +158,31 @@ mod tests {
assert_eq!(result.ilk(), Ilk::Ixn);
}

#[test]
fn serialize_ixn_rejects_event_beyond_version_size_capacity() {
// Bug probe: an event whose JSON exceeds the six-hex-digit size field
// (16 MiB - 1) previously rendered a widened version string, silently
// corrupting the frame instead of returning an error.
let anchors: Vec<Seal> = (0..340_000)
.map(|_| Seal::Digest { d: make_saider() })
.collect();
let event = InteractionEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
anchors,
);
let result = serialize_interaction(&event);
assert!(matches!(
result,
Err(SerderError::VersionStringOverflow {
field: "size",
max: VERSION_SIZE_MAX,
})
));
}

#[test]
fn serialize_ixn_version_string_size_matches() {
let event = make_event();
Expand Down
Loading
Loading