cultnet-rs: stored envelope payloads decode through cultcache-rs decode_payload
replication's document_record_from_envelope and the service identity credential read no longer call rmp_serde::from_slice on a stored payload, so the 256 bound holds there too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a5c0f876de
commit
d47cd03890
3 changed files with 74 additions and 2 deletions
|
|
@ -522,7 +522,7 @@ impl CultNetDocumentRegistry {
|
|||
envelope: &CultCacheEnvelope,
|
||||
) -> Result<CultNetDocumentRecord<Value>> {
|
||||
let binding = self.require_binding(&envelope.r#type)?;
|
||||
let payload: Value = rmp_serde::from_slice(&envelope.payload).with_context(|| {
|
||||
let payload: Value = cultcache_rs::decode_payload(&envelope.payload).with_context(|| {
|
||||
format!(
|
||||
"failed to decode CultCache envelope {:?} at {:?} as generic CultNet payload",
|
||||
envelope.r#type, envelope.key
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ fn open_service_identity_envelopes<P: ServiceIdentityProfile>(
|
|||
{
|
||||
bail!("service identity store belongs to a different profile or schema");
|
||||
}
|
||||
let entry: ServiceIdentityPrivateEntry = rmp_serde::from_slice(&envelope.payload)
|
||||
let entry: ServiceIdentityPrivateEntry = cultcache_rs::decode_payload(&envelope.payload)
|
||||
.context("service identity private payload is malformed MessagePack")?;
|
||||
validate_private::<P>(&entry)?;
|
||||
if entry.protector_kind != platform_protector_kind() || entry.assurance != platform_assurance()
|
||||
|
|
@ -741,6 +741,34 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_credential_payload_nested_past_the_bound_is_refused_by_the_cache_walk() -> Result<()> {
|
||||
let limit = cultcache_rs::MAX_PAYLOAD_DEPTH;
|
||||
let credential = |levels: usize| -> Result<Vec<u8>> {
|
||||
let envelope = CultCacheEnvelope {
|
||||
key: IdunnServiceIdentity::PRIVATE_KEY.into(),
|
||||
r#type: IdunnServiceIdentity::PRIVATE_TYPE.into(),
|
||||
payload: [vec![0x91u8; levels], vec![0xc0]].concat(),
|
||||
stored_at: "2026-10-11T00:00:00Z".into(),
|
||||
schema_id: Some(IdunnServiceIdentity::PRIVATE_SCHEMA.into()),
|
||||
};
|
||||
Ok(rmp_serde::to_vec(&vec![envelope])?)
|
||||
};
|
||||
let past = open_service_identity_credential_bytes::<IdunnServiceIdentity>(&credential(limit + 1)?)
|
||||
.err()
|
||||
.expect("a payload past the bound is refused");
|
||||
assert_eq!(
|
||||
past.downcast_ref::<cultcache_rs::PayloadTooDeep>(),
|
||||
Some(&cultcache_rs::PayloadTooDeep { depth: limit + 1, limit })
|
||||
);
|
||||
// At the bound the walk lets the payload through to the decoder, which refuses its shape.
|
||||
let at = open_service_identity_credential_bytes::<IdunnServiceIdentity>(&credential(limit)?)
|
||||
.err()
|
||||
.expect("the payload is not a credential");
|
||||
assert!(at.downcast_ref::<cultcache_rs::PayloadTooDeep>().is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immutable_credential_reader_opens_without_a_path_and_retains_signing_authority() -> Result<()>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use chrono::Duration;
|
||||
use chrono::Utc;
|
||||
use cultcache_rs::CacheBackingStore;
|
||||
use cultcache_rs::CultCache;
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use cultcache_rs::SingleFileMessagePackBackingStore;
|
||||
|
|
@ -1778,6 +1779,49 @@ fn document_registry_advertises_mutation_contracts_with_bindings() -> Result<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stored_payload_nested_past_the_bound_is_not_replicated_as_a_document() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("foreign.msgpack");
|
||||
let limit = cultcache_rs::MAX_PAYLOAD_DEPTH;
|
||||
let envelope = |key: &str, levels: usize| cultcache_rs::CultCacheEnvelope {
|
||||
key: key.to_string(),
|
||||
r#type: GhostlightAgentStateFixture::TYPE.to_string(),
|
||||
payload: [vec![0x91u8; levels], vec![0xc0]].concat(),
|
||||
stored_at: "2026-10-11T00:00:00Z".to_string(),
|
||||
schema_id: None,
|
||||
};
|
||||
let mut registry = CultNetDocumentRegistry::new();
|
||||
registry.register(CultNetDocumentBinding::for_entry::<GhostlightAgentStateFixture>(
|
||||
"ghostlight.agent_state.v0".to_string(),
|
||||
));
|
||||
// A store written by something that does not hold the bound.
|
||||
let mut backing = SingleFileMessagePackBackingStore::new(&store);
|
||||
backing.push(&envelope("at", limit))?;
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
|
||||
let at_limit = registry.create_snapshot_response(&cache, "at", None, None)?;
|
||||
let CultNetMessage::SnapshotResponse { documents, .. } = at_limit else {
|
||||
panic!("expected a snapshot response");
|
||||
};
|
||||
assert_eq!(documents.len(), 1);
|
||||
|
||||
backing.push(&envelope("past", limit + 1))?;
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let error = registry.create_snapshot_response(&cache, "past", None, None).unwrap_err();
|
||||
assert_eq!(
|
||||
error.downcast_ref::<cultcache_rs::PayloadTooDeep>(),
|
||||
Some(&cultcache_rs::PayloadTooDeep { depth: limit + 1, limit })
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_registry_replicates_typed_cultcache_state() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue