From 9998fef3a78c6b47661ccb0fa2d45603c150dcb5 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 15:51:10 +0000 Subject: [PATCH 01/12] fix(TC-1071): canonicalize CACAO recovery bytes before CID --- CHANGELOG.md | 2 + Cargo.lock | 1 + dependencies/cacao/Cargo.toml | 1 + dependencies/cacao/src/siwe_cacao.rs | 29 ++++++++- tinycloud-auth/src/authorization.rs | 9 ++- tinycloud-core/src/events/mod.rs | 91 ++++++++++++++++++++++++++++ tinycloud-verifier-wasm/src/lib.rs | 4 +- 7 files changed, 129 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e63159..996b39be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- CACAO/SIWE delegation ingestion normalizes ECDSA recovery IDs 0/1 to 27/28 before hashing and persisting the canonical DAG-CBOR bytes (TC-1071). Equivalent low-s signatures now share one delegation CID, including the CID used for revocation lookup. High-s signatures are rejected during decoding, matching the existing EIP-191 verifier and EIP-2 low-s convention; UCAN/JWS bytes remain unchanged. Existing stored delegation IDs are not rewritten. + ## [1.20.6] - 2026-10-14 A security fix (TC-1080, Part A). UCAN issuer resolution no longer dereferences network DID methods by default, so a request can't make the node fetch arbitrary or internal hosts through a crafted issuer DID (#284). diff --git a/Cargo.lock b/Cargo.lock index 60c674f2..f28f6a00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2156,6 +2156,7 @@ dependencies = [ "http 1.4.0", "ipld-core", "iri-string", + "k256", "serde", "serde_bytes", "serde_ipld_dagcbor 0.6.4", diff --git a/dependencies/cacao/Cargo.toml b/dependencies/cacao/Cargo.toml index 18a343d7..3ecbcf23 100755 --- a/dependencies/cacao/Cargo.toml +++ b/dependencies/cacao/Cargo.toml @@ -24,6 +24,7 @@ serde_ipld_dagcbor.workspace = true serde_with.workspace = true time.workspace = true http.workspace = true +k256 = "0.13" hex = { workspace = true, optional = true } serde_bytes = "0.11" serde_repr = "0.1" diff --git a/dependencies/cacao/src/siwe_cacao.rs b/dependencies/cacao/src/siwe_cacao.rs index dbf44815..82432860 100755 --- a/dependencies/cacao/src/siwe_cacao.rs +++ b/dependencies/cacao/src/siwe_cacao.rs @@ -193,10 +193,26 @@ impl AsRef<[u8]> for Signature { } } +impl Signature { + fn decode(mut bytes: [u8; 65]) -> Result { + let signature = k256::ecdsa::Signature::from_slice(&bytes[..64]) + .map_err(|_| SIWESignatureDecodeError::InvalidSignature)?; + if signature.normalize_s().is_some() { + return Err(SIWESignatureDecodeError::NonCanonicalS); + } + bytes[64] = match bytes[64] { + 0 | 1 => bytes[64] + 27, + 27 | 28 => bytes[64], + _ => return Err(SIWESignatureDecodeError::InvalidRecoveryId(bytes[64])), + }; + Ok(Self(bytes)) + } +} + impl TryFrom> for Signature { type Error = SIWESignatureDecodeError; fn try_from(s: Vec) -> Result { - Ok(Self(s.try_into().map_err(SIWESignatureDecodeError::from)?)) + Self::decode(s.try_into().map_err(SIWESignatureDecodeError::from)?) } } @@ -206,6 +222,12 @@ pub enum SIWESignatureDecodeError { InvalidLength(usize), #[error("Invalid Type, expected 'eip191', got {0}")] InvalidType(String), + #[error("Invalid ECDSA signature")] + InvalidSignature, + #[error("Non-canonical high-s ECDSA signature")] + NonCanonicalS, + #[error("Invalid recovery ID: {0}")] + InvalidRecoveryId(u8), } impl From> for SIWESignatureDecodeError { @@ -286,11 +308,12 @@ impl<'de> Deserialize<'de> for Signature { SIWESignatureDecodeError::InvalidLength(l), )); } - Ok(Signature( + Signature::decode( ds.s.try_into() .map_err(|_| SIWESignatureDecodeError::InvalidLength(l)) .map_err(serde::de::Error::custom)?, - )) + ) + .map_err(serde::de::Error::custom) } } diff --git a/tinycloud-auth/src/authorization.rs b/tinycloud-auth/src/authorization.rs index 0baee950..b03969c4 100644 --- a/tinycloud-auth/src/authorization.rs +++ b/tinycloud-auth/src/authorization.rs @@ -48,11 +48,14 @@ impl HeaderEncode for TinyCloudDelegation { s.as_bytes().to_vec(), ) } else { - // Use the imported engine and trait method + // The signature decoder canonicalizes v; serialize the parsed CACAO so + // the bytes hashed, persisted, and looked up by revocation agree. let v = URL_SAFE.decode(s)?; + let cacao: SiweCacao = serde_ipld_dagcbor::from_slice(&v)?; + let canonical = serde_ipld_dagcbor::to_vec(&cacao)?; ( - Self::Cacao(Box::new(serde_ipld_dagcbor::from_slice(&v)?)), - v, + Self::Cacao(Box::new(cacao)), + if canonical == v { v } else { canonical }, ) }) } diff --git a/tinycloud-core/src/events/mod.rs b/tinycloud-core/src/events/mod.rs index 8b3d99f1..dab7a329 100644 --- a/tinycloud-core/src/events/mod.rs +++ b/tinycloud-core/src/events/mod.rs @@ -251,3 +251,94 @@ fn hash_inv(inv_hash: &Hash, sp: &SpaceId, ops: &[Operation]) -> Result(Signature::from(signature)); + let header = URL_SAFE.encode(serde_ipld_dagcbor::to_vec(&cacao).unwrap()); + let event = Delegation::from_header_ser::(&header); + if label.starts_with("high") { + assert!( + event.is_err(), + "{label} must be rejected before CID assignment" + ); + continue; + } + let event = event.unwrap(); + let TinyCloudDelegation::Cacao(decoded) = &event.0.delegation else { + panic!("expected CACAO"); + }; + assert!(decoded.verify().await.is_ok(), "{label}"); + assert_eq!( + decoded.signature().as_ref(), + &low, + "{label} decoded signature" + ); + let cid = event.content_hash().to_cid(0x55); + if let Some(canonical_cid) = canonical_cid { + assert_eq!(cid, canonical_cid, "{label} must use the canonical CID"); + } else { + canonical_cid = Some(cid); + } + } + } +} diff --git a/tinycloud-verifier-wasm/src/lib.rs b/tinycloud-verifier-wasm/src/lib.rs index 789004d5..c907949c 100644 --- a/tinycloud-verifier-wasm/src/lib.rs +++ b/tinycloud-verifier-wasm/src/lib.rs @@ -675,8 +675,8 @@ mod tests { fn mutate_signature(signature: &str) -> String { let mut chars: Vec = signature.chars().collect(); - let last = chars.last_mut().expect("signature length"); - *last = if *last == '0' { '1' } else { '0' }; + let nibble = &mut chars[if signature.starts_with("0x") { 2 } else { 0 }]; + *nibble = if *nibble == '0' { '1' } else { '0' }; chars.into_iter().collect() } From 63b66ceebbe4ed78223b064521c867e0a3910ac0 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 16:09:45 +0000 Subject: [PATCH 02/12] fix(ci): sync standalone Cargo locks for cacao k256 (TC-1071) --- CHANGELOG.md | 1 + test/load/signer/Cargo.lock | 1 + test/m1-realdata-e2e/Cargo.lock | 1 + test/w5-policy-runtime-node-e2e/Cargo.lock | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 996b39be..7f9913e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - CACAO/SIWE delegation ingestion normalizes ECDSA recovery IDs 0/1 to 27/28 before hashing and persisting the canonical DAG-CBOR bytes (TC-1071). Equivalent low-s signatures now share one delegation CID, including the CID used for revocation lookup. High-s signatures are rejected during decoding, matching the existing EIP-191 verifier and EIP-2 low-s convention; UCAN/JWS bytes remain unchanged. Existing stored delegation IDs are not rewritten. +- Synchronize the standalone M1, W5 and load-signer Cargo locks with CACAO's `k256` dependency so `--locked` dependency resolution succeeds (TC-1071). ## [1.20.6] - 2026-10-14 diff --git a/test/load/signer/Cargo.lock b/test/load/signer/Cargo.lock index e42c7a6e..88523006 100644 --- a/test/load/signer/Cargo.lock +++ b/test/load/signer/Cargo.lock @@ -580,6 +580,7 @@ dependencies = [ "http 1.3.1", "ipld-core", "iri-string 0.7.8", + "k256 0.13.4", "serde", "serde_bytes", "serde_ipld_dagcbor", diff --git a/test/m1-realdata-e2e/Cargo.lock b/test/m1-realdata-e2e/Cargo.lock index e6a7cf42..c2df9b23 100644 --- a/test/m1-realdata-e2e/Cargo.lock +++ b/test/m1-realdata-e2e/Cargo.lock @@ -1020,6 +1020,7 @@ dependencies = [ "http 1.4.0", "ipld-core", "iri-string", + "k256", "serde", "serde_bytes", "serde_ipld_dagcbor 0.6.4", diff --git a/test/w5-policy-runtime-node-e2e/Cargo.lock b/test/w5-policy-runtime-node-e2e/Cargo.lock index 37974968..e3619958 100644 --- a/test/w5-policy-runtime-node-e2e/Cargo.lock +++ b/test/w5-policy-runtime-node-e2e/Cargo.lock @@ -1249,6 +1249,7 @@ dependencies = [ "http 1.4.0", "ipld-core", "iri-string", + "k256", "serde", "serde_bytes", "serde_ipld_dagcbor 0.6.4", @@ -8231,7 +8232,7 @@ dependencies = [ [[package]] name = "tinycloud-node" -version = "1.20.4" +version = "1.20.6" dependencies = [ "aes-gcm", "anyhow", From 2fad7176b87f6c8a0c2abdc9a607142b63154c71 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 16:37:06 +0000 Subject: [PATCH 03/12] fix: canonicalize CACAO issuers to verified SIWE identity --- dependencies/cacao/src/siwe_cacao.rs | 40 +- tinycloud-auth/src/authorization.rs | 5 +- .../tests/siwe_issuer_identity.rs | 552 ++++++++++++++++++ 3 files changed, 589 insertions(+), 8 deletions(-) create mode 100644 tinycloud-node-server/tests/siwe_issuer_identity.rs diff --git a/dependencies/cacao/src/siwe_cacao.rs b/dependencies/cacao/src/siwe_cacao.rs index 82432860..e5a4abf7 100755 --- a/dependencies/cacao/src/siwe_cacao.rs +++ b/dependencies/cacao/src/siwe_cacao.rs @@ -25,6 +25,9 @@ pub struct Payload { pub scheme: Option, #[serde_as(as = "DisplayFromStr")] pub domain: Authority, + /// Serialized as the verifier's parsed chain ID and EIP-55 address. + /// This changes only the DID representation, never the signed SIWE text. + #[serde(serialize_with = "serialize_issuer")] pub iss: UriAbsoluteString, pub statement: Option, pub aud: UriString, @@ -119,15 +122,40 @@ pub enum SIWEPayloadConversionError { InvalidDID, } +fn parse_issuer(issuer: &str) -> Result<(u64, [u8; 20]), SIWEPayloadConversionError> { + let mut parts = issuer.split(':'); + match [ + parts.next(), + parts.next(), + parts.next(), + parts.next(), + parts.next(), + parts.next(), + ] { + [Some("did"), Some("pkh"), Some("eip155"), Some(chain), Some(address), None] + if address.starts_with("0x") => + { + Ok((chain.parse()?, FromHex::from_hex(&address[2..])?)) + } + _ => Err(SIWEPayloadConversionError::InvalidDID), + } +} + +fn serialize_issuer(issuer: &UriAbsoluteString, serializer: S) -> Result +where + S: serde::Serializer, +{ + let (chain, address) = parse_issuer(issuer.as_str()).map_err(serde::ser::Error::custom)?; + serializer.collect_str(&format_args!( + "did:pkh:eip155:{chain}:0x{}", + encode_eip55(&address) + )) +} + impl TryInto for Payload { type Error = SIWEPayloadConversionError; fn try_into(self) -> Result { - let (chain_id, address) = match &self.iss.as_str().split(':').collect::>()[..] { - &["did", "pkh", "eip155", c, h] if h.get(..2) == Some("0x") => { - (c.parse()?, FromHex::from_hex(&h[2..])?) - } - _ => return Err(Self::Error::InvalidDID), - }; + let (chain_id, address) = parse_issuer(self.iss.as_str())?; Ok(Message { scheme: self.scheme, domain: self.domain, diff --git a/tinycloud-auth/src/authorization.rs b/tinycloud-auth/src/authorization.rs index b03969c4..ab3a419c 100644 --- a/tinycloud-auth/src/authorization.rs +++ b/tinycloud-auth/src/authorization.rs @@ -48,8 +48,9 @@ impl HeaderEncode for TinyCloudDelegation { s.as_bytes().to_vec(), ) } else { - // The signature decoder canonicalizes v; serialize the parsed CACAO so - // the bytes hashed, persisted, and looked up by revocation agree. + // Signature decoding canonicalizes v; payload serialization uses the + // verifier's parsed issuer. Hash, persistence and revocation lookup + // must all use these same canonical DAG-CBOR bytes. let v = URL_SAFE.decode(s)?; let cacao: SiweCacao = serde_ipld_dagcbor::from_slice(&v)?; let canonical = serde_ipld_dagcbor::to_vec(&cacao)?; diff --git a/tinycloud-node-server/tests/siwe_issuer_identity.rs b/tinycloud-node-server/tests/siwe_issuer_identity.rs new file mode 100644 index 00000000..45a19431 --- /dev/null +++ b/tinycloud-node-server/tests/siwe_issuer_identity.rs @@ -0,0 +1,552 @@ +//! TC-1071: changing only the CACAO issuer's address casing must not resurrect +//! a revoked, valid SIWE grant. All requests use throwaway keys and the real app. +use std::collections::HashMap; + +use anyhow::{ensure, Result}; +use k256::ecdsa::SigningKey; +use rocket::{ + figment::providers::Serialized, + http::{ContentType, Header, Status}, + local::asynchronous::{Client, LocalRequest}, +}; +use serde_json::{json, Value}; +use sha3::{Digest, Keccak256}; +use tinycloud_auth::{ + authorization::{ + make_invocation_from_uris, Cid, HeaderEncode, InvocationOptions, TinyCloudDelegation, + TinyCloudRevocation, + }, + cacaos::{ + siwe::{encode_eip55, Message}, + siwe_cacao::Signature, + }, + ipld_core::ipld::Ipld, + resource::{Path, Service, SpaceId}, +}; +use tinycloud_sdk_wasm::{ + host::{generate_host_siwe_message, siwe_to_delegation_headers, HostConfig, SignedMessage}, + session::{complete_session_setup, prepare_session, Session, SessionConfig, SignedSession}, +}; + +struct Wallet { + key: SigningKey, + address: [u8; 20], + space: SpaceId, +} + +impl Wallet { + fn new() -> Result { + let key = SigningKey::from_bytes(&[37; 32].into())?; + let public = key.verifying_key().to_encoded_point(false); + let digest = Keccak256::digest(&public.as_bytes()[1..]); + let address = digest[12..].try_into()?; + let did = format!("did:pkh:eip155:1:0x{}", encode_eip55(&address)); + Ok(Self { + key, + address, + space: SpaceId::new(did.parse()?, "issuer-case".parse()?), + }) + } + + fn sign(&self, message: &Message) -> Result { + let (signature, recovery) = self.key.sign_prehash_recoverable(&message.eip191_hash()?)?; + let mut bytes = [0; 65]; + bytes[..64].copy_from_slice(signature.to_bytes().as_ref()); + bytes[64] = u8::from(recovery) + 27; + Ok(Signature::from(bytes)) + } + + fn session(&self) -> Result { + let now = time::OffsetDateTime::now_utc(); + let prepared = prepare_session(SessionConfig { + abilities: HashMap::from([( + "kv".parse()?, + HashMap::from([("".parse()?, vec!["tinycloud.kv/list".parse()?])]), + )]), + space_abilities: None, + raw_abilities: HashMap::new(), + address: self.address, + chain_id: 1, + domain: "localhost".parse()?, + issued_at: now.into(), + space_id: self.space.clone(), + additional_spaces: None, + not_before: None, + expiration_time: (now + time::Duration::hours(1)).into(), + parents: None, + jwk: None, + delegate_uri: None, + nonce: None, + invocation_ttl_secs: None, + })?; + let signature = self.sign(&prepared.siwe)?; + Ok(complete_session_setup(SignedSession { + session: prepared, + signature, + })?) + } +} + +fn authorization(headers: &impl serde::Serialize) -> Result { + Ok(serde_json::to_value(headers)?["Authorization"] + .as_str() + .unwrap() + .to_owned()) +} + +fn wire(value: &Ipld) -> Result { + Ok(base64::encode_config( + serde_ipld_dagcbor::to_vec(value)?, + base64::URL_SAFE, + )) +} + +fn wire_value(header: &str) -> Result { + Ok(serde_ipld_dagcbor::from_slice(&base64::decode_config( + header, + base64::URL_SAFE, + )?)?) +} + +fn payload(value: &mut Ipld) -> &mut std::collections::BTreeMap { + let Ipld::Map(cacao) = value else { + panic!("CACAO map") + }; + let Ipld::Map(payload) = cacao.get_mut("p").unwrap() else { + panic!("payload map") + }; + payload +} + +async fn dispatch(request: LocalRequest<'_>) -> (Status, String) { + let response = request.dispatch().await; + ( + response.status(), + response.into_string().await.unwrap_or_default(), + ) +} + +async fn expect_ok(label: &str, request: LocalRequest<'_>) -> Result { + let (status, body) = dispatch(request).await; + ensure!(status == Status::Ok, "{label}: {status} {body}"); + Ok(body) +} + +async fn client() -> Result<(Client, tempfile::TempDir)> { + let dir = tempfile::tempdir()?; + let mut config = tinycloud::config::Config::default(); + config.storage.datadir = dir.path().into(); + config.storage.database = Some("sqlite::memory:".into()); + config.keys = serde_json::from_value( + json!({"type":"static", "secret":base64::encode_config([43; 32], base64::URL_SAFE_NO_PAD)}), + )?; + let figment = rocket::figment::Figment::from(rocket::Config { + address: "127.0.0.1".parse()?, + log_level: rocket::config::LogLevel::Off, + ..rocket::Config::default() + }) + .merge(Serialized::defaults(&config)); + let rocket = tinycloud::app_with_control(&figment, &config, None).await?; + Ok((Client::tracked(rocket).await?, dir)) +} + +async fn bootstrap(client: &Client, wallet: &Wallet) -> Result<()> { + let peer = expect_ok( + "peer", + client.get(format!("/peer/generate/{}", wallet.space)), + ) + .await?; + let message = generate_host_siwe_message(HostConfig { + address: wallet.address, + chain_id: 1, + domain: "localhost".parse()?, + issued_at: time::OffsetDateTime::now_utc().into(), + space_id: wallet.space.clone(), + peer_id: peer, + })?; + let signature = wallet.sign(&message)?; + let header = authorization(&siwe_to_delegation_headers(SignedMessage { + siwe: message, + signature, + }))?; + expect_ok( + "host grant", + client + .post("/delegate") + .header(Header::new("Authorization", header)), + ) + .await?; + Ok(()) +} + +fn list_header(session: &Session, cid: Cid) -> Result { + let mut invocation = session.clone(); + invocation.delegation_cid = cid; + Ok(invocation + .invoke( + [( + "kv".parse::()?, + "".parse::()?, + None, + None, + ["tinycloud.kv/list".parse()?], + )], + None, + )? + .encode()?) +} + +async fn status(client: &Client, session: &Session, cid: &str) -> Result { + let invocation = make_invocation_from_uris( + [( + format!("urn:cid:{cid}").parse()?, + ["tinycloud.delegation/status".parse()?], + )], + &cid.parse()?, + &session.jwk, + &session.verification_method, + (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).unix_timestamp() as f64, + InvocationOptions { + proof: Some(vec![]), + ..InvocationOptions::default() + }, + )? + .encode()?; + let body = expect_ok( + "status", + client + .post("/delegation/status") + .header(Header::new("Authorization", invocation)) + .header(ContentType::JSON) + .body(json!({"cid":cid}).to_string()), + ) + .await?; + Ok(serde_json::from_str(&body)?) +} + +#[tokio::test] +async fn issuer_case_reimport_cannot_resurrect_revoked_siwe_grant() -> Result<()> { + let (client, _dir) = client().await?; + let wallet = Wallet::new()?; + bootstrap(&client, &wallet).await?; + let session = wallet.session()?; + let canonical = authorization(&session.delegation_header)?; + let mut variant = wire_value(&canonical)?; + let Ipld::String(issuer) = payload(&mut variant).get("iss").unwrap() else { + panic!("issuer") + }; + let issuer = issuer.clone(); + let (prefix, address) = issuer.rsplit_once(':').unwrap(); + let lower = format!("{prefix}:{}", address.to_ascii_lowercase()); + assert_ne!( + issuer, lower, + "fixture must have a mixed-case EIP55 address" + ); + payload(&mut variant).insert("iss".into(), Ipld::String(lower)); + let variant = wire(&variant)?; + + // Same signature, same personal_sign bytes; no resigning the alias. + let (TinyCloudDelegation::Cacao(original), _) = TinyCloudDelegation::decode(&canonical)? else { + panic!("CACAO") + }; + let (TinyCloudDelegation::Cacao(alias), _) = TinyCloudDelegation::decode(&variant)? else { + panic!("CACAO") + }; + let message: Message = original.payload().clone().try_into()?; + let alias_message: Message = alias.payload().clone().try_into()?; + assert_eq!(message.eip191_bytes()?, alias_message.eip191_bytes()?); + assert_eq!(original.signature(), alias.signature()); + original.verify().await?; + alias.verify().await?; + + let grant: Value = serde_json::from_str( + &expect_ok( + "canonical grant", + client + .post("/delegate") + .header(Header::new("Authorization", canonical)), + ) + .await?, + )?; + let cid = grant["cid"].as_str().unwrap(); + assert_eq!(status(&client, &session, cid).await?["status"], "active"); + let before = expect_ok( + "signed kv/list before revoke", + client.post("/invoke").header(Header::new( + "Authorization", + list_header(&session, cid.parse()?)?, + )), + ) + .await?; + println!("canonical={cid} status=active signed kv/list=200 body={before}"); + + let mut revocation_message = message; + revocation_message.uri = format!("ucan:{cid}").parse()?; + revocation_message.statement = None; + revocation_message.resources.clear(); + let signature = wallet.sign(&revocation_message)?; + let revocation = TinyCloudRevocation::Cacao(Box::new( + tinycloud_auth::cacaos::siwe_cacao::Payload::from(revocation_message) + .sign::(signature), + )); + expect_ok( + "signed revoke", + client + .post("/revoke") + .header(Header::new("Authorization", revocation.encode()?)), + ) + .await?; + assert_eq!(status(&client, &session, cid).await?["status"], "revoked"); + let (revoked_status, revoked_body) = dispatch(client.post("/invoke").header(Header::new( + "Authorization", + list_header(&session, cid.parse()?)?, + ))) + .await; + assert_eq!( + revoked_status, + Status::Unauthorized, + "canonical after revoke: {revoked_body}" + ); + assert!( + revoked_body.contains("delegation-revoked"), + "{revoked_body}" + ); + + let imported: Value = serde_json::from_str( + &expect_ok( + "case-only reimport", + client + .post("/delegate") + .header(Header::new("Authorization", variant)), + ) + .await?, + )?; + let alias_cid = imported["cid"].as_str().unwrap(); + let alias_status = status(&client, &session, alias_cid).await?; + let (invoke_status, invoke_body) = dispatch(client.post("/invoke").header(Header::new( + "Authorization", + list_header(&session, alias_cid.parse()?)?, + ))) + .await; + println!( + "reimport={alias_cid} status={} signed kv/list={invoke_status} body={invoke_body}", + alias_status["status"] + ); + assert_eq!( + invoke_status, + Status::Unauthorized, + "case-only revoked grant must not authorize kv/list: {invoke_body}" + ); + assert!(invoke_body.contains("delegation-revoked"), "{invoke_body}"); + assert_eq!(alias_status["status"], "revoked"); + assert_eq!(alias_status["revoked"], true); + assert_eq!( + alias_cid, cid, + "equivalent signed messages must have one CID" + ); + Ok(()) +} + +fn signature_bytes(value: &mut Ipld) -> &mut Vec { + let Ipld::Map(cacao) = value else { + panic!("CACAO map") + }; + let Ipld::Map(signature) = cacao.get_mut("s").unwrap() else { + panic!("signature map") + }; + let Ipld::Bytes(bytes) = signature.get_mut("s").unwrap() else { + panic!("signature bytes") + }; + bytes +} + +#[tokio::test] +async fn issuer_representations_share_canonical_bytes_without_changing_signed_text() -> Result<()> { + let wallet = Wallet::new()?; + let session = wallet.session()?; + let header = authorization(&session.delegation_header)?; + let (TinyCloudDelegation::Cacao(original), canonical) = TinyCloudDelegation::decode(&header)? + else { + panic!("CACAO") + }; + let message: Message = original.payload().clone().try_into()?; + let signed_bytes = message.eip191_bytes()?; + let canonical_cid = tinycloud_core::hash::hash(&canonical).to_cid(0x55); + let checksum = encode_eip55(&wallet.address); + let cases = [ + checksum.to_ascii_lowercase(), + checksum.to_ascii_uppercase(), + checksum + .chars() + .map(|c| { + if c.is_ascii_uppercase() { + c.to_ascii_lowercase() + } else { + c.to_ascii_uppercase() + } + }) + .collect(), + ]; + for chain in ["1", "0001", "+1"] { + for address in &cases { + for recovery_offset in [0, 27] { + let mut variant = wire_value(&header)?; + payload(&mut variant).insert( + "iss".into(), + Ipld::String(format!("did:pkh:eip155:{chain}:0x{address}")), + ); + signature_bytes(&mut variant)[64] -= recovery_offset; + let (TinyCloudDelegation::Cacao(alias), bytes) = + TinyCloudDelegation::decode(&wire(&variant)?)? + else { + panic!("CACAO") + }; + let alias_message: Message = alias.payload().clone().try_into()?; + assert_eq!( + alias_message.eip191_bytes()?, + signed_bytes, + "{chain} {address}" + ); + alias.verify().await?; + assert_eq!(alias.signature(), original.signature(), "v normalization"); + assert_eq!( + bytes, canonical, + "{chain} {address} v-offset={recovery_offset}" + ); + assert_eq!( + tinycloud_core::hash::hash(&bytes).to_cid(0x55), + canonical_cid + ); + assert_eq!( + TinyCloudDelegation::Cacao(alias).encode()?, + header, + "new wire encoding" + ); + } + } + } + + Ok(()) +} + +#[tokio::test] +async fn issuer_canonicalization_does_not_accept_invalid_signatures_or_changed_signed_fields( +) -> Result<()> { + let (client, _dir) = client().await?; + let wallet = Wallet::new()?; + bootstrap(&client, &wallet).await?; + let session = wallet.session()?; + let header = authorization(&session.delegation_header)?; + let (TinyCloudDelegation::Cacao(original), bytes) = TinyCloudDelegation::decode(&header)? + else { + panic!("CACAO") + }; + let message: Message = original.payload().clone().try_into()?; + + let mutations = [ + ("nonce", Ipld::String("ChangedNonce1071".into())), + ( + "iss", + Ipld::String(format!( + "did:pkh:eip155:2:0x{}", + encode_eip55(&wallet.address) + )), + ), + ( + "iss", + Ipld::String("did:pkh:eip155:1:0x1111111111111111111111111111111111111111".into()), + ), + ("domain", Ipld::String("other.example".into())), + ( + "iat", + Ipld::String(message.issued_at.to_string().replace('Z', "+00:00")), + ), + ("aud", Ipld::String("did:key:z6MkiAlternateAudience".into())), + ]; + for (field, value) in mutations { + let mut variant = wire_value(&header)?; + payload(&mut variant).insert(field.into(), value); + let mutated = wire(&variant)?; + let (TinyCloudDelegation::Cacao(cacao), _) = TinyCloudDelegation::decode(&mutated)? else { + panic!("CACAO") + }; + assert!( + cacao.verify().await.is_err(), + "changed {field} must fail signature verification" + ); + let (status, body) = dispatch( + client + .post("/delegate") + .header(Header::new("Authorization", mutated)), + ) + .await; + assert_eq!(status, Status::Unauthorized, "changed {field}: {body}"); + } + + for failure in ["high-s", "zero-r", "zero-s", "invalid-v", "wrong-signer"] { + let mut variant = wire_value(&header)?; + let signature = signature_bytes(&mut variant); + match failure { + "high-s" => { + // secp256k1 n-s, with the matching recovery parity toggle. + let order = hex::decode( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + )?; + let mut borrow = 0i16; + for i in (0..32).rev() { + let difference = i16::from(order[i]) - i16::from(signature[32 + i]) - borrow; + signature[32 + i] = difference as u8; + borrow = i16::from(difference < 0); + } + signature[64] = 27 + ((signature[64] - 27) ^ 1); + } + "zero-r" => signature[..32].fill(0), + "zero-s" => signature[32..64].fill(0), + "invalid-v" => signature[64] = 29, + "wrong-signer" => signature[0] ^= 1, + _ => unreachable!(), + } + let invalid = wire(&variant)?; + let decoded = TinyCloudDelegation::decode(&invalid); + if failure == "wrong-signer" { + let (TinyCloudDelegation::Cacao(cacao), _) = decoded? else { + panic!("CACAO") + }; + assert!(cacao.verify().await.is_err()); + } else { + assert!(decoded.is_err(), "{failure} must be rejected at decode"); + } + let (status, body) = dispatch( + client + .post("/delegate") + .header(Header::new("Authorization", invalid)), + ) + .await; + assert_eq!(status, Status::Unauthorized, "{failure}: {body}"); + } + + // An independently signed message remains a different grant, not an alias. + let mut distinct_message = message; + distinct_message.nonce = "DifferentValidNonce1071".into(); + let signature = wallet.sign(&distinct_message)?; + let distinct = tinycloud_auth::cacaos::siwe_cacao::Payload::from(distinct_message) + .sign::(signature); + distinct.verify().await?; + let distinct = TinyCloudDelegation::Cacao(Box::new(distinct)).encode()?; + let (_, distinct_bytes) = TinyCloudDelegation::decode(&distinct)?; + assert_ne!( + tinycloud_core::hash::hash(&distinct_bytes), + tinycloud_core::hash::hash(&bytes) + ); + expect_ok( + "independently signed distinct grant", + client + .post("/delegate") + .header(Header::new("Authorization", distinct)), + ) + .await?; + println!( + "invalid signatures and changed signed fields rejected; distinct signed grant accepted" + ); + Ok(()) +} From 3464a04378ba40c90fee1a0184ed1b7c56583eb0 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 16:35:36 +0000 Subject: [PATCH 04/12] fix(sdk): canonicalize wallet signatures before session CID calculation --- .../tests/sdk_session_cid.rs | 298 ++++++++++++++++++ tinycloud-sdk-rs/src/serde_siwe.rs | 11 +- tinycloud-sdk-wasm/src/session.rs | 21 +- 3 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 tinycloud-node-server/tests/sdk_session_cid.rs diff --git a/tinycloud-node-server/tests/sdk_session_cid.rs b/tinycloud-node-server/tests/sdk_session_cid.rs new file mode 100644 index 00000000..000a1a13 --- /dev/null +++ b/tinycloud-node-server/tests/sdk_session_cid.rs @@ -0,0 +1,298 @@ +//! TC-1071: wallet signature import must address the delegation the node stores. +//! Drive the SDK's JSON and Rust entrypoints through the real node, not a hash-only oracle. + +use anyhow::{anyhow, ensure, Context, Result}; +use k256::ecdsa::SigningKey; +use rocket::{ + figment::{providers::Serialized, Figment}, + http::{ContentType, Header, Status}, + local::asynchronous::Client, +}; +use serde_json::{json, Value}; +use sha3::{Digest, Keccak256}; +use tinycloud_auth::{ + cacaos::{ + siwe::{encode_eip55, Message}, + siwe_cacao::Signature, + }, + resource::{Path, Service, SpaceId}, + siwe_recap::Ability, + ssi::jwk::JWK, +}; +use tinycloud_sdk_wasm::{ + host::{generate_host_siwe_message, siwe_to_delegation_headers, SignedMessage}, + session::{complete_session_setup, prepare_session, PreparedSession, Session, SignedSession}, +}; + +async fn node() -> Result<(Client, tempfile::TempDir)> { + let dir = tempfile::tempdir()?; + let mut config = tinycloud::config::Config::default(); + config.storage.datadir = dir.path().into(); + config.storage.database = Some("sqlite::memory:".into()); + config.keys = serde_json::from_value(json!({ + "type": "static", + "secret": base64::encode_config([107; 32], base64::URL_SAFE_NO_PAD), + }))?; + let figment = Figment::from(rocket::Config { + log_level: rocket::config::LogLevel::Off, + ..rocket::Config::default() + }) + .merge(Serialized::defaults(&config)); + Ok((Client::tracked(tinycloud::app(&figment).await?).await?, dir)) +} + +fn wallet() -> Result<(SigningKey, [u8; 20], SpaceId)> { + let key = SigningKey::from_bytes(&[107; 32].into())?; + let public = key.verifying_key().to_encoded_point(false); + let digest = Keccak256::digest(&public.as_bytes()[1..]); + let address = digest[12..].try_into()?; + let space = format!( + "tinycloud:pkh:eip155:1:0x{}:sdk-session-cid", + encode_eip55(&address), + ) + .parse()?; + Ok((key, address, space)) +} + +fn sign(key: &SigningKey, message: &Message) -> Result<[u8; 65]> { + let (signature, recovery) = + key.sign_prehash_recoverable(&message.eip191_hash().map_err(|e| anyhow!("{e}"))?)?; + ensure!(signature.normalize_s().is_none(), "fixture must be low-s"); + let mut bytes = [0; 65]; + bytes[..64].copy_from_slice(&signature.to_bytes()); + bytes[64] = recovery.to_byte(); + Ok(bytes) +} + +fn prepared(address: &[u8; 20], space: &SpaceId) -> Result { + let now = time::OffsetDateTime::now_utc(); + // Throwaway, deterministic session key; no credentials or network DID resolution. + let key = ed25519_dalek::SigningKey::from_bytes(&[108; 32]); + let jwk: JWK = serde_json::from_value(json!({ + "kty": "OKP", "crv": "Ed25519", + "d": base64::encode_config(key.to_bytes(), base64::URL_SAFE_NO_PAD), + "x": base64::encode_config(key.verifying_key().to_bytes(), base64::URL_SAFE_NO_PAD), + }))?; + Ok(prepare_session(serde_json::from_value(json!({ + "abilities": {"kv": {"": ["tinycloud.kv/put", "tinycloud.kv/get"]}}, + "address": format!("0x{}", encode_eip55(address)), + "chainId": 1, + "domain": "localhost", + "issuedAt": (now - time::Duration::minutes(1)).format(&time::format_description::well_known::Rfc3339)?, + "expirationTime": (now + time::Duration::hours(1)).format(&time::format_description::well_known::Rfc3339)?, + "spaceId": space, + "jwk": jwk, + "nonce": "tc1071session", + }))?)?) +} + +fn json_signed(prepared: &PreparedSession, signature: &str) -> Result { + let mut signed = serde_json::to_value(prepared)?; + signed["signature"] = signature.into(); + Ok(signed) +} + +fn authorization(headers: &impl serde::Serialize) -> Result { + serde_json::to_value(headers)?["Authorization"] + .as_str() + .map(str::to_owned) + .context("SDK delegation Authorization header") +} + +async fn register(client: &Client, headers: &impl serde::Serialize) -> Result { + let response = client + .post("/delegate") + .header(Header::new("Authorization", authorization(headers)?)) + .dispatch() + .await; + let status = response.status(); + let body = response.into_string().await.unwrap_or_default(); + ensure!(status == Status::Ok, "delegate: {status} {body}"); + Ok(serde_json::from_str(&body)?) +} + +async fn invoke( + client: &Client, + session: &Session, + path: &str, + action: &str, + body: &str, +) -> Result<(Status, String)> { + let resource = session.space_id.clone().to_resource( + "kv".parse::()?, + Some(path.parse::()?), + None, + None, + ); + let invocation = session.invoke_any([(resource, [action.parse::()?])], None)?; + let response = client + .post("/invoke") + .header(Header::new("Authorization", invocation.encode()?)) + .header(ContentType::Plain) + .body(body) + .dispatch() + .await; + Ok(( + response.status(), + response.into_string().await.unwrap_or_default(), + )) +} + +#[tokio::test] +async fn wallet_recovery_forms_register_and_invoke() -> Result<()> { + let (client, _dir) = node().await?; + let (wallet, address, space) = wallet()?; + let peer = client + .get(format!("/peer/generate/{space}")) + .dispatch() + .await; + ensure!(peer.status() == Status::Ok, "peer generation"); + let host = generate_host_siwe_message(serde_json::from_value(json!({ + "address": format!("0x{}", encode_eip55(&address)), + "chainId": 1, "domain": "localhost", + "issuedAt": (time::OffsetDateTime::now_utc() - time::Duration::minutes(1)).format(&time::format_description::well_known::Rfc3339)?, + "spaceId": space, + "peerId": peer.into_string().await.context("peer DID")?, + }))?)?; + let host_signature = sign(&wallet, &host)?; + let host: SignedMessage = serde_json::from_value(json!({ + "siwe": host.to_string(), "signature": format!("0x{}", hex::encode(host_signature)), + }))?; + register(&client, &siwe_to_delegation_headers(host)).await?; + + let mut failures = Vec::new(); + for recovery in [0, 1] { + let mut prepared = prepared(&address, &space)?; + let mut signature = None; + // Select a genuine signature for each recovery parity, never alter its parity. + for nonce in 0..256 { + prepared.siwe.nonce = format!("tc1071{recovery}{nonce:03}"); + let candidate = sign(&wallet, &prepared.siwe)?; + if candidate[64] == recovery { + signature = Some(candidate); + break; + } + } + let signature = signature.context("signature with requested recovery parity")?; + for v in [recovery + 27, recovery] { + let mut bytes = signature; + bytes[64] = v; + for import in ["json", "rust"] { + let session = if import == "json" { + let prefix = if v >= 27 { "" } else { "0x" }; + let signed = + json_signed(&prepared, &format!("{prefix}{}", hex::encode(bytes)))?; + complete_session_setup(serde_json::from_value::(signed)?)? + } else { + complete_session_setup(SignedSession { + session: prepared.clone(), + signature: Signature::from(bytes), + })? + }; + let registered = register(&client, &session.delegation_header).await?; + let path = format!("tc1071/{import}/{v}"); + let value = format!("sdk-session-{import}-v{v}"); + let (status, body) = + invoke(&client, &session, &path, "tinycloud.kv/put", &value).await?; + println!( + "{import} v={v}: SDK CID={} node CID={} invoke={status} {body}", + session.delegation_cid, + registered["cid"].as_str().unwrap_or_default() + ); + if status != Status::Ok { + failures.push(format!("{import} v={v}: invoke={status} {body}")); + continue; + } + ensure!( + registered["cid"] == session.delegation_cid.to_string(), + "{import} v={v}: registered CID must match SDK proof" + ); + let (status, body) = + invoke(&client, &session, &path, "tinycloud.kv/get", "").await?; + println!("{import} v={v}: get={status} value={body:?}"); + ensure!( + status == Status::Ok && body == value, + "{import} v={v}: get={status} {body}" + ); + } + } + } + ensure!( + failures.is_empty(), + "SDK invocation failures:\n{}", + failures.join("\n") + ); + Ok(()) +} + +#[test] +fn wallet_import_and_rust_setup_reject_noncanonical_signatures() -> Result<()> { + let (wallet, address, space) = wallet()?; + let prepared = prepared(&address, &space)?; + let bytes = sign(&wallet, &prepared.siwe)?; + let signature = k256::ecdsa::Signature::from_slice(&bytes[..64])?; + let (r, s) = signature.split_scalars(); + let high_s = k256::ecdsa::Signature::from_scalars(r.to_bytes(), (-s).to_bytes())?; + let mut high_s_bytes = bytes; + high_s_bytes[..64].copy_from_slice(&high_s.to_bytes()); + high_s_bytes[64] ^= 1; + let mut cases = vec![("high-s".to_owned(), high_s_bytes.to_vec())]; + for v in [2, 26, 29, 35, 255] { + let mut invalid = bytes; + invalid[64] = v; + cases.push((format!("invalid-v-{v}"), invalid.to_vec())); + } + for range in [0..32, 32..64] { + for value in [0, 255] { + let mut invalid = bytes; + invalid[range.clone()].fill(value); + cases.push(( + format!("invalid-scalar-{}-{value}", range.start), + invalid.to_vec(), + )); + } + } + cases.push(("short".into(), bytes[..64].to_vec())); + cases.push(("long".into(), [bytes.as_slice(), &[0]].concat())); + for (label, bytes) in cases { + let node_error = Signature::try_from(bytes.clone()) + .expect_err("node decoder rejects fixture") + .to_string(); + for prefix in ["", "0x"] { + let signed = json_signed(&prepared, &format!("{prefix}{}", hex::encode(&bytes)))?; + let error = serde_json::from_value::(signed) + .err() + .with_context(|| format!("SDK accepted {label}"))?; + ensure!( + error.to_string().contains(&node_error), + "{label}: SDK={error}; node={node_error}" + ); + let host = json!({"siwe": prepared.siwe.to_string(), "signature": format!("{prefix}{}", hex::encode(&bytes))}); + let error = serde_json::from_value::(host) + .err() + .with_context(|| format!("host SDK accepted {label}"))?; + ensure!( + error.to_string().contains(&node_error), + "host {label}: SDK={error}; node={node_error}" + ); + } + if let Ok(bytes) = <[u8; 65]>::try_from(bytes) { + let error = complete_session_setup(SignedSession { + session: prepared.clone(), + signature: Signature::from(bytes), + }) + .expect_err("Rust setup must validate before hashing"); + ensure!( + error.to_string().contains(&node_error), + "Rust {label}: SDK={error}; node={node_error}" + ); + } + } + for signature in ["0xnothex", "0", "0x"] { + ensure!( + serde_json::from_value::(json_signed(&prepared, signature)?).is_err(), + "accepted malformed hex {signature}" + ); + } + Ok(()) +} diff --git a/tinycloud-sdk-rs/src/serde_siwe.rs b/tinycloud-sdk-rs/src/serde_siwe.rs index 83092a4d..41e7dba5 100644 --- a/tinycloud-sdk-rs/src/serde_siwe.rs +++ b/tinycloud-sdk-rs/src/serde_siwe.rs @@ -39,13 +39,18 @@ pub mod signature { format!("0x{}", addr.deref().encode_hex::()).serialize(s) } + /// Decode a 65-byte hex wallet signature, with an optional `0x` prefix. + /// + /// Uses the node's CACAO decoder: low-s signatures with recovery IDs + /// `0/1` or `27/28` are accepted and stored as `27/28`; high-s signatures, + /// invalid scalars, lengths, and recovery IDs are rejected. pub fn deserialize<'de, D>(d: D) -> Result where D: Deserializer<'de>, { let sig = Cow::<'_, str>::deserialize(d)?; - <[u8; 65]>::from_hex(sig.strip_prefix("0x").unwrap_or(&sig)) - .map(Into::into) - .map_err(|e| D::Error::custom(format!("failed to parse SIWE signature: {e}"))) + let bytes = >::from_hex(sig.strip_prefix("0x").unwrap_or(&sig)) + .map_err(|e| D::Error::custom(format!("failed to parse SIWE signature: {e}")))?; + Signature::try_from(bytes).map_err(D::Error::custom) } } diff --git a/tinycloud-sdk-wasm/src/session.rs b/tinycloud-sdk-wasm/src/session.rs index 608a637b..9b2811f2 100644 --- a/tinycloud-sdk-wasm/src/session.rs +++ b/tinycloud-sdk-wasm/src/session.rs @@ -118,6 +118,8 @@ pub struct PreparedSession { pub struct SignedSession { #[serde(flatten)] pub session: PreparedSession, + /// EIP-191 wallet signature. Hex import validates its encoding and + /// canonicalizes recovery IDs `0/1` to `27/28`. #[serde(with = "tinycloud_sdk_rs::serde_siwe::signature")] pub signature: Signature, } @@ -696,12 +698,18 @@ pub fn prepare_session(config: SessionConfig) -> Result }) } +/// Finish session setup using a validated, canonical wallet signature encoding. +/// +/// Low-s signatures with recovery IDs `0/1` and `27/28` produce the same +/// delegation header and CID registered by the node. Invalid signature scalars, +/// high-s values, and unsupported recovery IDs fail before CID calculation, +/// including when Rust callers construct [`SignedSession`] directly. +/// +/// The node still verifies the signature against the SIWE message and checks +/// the delegation's authority; setup only validates the signature encoding. pub fn complete_session_setup(signed_session: SignedSession) -> Result { - let delegation = SiweCacao::new( - signed_session.session.siwe.into(), - signed_session.signature, - SiweHeader, - ); + let signature = Signature::try_from(signed_session.signature.as_ref().to_vec())?; + let delegation = SiweCacao::new(signed_session.session.siwe.into(), signature, SiweHeader); let serialised = serde_ipld_dagcbor::to_vec(&delegation)?; let hash = Code::Blake3_256.digest(&serialised); // Use raw codec 0x55 to match server behavior @@ -729,6 +737,9 @@ pub enum Error { UnableToGenerateDID(#[from] tinycloud_auth::ssi::dids::GenerateError), #[error("unable to generate the SIWE message to start the session: {0}")] UnableToGenerateSIWEMessage(String), + /// The wallet signature encoding is not accepted by the node's CACAO decoder. + #[error("invalid SIWE signature: {0}")] + InvalidSIWESignature(#[from] tinycloud_auth::cacaos::siwe_cacao::SIWESignatureDecodeError), #[error("unable to generate the CID: {0}")] UnableToGenerateCid(#[from] EncodeError), } From 2f1faec0a13b1b2a5aba54a526fb6cb0f7afd030 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 19:18:30 +0000 Subject: [PATCH 05/12] fix(TC-1071): preserve historical CACAO revocation identities --- CHANGELOG.md | 2 + docs/delegation-flow.md | 35 ++ tinycloud-core/src/auth_graph.rs | 94 +++- tinycloud-core/src/db.rs | 238 ++++----- tinycloud-core/src/hash.rs | 3 +- .../m20261014_000000_delegation_cid_alias.rs | 90 ++++ tinycloud-core/src/migrations/mod.rs | 5 +- tinycloud-core/src/models/delegation.rs | 82 ++- .../src/models/delegation_cid_alias.rs | 324 ++++++++++++ tinycloud-core/src/models/mod.rs | 1 + tinycloud-core/src/models/revocation.rs | 104 +++- .../fixtures/tc1071/generated/active.sqlite | Bin 0 -> 516096 bytes .../tc1071/generated/encrypted-active.sqlite | Bin 0 -> 516096 bytes .../fixtures/tc1071/generated/manifest.json | 342 +++++++++++++ .../tc1071/generated/revoked-v0.sqlite | Bin 0 -> 528384 bytes .../tc1071/generated/revoked-v27.sqlite | Bin 0 -> 528384 bytes .../tc1071/generated/single-v0.sqlite | Bin 0 -> 487424 bytes .../tests/fixtures/tc1071/generator.rs | 113 ++++ .../tests/fixtures/tc1071/support.rs | 331 ++++++++++++ .../tests/fixtures/tc1071/upgrade_checks.rs | 396 ++++++++++++++ .../tests/historical_cacao_revocation.rs | 484 ++++++++++++++++++ tinycloud-node-server/src/auth_guards.rs | 120 ++++- tinycloud-node-server/src/lib.rs | 4 +- tinycloud-node-server/src/share_v2.rs | 44 +- .../src/webhook_dispatcher.rs | 18 +- 25 files changed, 2625 insertions(+), 205 deletions(-) create mode 100644 tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs create mode 100644 tinycloud-core/src/models/delegation_cid_alias.rs create mode 100644 tinycloud-core/tests/fixtures/tc1071/generated/active.sqlite create mode 100644 tinycloud-core/tests/fixtures/tc1071/generated/encrypted-active.sqlite create mode 100644 tinycloud-core/tests/fixtures/tc1071/generated/manifest.json create mode 100644 tinycloud-core/tests/fixtures/tc1071/generated/revoked-v0.sqlite create mode 100644 tinycloud-core/tests/fixtures/tc1071/generated/revoked-v27.sqlite create mode 100644 tinycloud-core/tests/fixtures/tc1071/generated/single-v0.sqlite create mode 100644 tinycloud-core/tests/fixtures/tc1071/generator.rs create mode 100755 tinycloud-core/tests/fixtures/tc1071/support.rs create mode 100644 tinycloud-core/tests/fixtures/tc1071/upgrade_checks.rs create mode 100644 tinycloud-core/tests/historical_cacao_revocation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9913e1..c970a561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - CACAO/SIWE delegation ingestion normalizes ECDSA recovery IDs 0/1 to 27/28 before hashing and persisting the canonical DAG-CBOR bytes (TC-1071). Equivalent low-s signatures now share one delegation CID, including the CID used for revocation lookup. High-s signatures are rejected during decoding, matching the existing EIP-191 verifier and EIP-2 low-s convention; UCAN/JWS bytes remain unchanged. Existing stored delegation IDs are not rewritten. - Synchronize the standalone M1, W5 and load-signer Cargo locks with CACAO's `k256` dependency so `--locked` dependency resolution succeeds (TC-1071). +- Historical CACAO revocations now apply to every stored encoding of the same delegation, in both directions (TC-1071). A persisted CID-alias index derives identities from current ingestion's canonical bytes, including recovery-ID, issuer-address/chain, and accepted CBOR representation normalization. Startup backfills aliases transactionally after loading the column encryption key and fails closed on undecodable or mismatched history or missing/wrong keys. Historical IDs, signed bytes, revocation targets, and signed child proof references remain unchanged; legacy list/chain responses retain their exact raw-header/CID relationship. This closes reuse of an already owner-signed grant under another accepted encoding; it does not involve forging new rights. + ## [1.20.6] - 2026-10-14 A security fix (TC-1080, Part A). UCAN issuer resolution no longer dereferences network DID methods by default, so a request can't make the node fetch arbitrary or internal hosts through a crafted issuer DID (#284). diff --git a/docs/delegation-flow.md b/docs/delegation-flow.md index a1eaf0db..b477d801 100644 --- a/docs/delegation-flow.md +++ b/docs/delegation-flow.md @@ -393,6 +393,41 @@ export class Authenticator { } ``` +## Historical CACAO CIDs and Revocation + +New CACAO imports hash the canonical bytes returned by +`TinyCloudDelegation::decode`: accepted recovery IDs, issuer address/chain +representations, and DAG-CBOR encodings share the same ingestion identity. +UCAN/JWS bytes remain exact. + +At startup, `m20261014_000000_delegation_cid_alias` creates the persisted alias +index. The database then backfills missing entries in a transaction from each +stored delegation's exact plaintext serialization, using that same decoder. +Encrypted databases must open with `SpaceDatabase::new_with_encryption`; the +column key must be available before backfill. Missing/wrong keys, decoding +failures, and plaintext hash mismatches fail startup rather than silently +leaving an active alias. Completed backfills do not re-decode indexed history; +one indexed encrypted record is checked on each boot to validate the key. + +Revocation status, invocation authorization, and ancestor checks union owner +revocations across equivalent stored CIDs through indexed lookups. Historical +delegation IDs, signed bytes, revocation targets, and child proof edges are +never rewritten. A child signed against an old CID remains valid until the +owner revokes that identity; revocation also denies descendants and new +redelegations citing any equivalent root. + +Canonical chain guards use indexed alias joins in the existing bounded closure +query, so equivalent revocations serialize against in-flight ancestor +authorization without an extra request round trip. New delegations and their +alias commit together; PostgreSQL uses a dependent insert CTE to preserve the +existing registration round-trip budget. + +Legacy capability list/chain responses advertise the stored CID and return +the exact stored raw header, not a re-encoding of the decoded CACAO. Their +parent CID fields retain the signed child references. Existing legacy list +ancestor-visibility behavior is unchanged; authorization always checks the +complete chain. + ## Child Delegation (UCAN) Creation For sharing, child UCANs are created that delegate from the session key to another DID. diff --git a/tinycloud-core/src/auth_graph.rs b/tinycloud-core/src/auth_graph.rs index 0fafaecd..8246c743 100644 --- a/tinycloud-core/src/auth_graph.rs +++ b/tinycloud-core/src/auth_graph.rs @@ -10,7 +10,7 @@ //! the same consistent view. use crate::hash::Hash; -use crate::models::{abilities, delegation, revocation}; +use crate::models::{abilities, delegation, delegation_cid_alias, revocation}; use crate::relationships::parent_delegations; use sea_orm::{ entity::prelude::*, @@ -29,6 +29,23 @@ use crate::models::revocation::MAX_CHAIN_TRAVERSAL_NODES; pub(crate) async fn load_closure_edges( db: &C, roots: &[Hash], +) -> Result<(Vec, HashMap>), ChainTraversalError> { + load_closure(db, roots, false).await +} + +/// Resolve the same bounded closure to canonical lock identities. Signed graph +/// IDs stay untouched; indexed alias joins share the closure's one round trip. +pub(crate) async fn load_chain_lock_ids( + db: &C, + roots: &[Hash], +) -> Result, ChainTraversalError> { + load_closure(db, roots, true).await.map(|(ids, _)| ids) +} + +async fn load_closure( + db: &C, + roots: &[Hash], + canonical_locks: bool, ) -> Result<(Vec, HashMap>), ChainTraversalError> { let mut nodes = roots.to_vec(); nodes.sort_by(|left, right| left.as_ref().cmp(right.as_ref())); @@ -74,6 +91,17 @@ pub(crate) async fn load_closure_edges( ) .to_owned(); let mut closure_query = base; + if canonical_locks { + // Parentless stored roots otherwise produce no edge row. Seed them + // only for locking; roots without a persisted alias retain their ID. + let seed = Query::select() + .expr_as(Expr::col(delegation_cid_alias::Column::Id), child.clone()) + .expr_as(Expr::val(Option::::None), parent.clone()) + .from(delegation_cid_alias::Entity) + .and_where(Expr::col(delegation_cid_alias::Column::Id).is_in(nodes.iter().copied())) + .to_owned(); + closure_query.union(UnionType::Distinct, seed); + } closure_query.union(UnionType::Distinct, recursive); let cte = CommonTableExpression::new() .table_name(closure.clone()) @@ -87,20 +115,65 @@ pub(crate) async fn load_closure_edges( (closure.clone(), child.clone()), (closure.clone(), parent.clone()), ]) - .from(closure) - .limit((edge_cap + 1) as u64); + .from(closure.clone()) + .limit((edge_cap + if canonical_locks { nodes.len() } else { 0 } + 1) as u64); + if canonical_locks { + for (column, alias, output) in [ + ( + child.clone(), + Alias::new("child_alias"), + Alias::new("child_lock"), + ), + ( + parent.clone(), + Alias::new("parent_alias"), + Alias::new("parent_lock"), + ), + ] { + query + .expr_as( + Expr::col((alias.clone(), delegation_cid_alias::Column::Canonical)), + output, + ) + .join_as( + JoinType::LeftJoin, + delegation_cid_alias::Entity, + alias.clone(), + Expr::col((alias, delegation_cid_alias::Column::Id)) + .equals((closure.clone(), column)), + ); + } + } let query = query.with(WithClause::new().recursive(true).cte(cte).to_owned()); let rows = db .query_all(db.get_database_backend().build(&query)) .await?; - if rows.len() > edge_cap { + if rows.len() > edge_cap + if canonical_locks { nodes.len() } else { 0 } { return Err(ChainTraversalError::LimitExceeded); } let mut edges: HashMap> = HashMap::new(); + let mut lock_aliases = HashMap::new(); for row in rows { let edge_child: Hash = row.try_get("", "child")?; - let edge_parent: Hash = row.try_get("", "parent")?; + let edge_parent: Option = row.try_get("", "parent")?; + if canonical_locks { + if let Some(canonical) = row.try_get::>("", "child_lock")? { + if canonical != edge_child { + lock_aliases.insert(edge_child, canonical); + } + } + if let (Some(parent), Some(canonical)) = + (edge_parent, row.try_get::>("", "parent_lock")?) + { + if canonical != parent { + lock_aliases.insert(parent, canonical); + } + } + } + let Some(edge_parent) = edge_parent else { + continue; + }; let parents = edges.entry(edge_child).or_default(); if !parents.contains(&edge_parent) { parents.push(edge_parent); @@ -123,6 +196,11 @@ pub(crate) async fn load_closure_edges( } } } + for id in &mut nodes { + if let Some(canonical) = lock_aliases.get(id) { + *id = *canonical; + } + } Ok((nodes, edges)) } @@ -168,12 +246,10 @@ impl AuthGraphSnapshot { ability_rows.entry(row.delegation).or_default().push(row); } - let revoked = revocation::Entity::find() - .filter(revocation::Column::Revoked.is_in(nodes.iter().copied())) - .all(db) + let revoked = revocation::effective_rows(db, &nodes) .await? .into_iter() - .map(|row| row.revoked) + .map(|(id, _)| id) .collect(); Ok(Self { diff --git a/tinycloud-core/src/db.rs b/tinycloud-core/src/db.rs index d04594bb..72565071 100644 --- a/tinycloud-core/src/db.rs +++ b/tinycloud-core/src/db.rs @@ -20,7 +20,7 @@ use crate::types::{ DelegationQueryPage, DelegationQueryStatus, DelegationResource, ListFilters, Metadata, Resource, SpaceIdWrap, }; -use crate::util::{Capability, DelegationInfo, DelegationMode}; +use crate::util::DelegationInfo; use sea_orm::{ entity::prelude::*, error::{DbErr, RuntimeErr, SqlxError}, @@ -343,11 +343,32 @@ impl SpaceDatabase where C: ConnectionTrait + Clone + Into, { + /// Open an unencrypted delegation database and complete historical CID + /// indexing before accepting requests. pub async fn new(conn: C, storage: B, secrets: K) -> Result { + Self::new_with_encryption(conn, storage, secrets, None).await + } + + /// Open a database with its column encryption key available before the + /// historical CID backfill. Missing/wrong keys or undecodable history fail + /// startup; signed records and proof edges are never rewritten. + pub async fn new_with_encryption( + conn: C, + storage: B, + secrets: K, + encryption: Option, + ) -> Result { let pool: RecoveringConnection = conn.clone().into(); // sea-orm-migration's schema manager accepts only the raw sea-orm // connection. Migrations run once at boot, before any request. migrate(&pool).await?; + let tx = begin_write_transaction(&pool, None).await?; + if pool.get_database_backend() == DbBackend::Postgres { + tx.execute_unprepared(crate::db_pool::EXEMPT_STATEMENT_TIMEOUT) + .await?; + } + delegation_cid_alias::backfill(&tx, encryption.as_ref()).await?; + tx.commit().await?; let writer_lock = (pool.get_database_backend() == sea_orm::DatabaseBackend::Sqlite) .then(|| Arc::new(tokio::sync::Mutex::new(()))); let read_audit = ReadAuditPipeline::start(pool, writer_lock.clone()); @@ -355,7 +376,7 @@ where conn, storage, secrets, - encryption: None, + encryption, sql_sizes: SqlSizes::default(), revocation_chain_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), kv_object_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), @@ -368,11 +389,6 @@ where }) } - pub fn with_encryption(mut self, encryption: Option) -> Self { - self.encryption = encryption; - self - } - pub fn with_sql_sizes(mut self, sql_sizes: SqlSizes) -> Self { self.sql_sizes = sql_sizes; self @@ -592,10 +608,7 @@ where }; let bytes = crate::encryption::maybe_decrypt(self.encryption.as_ref(), &row.serialization) .map_err(|error| error.to_string())?; - let delegation = - TinyCloudDelegation::from_bytes(&bytes).map_err(|error| error.to_string())?; - let info = DelegationInfo::try_from(delegation).map_err(|error| error.to_string())?; - let event = SerializedEvent(info, bytes); + let event = decode_stored_delegation(bytes).map_err(|error| error.to_string())?; if event.content_hash() != row.id || event.content_hash() != Hash::from(cid) { return Err("delegation-signed-bytes-mismatch".to_string()); } @@ -1292,7 +1305,7 @@ where &self, roots: &[Hash], ) -> Result, TxError> { - let keys = revocation::ancestor_chain_ids_for_roots(&self.conn, roots) + let keys = crate::auth_graph::load_chain_lock_ids(&self.conn, roots) .await .map_err(|error| match error { revocation::ChainTraversalError::Db(error) => TxError::Db(error), @@ -1980,9 +1993,8 @@ where ); let authz_start = Instant::now(); let closure_start = Instant::now(); - let lock_keys = crate::auth_graph::load_closure_edges(&self.conn, &guard_roots) + let lock_keys = crate::auth_graph::load_chain_lock_ids(&self.conn, &guard_roots) .await - .map(|(keys, _)| keys) .map_err(|error| match error { revocation::ChainTraversalError::Db(error) => TxError::Db(error), revocation::ChainTraversalError::LimitExceeded => { @@ -2880,9 +2892,9 @@ pub enum InvocationOutcome { KvBatchWrite(Vec), KvBatchRead(Vec), KvRead(Option<(Metadata, Hash, Content)>), - OpenSessions(HashMap), - /// Ordered delegation chain from leaf to root - DelegationChain(Vec), + OpenSessions(HashMap), + /// Ordered delegation chain from leaf to root, retaining exact signed bytes. + DelegationChain(Vec), SqlResult(serde_json::Value), SqlExport(Vec), DuckDbResult(serde_json::Value), @@ -3912,15 +3924,25 @@ fn delegations_touching_space(backend: DbBackend, space_id: &SpaceId) -> SimpleE ) } +/// Reparse response projections from their signed source, preserving complete +/// proof CIDs (including codecs and order) rather than reconstructing them from +/// the graph's hash-only edges. +fn decode_stored_delegation(serialization: Vec) -> Result { + let delegation = TinyCloudDelegation::from_bytes(&serialization) + .map_err(|_| DbErr::Custom("delegation-signed-bytes-invalid".into()))?; + let info = DelegationInfo::try_from(delegation) + .map_err(|_| DbErr::Custom("delegation-signed-info-invalid".into()))?; + Ok(SerializedEvent(info, serialization)) +} + async fn get_valid_delegations( db: &C, space_id: &SpaceId, encryption: Option<&ColumnEncryption>, -) -> Result, TxError> { +) -> Result, TxError> { let (dels, abilities): (Vec, Vec>) = delegation::Entity::find() - .left_join(revocation::Entity) - .filter(revocation::Column::Id.is_null()) + .filter(revocation::unrevoked_condition()) .filter(delegations_touching_space( db.get_database_backend(), space_id, @@ -3930,12 +3952,10 @@ async fn get_valid_delegations( .await? .into_iter() .unzip(); - let parents = dels.load_many(parent_delegations::Entity, db).await?; let now = time::OffsetDateTime::now_utc(); dels.into_iter() .zip(abilities) - .zip(parents) - .filter_map(|((del, ability), parents)| { + .filter_map(|(del, ability)| { // `delegations_touching_space` has already narrowed this to the // delegations whose resources fall in the space's prefix range; // the space check below is the exact filter over that superset. @@ -3948,51 +3968,16 @@ async fn get_valid_delegations( Ok(s) => s, Err(e) => return Some(Err(TxError::Encryption(e))), }; - Some(match TinyCloudDelegation::from_bytes(&serialization) { - Ok(delegation) => Ok(( - del.id, - DelegationInfo { - delegator: del.delegator, - delegate: del.delegatee, - parents: parents.into_iter().map(|p| p.parent.to_cid(0x55)).collect(), - expiry: del.expiry, - not_before: del.not_before, - issued_at: del.issued_at, - delegation_mode: mode_from_facts(&del.facts), - capabilities: ability - .into_iter() - .map(|a| Capability { - resource: a.resource, - ability: a.ability, - caveats: a.caveats, - }) - .collect(), - delegation, - }, - )), - Err(e) => Err(TxError::Encoding(e)), - }) + Some( + decode_stored_delegation(serialization) + .map(|event| (del.id, event)) + .map_err(TxError::Db), + ) } else { None } }) - .collect::, TxError>>() -} - -/// Decode the persisted `xyz.tinycloud.policy/delegationMode` marker from -/// a stored delegation row's facts column. -fn mode_from_facts(facts: &Option) -> DelegationMode { - facts - .as_ref() - .and_then(|f| f.0.get(DelegationMode::FACT_KEY).and_then(|v| v.as_str())) - .map(|s| { - if s == "terminal" { - DelegationMode::Terminal - } else { - DelegationMode::Attenuable - } - }) - .unwrap_or(DelegationMode::Attenuable) + .collect::, TxError>>() } /// Resolve a session key DID (did:key:...) to its root PKH DID (did:pkh:...). @@ -4297,17 +4282,14 @@ async fn load_account_ancestor_state( .filter(delegation::Column::Id.is_in(ids.iter().copied())) .all(db) .await?; - let revocation_rows = revocation::Entity::find() - .filter(revocation::Column::Revoked.is_in(ids)) - .all(db) - .await?; + let revocation_rows = revocation::effective_rows(db, &ids).await?; let delegations = delegation_rows .into_iter() .map(|row| (row.id, row)) .collect(); let mut revocations: HashMap> = HashMap::new(); - for row in revocation_rows { - revocations.entry(row.revoked).or_default().push(row); + for (id, row) in revocation_rows { + revocations.entry(id).or_default().push(row); } for values in revocations.values_mut() { values.sort_by(|left, right| left.id.as_ref().cmp(right.id.as_ref())); @@ -4327,7 +4309,7 @@ async fn get_filtered_delegations, encryption: Option<&ColumnEncryption>, -) -> Result, TxError> { +) -> Result, TxError> { // Resolve session key DID to PKH DID for direction filtering let pkh_did = resolve_pkh_did(db, invoker) .await @@ -4335,8 +4317,7 @@ async fn get_filtered_delegations, Vec>) = delegation::Entity::find() - .left_join(revocation::Entity) - .filter(revocation::Column::Id.is_null()) + .filter(revocation::unrevoked_condition()) .filter(delegations_touching_space( db.get_database_backend(), space_id, @@ -4346,7 +4327,6 @@ async fn get_filtered_delegations now).unwrap_or(true) && del.not_before.map(|n| n <= now).unwrap_or(true)) @@ -4414,32 +4393,13 @@ async fn get_filtered_delegations s, Err(e) => return Some(Err(TxError::Encryption(e))), }; - Some(match TinyCloudDelegation::from_bytes(&serialization) { - Ok(delegation) => Ok(( - del.id, - DelegationInfo { - delegator: del.delegator, - delegate: del.delegatee, - parents: parents.into_iter().map(|p| p.parent.to_cid(0x55)).collect(), - expiry: del.expiry, - not_before: del.not_before, - issued_at: del.issued_at, - delegation_mode: mode_from_facts(&del.facts), - capabilities: ability - .into_iter() - .map(|a| Capability { - resource: a.resource, - ability: a.ability, - caveats: a.caveats, - }) - .collect(), - delegation, - }, - )), - Err(e) => Err(TxError::Encoding(e)), - }) + Some( + decode_stored_delegation(serialization) + .map(|event| (del.id, event)) + .map_err(TxError::Db), + ) }) - .collect::, TxError>>() + .collect::, TxError>>() } /// Get the delegation chain for a specific delegation, ordered from leaf to root. @@ -4449,7 +4409,7 @@ async fn get_delegation_chain( space_id: &SpaceId, delegation_cid: &str, encryption: Option<&ColumnEncryption>, -) -> Result, TxError> { +) -> Result, TxError> { use tinycloud_auth::ipld_core::cid::Cid; // Parse the delegation CID @@ -4466,8 +4426,7 @@ async fn get_delegation_chain( loop { // Find the delegation with this hash let del_with_abilities = delegation::Entity::find_by_id(current_hash) - .left_join(revocation::Entity) - .filter(revocation::Column::Id.is_null()) + .filter(revocation::unrevoked_condition()) .find_with_related(abilities::Entity) .all(db) .await?; @@ -4496,31 +4455,8 @@ async fn get_delegation_chain( .all(db) .await?; - let parent_cids: Vec = parents.iter().map(|p| p.parent.to_cid(0x55)).collect(); - - // Create DelegationInfo let serialization = crate::encryption::maybe_decrypt(encryption, &del.serialization)?; - let delegation = TinyCloudDelegation::from_bytes(&serialization)?; - let info = DelegationInfo { - delegator: del.delegator, - delegate: del.delegatee, - parents: parent_cids.clone(), - expiry: del.expiry, - not_before: del.not_before, - issued_at: del.issued_at, - delegation_mode: mode_from_facts(&del.facts), - capabilities: ability - .into_iter() - .map(|a| Capability { - resource: a.resource, - ability: a.ability, - caveats: a.caveats, - }) - .collect(), - delegation, - }; - - chain.push(info); + chain.push(decode_stored_delegation(serialization)?); // Move to the first parent (if any) to continue the chain // Note: We follow the first parent; for multiple parents, this gives one path @@ -6083,9 +6019,8 @@ mod test { /// Resolve the ancestor closure exactly as `invoke_with_options_mode` /// does, then take the guards shared — the production invocation path. async fn invocation_chain_keys(db: &TestDb, roots: &[Hash]) -> Vec { - crate::auth_graph::load_closure_edges(&db.conn, roots) + crate::auth_graph::load_chain_lock_ids(&db.conn, roots) .await - .map(|(keys, _)| keys) .expect("chain closure query") } @@ -6115,6 +6050,49 @@ mod test { tokio::time::sleep(std::time::Duration::from_millis(150)).await; } + /// Equivalent historical parents and an as-yet unregistered canonical CID + /// must contend on the same guard as an invocation citing the signed child. + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn historical_alias_revocation_waits_for_signed_child_invocation() { + use sea_orm::{ActiveModelTrait, ActiveValue::Set}; + + let db = get_db().await.unwrap(); + insert_chain_guard_actors(&db).await; + let (legacy, child) = insert_test_chain(&db, "historical-alias-lock").await; + let (other_legacy, _) = insert_test_chain(&db, "other-historical-alias-lock").await; + let canonical = crate::hash::hash(b"canonical parent not registered as a delegation"); + for id in [legacy, other_legacy] { + delegation_cid_alias::ActiveModel { + id: Set(id), + canonical: Set(canonical), + encrypted: Set(false), + } + .insert(&db.conn) + .await + .unwrap(); + } + + for target in [legacy, other_legacy, canonical] { + let in_flight = acquire_invocation_guards(&db, &[child]).await; + let revoke_db = db.clone(); + let revoke = + tokio::spawn(async move { revoke_db.acquire_chain_guards(&[target]).await }); + settle().await; + assert!( + !revoke.is_finished(), + "equivalent ancestor revocation must wait for the signed child's invocation" + ); + drop(in_flight); + let guards = tokio::time::timeout(std::time::Duration::from_secs(10), revoke) + .await + .expect("alias revocation proceeds after the child releases its guards") + .unwrap() + .ok() + .expect("alias revocation guards"); + drop(guards); + } + } + /// TC-324 requirement 1: invocations sharing one root delegation must /// hold the chain guard at the same time, not merely all succeed. /// diff --git a/tinycloud-core/src/hash.rs b/tinycloud-core/src/hash.rs index 5a7f151e..79ac6b1d 100644 --- a/tinycloud-core/src/hash.rs +++ b/tinycloud-core/src/hash.rs @@ -108,7 +108,8 @@ impl sea_orm::TryGetable for Hash { res: &QueryResult, idx: I, ) -> Result { - let vec: Vec = res.try_get_by(idx).map_err(sea_orm::TryGetError::DbErr)?; + // Preserve TryGetError::Null so nullable closure/alias joins decode as None. + let vec = as sea_orm::TryGetable>::try_get_by(res, idx)?; Hash::try_from(vec).map_err(|e| { sea_orm::TryGetError::DbErr(DbErr::TryIntoErr { from: "Vec", diff --git a/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs b/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs new file mode 100644 index 00000000..ed6484e9 --- /dev/null +++ b/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs @@ -0,0 +1,90 @@ +use sea_orm_migration::prelude::*; + +use crate::models::{delegation, delegation_cid_alias}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(delegation_cid_alias::Entity) + .if_not_exists() + .col( + ColumnDef::new(delegation_cid_alias::Column::Id) + .binary() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(delegation_cid_alias::Column::Canonical) + .binary() + .not_null(), + ) + .col( + ColumnDef::new(delegation_cid_alias::Column::Encrypted) + .boolean() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .from( + delegation_cid_alias::Entity, + delegation_cid_alias::Column::Id, + ) + .to(delegation::Entity, delegation::Column::Id), + ) + // Canonical identities may have no separately persisted + // serialization, so only the exact ID has a foreign key. + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("idx_delegation_cid_alias_canonical") + .table(delegation_cid_alias::Entity) + .col(delegation_cid_alias::Column::Canonical) + .col(delegation_cid_alias::Column::Id) + .if_not_exists() + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("idx_delegation_cid_alias_encrypted") + .table(delegation_cid_alias::Entity) + .col(delegation_cid_alias::Column::Encrypted) + .col(delegation_cid_alias::Column::Id) + .if_not_exists() + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .name("idx_delegation_cid_alias_canonical") + .table(delegation_cid_alias::Entity) + .to_owned(), + ) + .await?; + manager + .drop_index( + Index::drop() + .name("idx_delegation_cid_alias_encrypted") + .table(delegation_cid_alias::Entity) + .to_owned(), + ) + .await?; + manager + .drop_table(Table::drop().table(delegation_cid_alias::Entity).to_owned()) + .await + } +} diff --git a/tinycloud-core/src/migrations/mod.rs b/tinycloud-core/src/migrations/mod.rs index b73c241c..97ea9e61 100644 --- a/tinycloud-core/src/migrations/mod.rs +++ b/tinycloud-core/src/migrations/mod.rs @@ -24,6 +24,7 @@ pub mod m20261005_000000_current_kv_sync_order; pub mod m20261005_000000_deactivate_hook_subscriptions; pub mod m20261007_000000_database_alias; pub mod m20261007_010000_database_identity_fence; +pub mod m20261014_000000_delegation_cid_alias; pub struct Migrator; @@ -56,6 +57,7 @@ impl MigratorTrait for Migrator { Box::new(m20261005_000000_deactivate_hook_subscriptions::Migration), Box::new(m20261007_000000_database_alias::Migration), Box::new(m20261007_010000_database_identity_fence::Migration), + Box::new(m20261014_000000_delegation_cid_alias::Migration), ] } } @@ -73,7 +75,7 @@ mod release_line_tests { return; }; let admin = Database::connect(url.clone()).await.unwrap(); - let base_count = (Migrator::migrations().len() - 2) as u32; + let base_count = (Migrator::migrations().len() - 3) as u32; for path in ["fresh", "release"] { let schema = format!( "tc780_{path}_{}_{}", @@ -130,6 +132,7 @@ mod release_line_tests { .collect(); assert!(versions.contains(&"m20261007_000000_database_alias".to_string())); assert!(versions.contains(&"m20261007_010000_database_identity_fence".to_string())); + assert!(versions.contains(&"m20261014_000000_delegation_cid_alias".to_string())); drop(db); admin .execute(Statement::from_string( diff --git a/tinycloud-core/src/models/delegation.rs b/tinycloud-core/src/models/delegation.rs index 931e16f6..98fc2c03 100644 --- a/tinycloud-core/src/models/delegation.rs +++ b/tinycloud-core/src/models/delegation.rs @@ -6,7 +6,12 @@ use crate::policy_capability::sql_caveat; use crate::types::{Ability, Caveats, Facts, Resource}; use crate::util::DelegationMode; use crate::{events::Delegation, models::*, relationships::*, util}; -use sea_orm::{entity::prelude::*, sea_query::OnConflict, ConnectionTrait}; +use sea_orm::ActiveValue::Set; +use sea_orm::{ + entity::prelude::*, + sea_query::{Alias, CommonTableExpression, Expr, OnConflict, Query, WithClause}, + ConnectionTrait, DbBackend, QueryTrait, +}; use std::collections::BTreeMap; use time::OffsetDateTime; use tinycloud_auth::{authorization::TinyCloudDelegation, identity::did_principal_matches}; @@ -835,10 +840,11 @@ async fn save( // idempotency gate below. save_actors(&[&delegation.delegator, &delegation.delegate], db).await?; - // Idempotency gate: the delegation INSERT ON CONFLICT is the claim. If - // the row already exists (RecordNotInserted) we return Existing immediately - // — no abilities or parent rows are written. - match Entity::insert(ActiveModel::from(Model { + // The signed row remains the idempotency gate. Persist its alias in the + // same transaction; PostgreSQL uses a dependent INSERT CTE to keep the + // existing registration round-trip budget. + let canonical = delegation_cid_alias::canonical_hash(&serialization)?; + let insert = Entity::insert(ActiveModel::from(Model { id: hash, delegator: delegation.delegator, delegatee: delegation.delegate, @@ -848,15 +854,65 @@ async fn save( facts, serialization: stored_serialization, })) - .on_conflict(OnConflict::column(Column::Id).do_nothing().to_owned()) - .exec(db) - .await - { - Err(DbErr::RecordNotInserted) => return Ok(DelegationRegistration::Existing(hash)), - r => { - r?; + .on_conflict(OnConflict::column(Column::Id).do_nothing().to_owned()); + if db.get_database_backend() == DbBackend::Postgres { + let claimed = Alias::new("registered_delegation"); + let mut delegation_insert = insert.into_query(); + delegation_insert.returning(Query::returning().column(Column::Id).to_owned()); + let mut alias_insert = Query::insert(); + alias_insert + .into_table(delegation_cid_alias::Entity) + .columns([ + delegation_cid_alias::Column::Id, + delegation_cid_alias::Column::Canonical, + delegation_cid_alias::Column::Encrypted, + ]) + .select_from( + Query::select() + .column(Column::Id) + .expr(Expr::val(canonical)) + .expr(Expr::val(encryption.is_some())) + .from(claimed.clone()) + .to_owned(), + ) + .map_err(|error| DbErr::Custom(error.to_string()))? + .returning( + Query::returning() + .column(delegation_cid_alias::Column::Id) + .to_owned(), + ) + .with_cte( + WithClause::new() + .cte( + CommonTableExpression::new() + .table_name(claimed) + .query(delegation_insert) + .to_owned(), + ) + .to_owned(), + ); + if db + .query_one(db.get_database_backend().build(&alias_insert)) + .await? + .is_none() + { + return Ok(DelegationRegistration::Existing(hash)); } - }; + } else { + match insert.exec(db).await { + Err(DbErr::RecordNotInserted) => return Ok(DelegationRegistration::Existing(hash)), + result => { + result?; + } + }; + delegation_cid_alias::Entity::insert(delegation_cid_alias::ActiveModel { + id: Set(hash), + canonical: Set(canonical), + encrypted: Set(encryption.is_some()), + }) + .exec(db) + .await?; + } // save abilities — persist UCAN caveats with the delegation row. W1 // contract: caveats MUST NOT be reconstructed from invocation facts; diff --git a/tinycloud-core/src/models/delegation_cid_alias.rs b/tinycloud-core/src/models/delegation_cid_alias.rs new file mode 100644 index 00000000..985d0f6f --- /dev/null +++ b/tinycloud-core/src/models/delegation_cid_alias.rs @@ -0,0 +1,324 @@ +//! Canonical revocation identities without rewriting signed delegation history. + +use std::borrow::Cow; + +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use sea_orm::{ + entity::prelude::*, sea_query::Query, ActiveValue::Set, ConnectionTrait, QueryOrder, + QuerySelect, +}; +use tinycloud_auth::authorization::{HeaderEncode, TinyCloudDelegation}; + +use crate::encryption::{maybe_decrypt, ColumnEncryption}; +use crate::hash::{hash, Hash}; +use crate::models::delegation; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "delegation_cid_alias")] +pub struct Model { + /// Hash of the exact plaintext serialization persisted in `delegation`. + #[sea_orm(primary_key, auto_increment = false)] + pub id: Hash, + /// Hash of current ingestion's canonical bytes; it need not be a stored row. + pub canonical: Hash, + /// Whether the original stored serialization requires the column key. + pub encrypted: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "delegation::Entity", + from = "Column::Id", + to = "delegation::Column::Id" + )] + Delegation, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Delegation.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +/// Derive the same identity as current delegation ingestion, without changing +/// the stored serialization. UCAN JWT bytes remain exact; CACAO goes through +/// the public header decoder, including all current canonicalization rules. +/// Decoder rejection is fatal: a guessed self-alias could hide a revocation. +pub(crate) fn canonical_hash(serialization: &[u8]) -> Result { + let decoded = match std::str::from_utf8(serialization) { + Ok(token) if token.contains('.') => TinyCloudDelegation::decode(token), + _ => TinyCloudDelegation::decode(&URL_SAFE.encode(serialization)), + }; + let (_, canonical) = decoded.map_err(|_| { + DbErr::Custom("delegation CID alias backfill: delegation decoding failed".into()) + })?; + Ok(hash(&canonical)) +} + +/// Validate one indexed encrypted record on every boot, including after the +/// backfill is complete. A completed index must not permit keyless restarts. +async fn validate_encryption( + db: &C, + encryption: Option<&ColumnEncryption>, +) -> Result<(), DbErr> { + let encrypted_id = Query::select() + .column(Column::Id) + .from(Entity) + .and_where(Expr::col(Column::Encrypted).eq(true)) + .limit(1) + .to_owned(); + let Some((id, stored)) = delegation::Entity::find() + .select_only() + .column(delegation::Column::Id) + .column(delegation::Column::Serialization) + .filter(delegation::Column::Id.in_subquery(encrypted_id)) + .into_tuple::<(Hash, Vec)>() + .one(db) + .await? + else { + return Ok(()); + }; + let encryption = encryption.ok_or_else(|| { + DbErr::Custom( + "delegation CID alias backfill: encrypted delegation requires a column encryption key" + .into(), + ) + })?; + let plaintext = encryption + .decrypt(&stored) + .map_err(|_| DbErr::Custom("delegation CID alias backfill: decryption failed".into()))?; + if hash(&plaintext) != id { + return Err(DbErr::Custom( + "delegation CID alias backfill: plaintext content hash does not match stored ID".into(), + )); + } + Ok(()) +} + +/// Backfill only persisted delegations with no alias, in bounded keyset pages. +/// The caller must use its boot transaction so any failure rolls back all +/// aliases written by this call. Encrypted rows require the configured key; +/// decryption, plaintext content-hash, and decoder failures abort startup. +/// Original IDs, serializations, proof edges, and revocation targets are never +/// updated. Completed restarts validate only one indexed encrypted record. +pub(crate) async fn backfill( + db: &C, + encryption: Option<&ColumnEncryption>, +) -> Result<(), DbErr> { + const PAGE_SIZE: u64 = 128; + let mut after = None; + loop { + let mut query = delegation::Entity::find() + .select_only() + .column(delegation::Column::Id) + .column(delegation::Column::Serialization) + .filter( + Expr::exists( + Query::select() + .expr(Expr::value(1)) + .from(Entity) + .and_where( + Expr::col((Entity, Column::Id)) + .equals((delegation::Entity, delegation::Column::Id)), + ) + .to_owned(), + ) + .not(), + ) + .order_by_asc(delegation::Column::Id) + .limit(PAGE_SIZE); + if let Some(id) = after { + query = query.filter(delegation::Column::Id.gt(id)); + } + let rows = query.into_tuple::<(Hash, Vec)>().all(db).await?; + if rows.is_empty() { + return validate_encryption(db, encryption).await; + } + + let mut aliases = Vec::with_capacity(rows.len()); + for (id, stored) in rows { + // This is ColumnEncryption's version marker. Borrow legacy + // plaintext instead of copying it through the passthrough helper. + let serialization = if stored.first() == Some(&0x01) { + let encryption = encryption.ok_or_else(|| { + DbErr::Custom( + "delegation CID alias backfill: encrypted delegation requires a column encryption key" + .into(), + ) + })?; + Cow::Owned(maybe_decrypt(Some(encryption), &stored).map_err(|_| { + DbErr::Custom("delegation CID alias backfill: decryption failed".into()) + })?) + } else { + Cow::Borrowed(stored.as_slice()) + }; + if hash(serialization.as_ref()) != id { + return Err(DbErr::Custom( + "delegation CID alias backfill: plaintext content hash does not match stored ID" + .into(), + )); + } + aliases.push(ActiveModel { + id: Set(id), + canonical: Set(canonical_hash(serialization.as_ref())?), + encrypted: Set(stored.first() == Some(&0x01)), + }); + after = Some(id); + } + Entity::insert_many(aliases).exec(db).await?; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::migrations::Migrator; + use crate::models::actor; + use crate::test_support::did_resolution::signed_ucan; + use sea_orm::{ActiveModelTrait, Database, DbConn, TransactionTrait}; + use sea_orm_migration::MigratorTrait; + use tinycloud_auth::ssi::jwk::{Algorithm, JWK}; + + const ACTOR: &str = "did:key:cid-alias-fixture"; + + fn ucan_serialization() -> Vec { + signed_ucan(ACTOR, &JWK::generate_ed25519().unwrap(), Algorithm::EdDSA) + .encode() + .unwrap() + .into_bytes() + } + + async fn database() -> DbConn { + let db = Database::connect("sqlite::memory:").await.unwrap(); + Migrator::up(&db, None).await.unwrap(); + actor::ActiveModel { + id: Set(ACTOR.into()), + } + .insert(&db) + .await + .unwrap(); + db + } + + async fn insert_delegation(db: &DbConn, id: Hash, serialization: Vec) { + delegation::ActiveModel { + id: Set(id), + delegator: Set(ACTOR.into()), + delegatee: Set(ACTOR.into()), + expiry: Set(None), + issued_at: Set(None), + not_before: Set(None), + facts: Set(None), + serialization: Set(serialization), + } + .insert(db) + .await + .unwrap(); + } + + #[test] + fn ucan_canonical_identity_preserves_exact_jwt_bytes() { + let serialization = ucan_serialization(); + assert_eq!( + canonical_hash(&serialization).unwrap(), + hash(&serialization) + ); + } + + #[tokio::test] + async fn encrypted_backfill_preserves_storage_and_restart_requires_the_key() { + let db = database().await; + let encryption = ColumnEncryption::new([0x42; 32]); + let serialization = ucan_serialization(); + let id = hash(&serialization); + let stored = encryption.encrypt(&serialization); + insert_delegation(&db, id, stored.clone()).await; + + backfill(&db, Some(&encryption)).await.unwrap(); + assert_eq!( + Entity::find_by_id(id).one(&db).await.unwrap().unwrap(), + Model { + id, + canonical: id, + encrypted: true + } + ); + assert_eq!( + delegation::Entity::find_by_id(id) + .one(&db) + .await + .unwrap() + .unwrap() + .serialization, + stored + ); + assert!(backfill(&db, None).await.is_err()); + assert!(backfill(&db, Some(&ColumnEncryption::new([0x43; 32]))) + .await + .is_err()); + backfill(&db, Some(&encryption)).await.unwrap(); + } + + #[tokio::test] + async fn encrypted_backfill_requires_the_correct_key() { + let db = database().await; + let encryption = ColumnEncryption::new([0x42; 32]); + let serialization = ucan_serialization(); + insert_delegation( + &db, + hash(&serialization), + encryption.encrypt(&serialization), + ) + .await; + + let missing = backfill(&db, None).await.unwrap_err(); + assert!(missing + .to_string() + .contains("requires a column encryption key")); + let wrong = ColumnEncryption::new([0x43; 32]); + let rejected = backfill(&db, Some(&wrong)).await.unwrap_err(); + assert!(rejected.to_string().contains("decryption failed")); + assert_eq!(Entity::find().count(&db).await.unwrap(), 0); + } + + #[tokio::test] + async fn mismatched_plaintext_hash_is_not_aliased() { + let db = database().await; + insert_delegation(&db, hash(b"different content"), ucan_serialization()).await; + let error = backfill(&db, None).await.unwrap_err(); + assert!(error + .to_string() + .contains("plaintext content hash does not match stored ID")); + assert_eq!(Entity::find().count(&db).await.unwrap(), 0); + } + + #[tokio::test] + async fn undecodable_cacao_aborts_without_a_self_alias() { + let db = database().await; + let serialization = vec![0xa0]; + insert_delegation(&db, hash(&serialization), serialization).await; + let transaction = db.begin().await.unwrap(); + let error = backfill(&transaction, None).await.unwrap_err(); + assert!(error.to_string().contains("delegation decoding failed")); + transaction.rollback().await.unwrap(); + assert_eq!(Entity::find().count(&db).await.unwrap(), 0); + } + + #[tokio::test] + async fn keyset_backfill_indexes_more_than_one_page() { + let db = database().await; + for _ in 0..129 { + let serialization = ucan_serialization(); + let id = hash(&serialization); + insert_delegation(&db, id, serialization).await; + } + + backfill(&db, None).await.unwrap(); + assert_eq!(Entity::find().count(&db).await.unwrap(), 129); + backfill(&db, None).await.unwrap(); + assert_eq!(Entity::find().count(&db).await.unwrap(), 129); + } +} diff --git a/tinycloud-core/src/models/mod.rs b/tinycloud-core/src/models/mod.rs index a8a0afca..afefa687 100644 --- a/tinycloud-core/src/models/mod.rs +++ b/tinycloud-core/src/models/mod.rs @@ -6,6 +6,7 @@ pub mod database_artifact; pub mod database_identity_fence; pub mod database_legacy_artifact; pub mod delegation; +pub mod delegation_cid_alias; pub(crate) use crate::did_resolution; pub mod encryption_audit; pub mod encryption_ceremony; diff --git a/tinycloud-core/src/models/revocation.rs b/tinycloud-core/src/models/revocation.rs index 7321ef7c..02137040 100644 --- a/tinycloud-core/src/models/revocation.rs +++ b/tinycloud-core/src/models/revocation.rs @@ -2,8 +2,11 @@ use super::super::{events::Revocation, models::*, relationships::*}; use crate::hash::{hash, Hash}; use crate::models::did_resolution::{verify_ucan_signature, UcanVerifyError}; use crate::types::Resource; +use sea_orm::Condition; use sea_orm::{ - entity::prelude::*, sea_query::OnConflict, ConnectionTrait, QueryOrder, QuerySelect, + entity::prelude::*, + sea_query::{Alias, Expr, JoinType, OnConflict, Query, SimpleExpr, UnionType}, + ConnectionTrait, FromQueryResult, QueryOrder, QuerySelect, }; use std::collections::HashSet; use time::OffsetDateTime; @@ -68,15 +71,108 @@ pub enum ChainTraversalError { LimitExceeded, } +/// Resolve revocation targets through the persisted canonical identity. The +/// exact-target branch also handles graph rows created directly by unit tests. +fn target_condition(id: SimpleExpr) -> Condition { + use delegation_cid_alias::{Column as AliasColumn, Entity as AliasEntity}; + let canonical = Query::select() + .column(AliasColumn::Canonical) + .from(AliasEntity) + .and_where(Expr::col(AliasColumn::Id).eq(id.clone())) + .to_owned(); + let equivalents = Query::select() + .column(AliasColumn::Id) + .from(AliasEntity) + .and_where(Expr::col(AliasColumn::Canonical).in_subquery(canonical)) + .to_owned(); + Condition::any() + .add(Expr::col((Entity, Column::Revoked)).eq(id)) + .add(Expr::col((Entity, Column::Revoked)).in_subquery(equivalents)) +} + +/// Correlated, indexed revocation exclusion for legacy delegation listings. +pub(crate) fn unrevoked_condition() -> SimpleExpr { + Expr::exists( + Query::select() + .expr(Expr::value(1)) + .from(Entity) + .cond_where(target_condition( + Expr::col((delegation::Entity, delegation::Column::Id)).into(), + )) + .to_owned(), + ) + .not() +} + +/// Load effective revocations in one indexed batch while keeping the original +/// signed revocation target and bytes intact. The returned key is the queried +/// delegation, not necessarily the CID the owner named in the revocation. +pub(crate) async fn effective_rows( + db: &C, + ids: &[Hash], +) -> Result, DbErr> { + if ids.is_empty() { + return Ok(Vec::new()); + } + use delegation_cid_alias::{Column as AliasColumn, Entity as AliasEntity}; + let source = Alias::new("revocation_source_alias"); + let target = Alias::new("revocation_target_alias"); + let effective_id = Alias::new("effective_id"); + let columns = [ + Column::Id, + Column::Revoker, + Column::Revoked, + Column::Serialization, + Column::RevokedAt, + ]; + let mut direct = Query::select(); + direct + .columns(columns.map(|column| (Entity, column))) + .expr_as(Expr::col((Entity, Column::Revoked)), effective_id.clone()) + .from(Entity) + .and_where(Expr::col((Entity, Column::Revoked)).is_in(ids.iter().copied())); + let mut aliased = Query::select(); + aliased + .columns(columns.map(|column| (Entity, column))) + .expr_as(Expr::col((source.clone(), AliasColumn::Id)), effective_id) + .from_as(AliasEntity, source.clone()) + .join_as( + JoinType::InnerJoin, + AliasEntity, + target.clone(), + Expr::col((source.clone(), AliasColumn::Canonical)) + .equals((target.clone(), AliasColumn::Canonical)), + ) + .join( + JoinType::InnerJoin, + Entity, + Expr::col((target, AliasColumn::Id)).equals((Entity, Column::Revoked)), + ) + .and_where(Expr::col((source, AliasColumn::Id)).is_in(ids.iter().copied())); + // A self-alias appears in both branches. UNION removes that duplicate + // without manufacturing a second revocation or rewriting its target. + direct.union(UnionType::Distinct, aliased); + db.query_all(db.get_database_backend().build(&direct)) + .await? + .into_iter() + .map(|row| { + Ok(( + row.try_get("", "effective_id")?, + Model::from_query_result(&row, "")?, + )) + }) + .collect() +} + pub(crate) async fn is_revoked( db: &C, delegation_id: &Hash, ) -> Result { Ok(Entity::find() - .filter(Column::Revoked.eq(*delegation_id)) - .count(db) + .filter(target_condition(Expr::value(*delegation_id))) + .one(db) .await? - > 0) + .is_some()) } pub(crate) async fn first_revoked_ancestor( diff --git a/tinycloud-core/tests/fixtures/tc1071/generated/active.sqlite b/tinycloud-core/tests/fixtures/tc1071/generated/active.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..cf3d117eb3e4046cf0378d2a5458d7d886ba156d GIT binary patch literal 516096 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
#1>zdL6iO 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≷9%shjE=EN7=7KAjeRtVcl z;yc(@{Pe> zvAJ9k9gjZHFQrLAh%UUQ;wF+nN@WzFnqJbCua55k|MQDM7tG-7tD*qGzq?a_MYB48 zL3SxOgiyZEm3C^2Al*jm`Hb05zB)=YAP#h>{?Ln_egVs)m9IG2&`l{O0{bSZm6fhA+P|KUUnvtN&RZp4IZsR7}{4;2L`yOF%7nX z0tD$v7V}(_)h3R}1{jmQYy9mXXvP0YsQyP@*Z-{h-$KOFyNe1KHMImYz)K0Cy}ZE% zrqmP}Vap0xZywyf1Oy;#S=nU99-vA{Sx2|pLFqJtnI|`8$UQwcMU8M3IMMfpNddmc z=z4!NO7yMF`w!&Fik~%Gof3)PX=$0|Q7?6S8!vT375Au#)G~vzowT{#u#zuAz&J_Ku)fq?&3fYnoNHhT9(!^I9u;IP;~j4Ubk zqemhrzg56O$BMk?sa5-%sGmz2%*3&+%iRE3^&XRg-=UGCgW+HIf$p$#={f+zi&Gqj zsbw((Drp!>hPAOU4h7tV?DF?>4@`jY#@;{*c6TcagpNEeRrO-rWJm?CmJah^tsb66 z?dwr>ndI4lVs%zYq1{BcRZ(xUJMc-8EEb<- zT2oA^qQF}AxU(hPAsCy*#q{Bp3I z@vG05dolzP4P-g~+-G@@z$!kQjg7rn2ZW-i7=cp+3_gz9&p4rW#E0VnW&tV@!pO=s zl~G4}TD%<5`3@ko4897n=oSiFyWPr|ay9JA>&IVF=k#C0QA^Wf&Pv&YKW}SLr-^eR zhRBH_MRN;!pEJz840kq^C8Af?!p(FkkBbiou?#6U?sk|txt28`?X6Cn8`iFe8(}4} zuf9FiNuMv~$AduFG7CzG!#8w2TiCM_$1w&v^Ef<|a}_(ek|_4~7I7>^Ugq-$=zDe4 zz>c@((OWSuiV3@rMEqADq`wjdqAi@@D8qNDBpGQ zP=1`DFjF#PaSyN%I>OH_TX8v*WeB4>L=$|{bL|;MX;D7@Qg=-#!fgr1UrCPFSU)%`G?Z4RqFA!?Vqr_dx)2RDPj?m{}E5D|0z8PI_BX&hKvVBt^Y_yDDN< zCL78a>zZ3tLNwbe_XD=5#Mr0ctHAVyRd>A(D+mtwDzmhobWH@86h%!W5In0`$4fCa LPANIGHh%vNL()7v literal 0 HcmV?d00001 diff --git a/tinycloud-core/tests/fixtures/tc1071/generated/manifest.json b/tinycloud-core/tests/fixtures/tc1071/generated/manifest.json new file mode 100644 index 00000000..67d49fb6 --- /dev/null +++ b/tinycloud-core/tests/fixtures/tc1071/generated/manifest.json @@ -0,0 +1,342 @@ +[ + { + "generated_by": "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke", + "database": "revoked-v0.sqlite", + "encrypted": false, + "issuer": "did:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569", + "resource": "tinycloud:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569:revoked-v0/kv", + "session_seed": 11, + "child_seed": 12, + "session_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws", + "kty": "OKP", + "x": "Zr5-Myx6RTMyvZ0Kf32wVfXF7xoGraZtmLOftoEMRzo" + }, + "child_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "kty": "OKP", + "x": "C1E62bSSQBXKCQLtB5BE06xdvsIwbwaUjBDajrbjny0" + }, + "canonical_header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VxdGMxMDcxLXJldm9rZWQtdjBmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFNdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZjbVYyYjJ0bFpDMTJNQzlyZGlJNmV5SjBhVzU1WTJ4dmRXUXVhM1l2YkdsemRDSTZXM3Q5WFgwc0luUnBibmxqYkc5MVpEcHdhMmc2Wldsd01UVTFPakU2TUhnMFlUWXlNekUyTmpJellXUTBOVGRHTURKalJFTTFSRGs1TjJSbFJEWTNZVE00TTBWRE5UWTVPbkpsZG05clpXUXRkakF2YzNCaFkyVWlPbnNpZEdsdWVXTnNiM1ZrTG5Od1lXTmxMMmh2YzNRaU9sdDdmVjE5ZlN3aWNISm1JanBiWFgwaXN0YXRlbWVudHkBO0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAvc3BhY2UnLmFzomFzWEEzStHJIfiJ814D9uou9A7MGqhkAgd2p6gNHtR01DrG1HTn9JldgtspjI5pzMZoeFCGQzfk2BgfJ3ZvebEcFI0LHGF0ZmVpcDE5MQ==", + "rows": [ + { + "encoding": "v27", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VxdGMxMDcxLXJldm9rZWQtdjBmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFNdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZjbVYyYjJ0bFpDMTJNQzlyZGlJNmV5SjBhVzU1WTJ4dmRXUXVhM1l2YkdsemRDSTZXM3Q5WFgwc0luUnBibmxqYkc5MVpEcHdhMmc2Wldsd01UVTFPakU2TUhnMFlUWXlNekUyTmpJellXUTBOVGRHTURKalJFTTFSRGs1TjJSbFJEWTNZVE00TTBWRE5UWTVPbkpsZG05clpXUXRkakF2YzNCaFkyVWlPbnNpZEdsdWVXTnNiM1ZrTG5Od1lXTmxMMmh2YzNRaU9sdDdmVjE5ZlN3aWNISm1JanBiWFgwaXN0YXRlbWVudHkBO0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAvc3BhY2UnLmFzomFzWEEzStHJIfiJ814D9uou9A7MGqhkAgd2p6gNHtR01DrG1HTn9JldgtspjI5pzMZoeFCGQzfk2BgfJ3ZvebEcFI0LHGF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4icbta2ccgjle5elh6xrt4e7nyphculci6kfend2h2fxfcqcdt527q", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWNidGEyY2NnamxlNWVsaDZ4cnQ0ZTdueXBoY3VsY2k2a2ZlbmQyaDJmeGZjcWNkdDUyN3EiLCJwcmYiOlsiYmFma3I0aWNidGEyY2NnamxlNWVsaDZ4cnQ0ZTdueXBoY3VsY2k2a2ZlbmQyaDJmeGZjcWNkdDUyN3EiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.oCsIqdGQET9cUPYx-8kltIqBBn8MR4EY9V1bjJE6TGXZchxt4KmlhtTTLD_N4S2OT6uywkLLf9LXbdyVqBTKBw", + "child_cid": "bafkr4ibxd7cfy4kbspzqciitt5deazvrgtedplygztsqogzrztjqb475x4", + "child_proof_cid": "bafkr4icbta2ccgjle5elh6xrt4e7nyphculci6kfend2h2fxfcqcdt527q" + }, + { + "encoding": "v0", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VxdGMxMDcxLXJldm9rZWQtdjBmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFNdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZjbVYyYjJ0bFpDMTJNQzlyZGlJNmV5SjBhVzU1WTJ4dmRXUXVhM1l2YkdsemRDSTZXM3Q5WFgwc0luUnBibmxqYkc5MVpEcHdhMmc2Wldsd01UVTFPakU2TUhnMFlUWXlNekUyTmpJellXUTBOVGRHTURKalJFTTFSRGs1TjJSbFJEWTNZVE00TTBWRE5UWTVPbkpsZG05clpXUXRkakF2YzNCaFkyVWlPbnNpZEdsdWVXTnNiM1ZrTG5Od1lXTmxMMmh2YzNRaU9sdDdmVjE5ZlN3aWNISm1JanBiWFgwaXN0YXRlbWVudHkBO0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAvc3BhY2UnLmFzomFzWEEzStHJIfiJ814D9uou9A7MGqhkAgd2p6gNHtR01DrG1HTn9JldgtspjI5pzMZoeFCGQzfk2BgfJ3ZvebEcFI0LAWF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4ich2ybsskzmjef2eexmb5cbqv473wd3hzmkjrtos3ijph4euzoixe", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWNoMnlic3Nrem1qZWYyZWV4bWI1Y2JxdjQ3M3dkM2h6bWtqcnRvczNpanBoNGV1em9peGUiLCJwcmYiOlsiYmFma3I0aWNoMnlic3Nrem1qZWYyZWV4bWI1Y2JxdjQ3M3dkM2h6bWtqcnRvczNpanBoNGV1em9peGUiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.7ILzcnU_3npegeefK-2JQmdDFDnuEiWQuZOVbt4dEv_BzgrhOmnCX86fl1FKrcOEYhpcuzhgTvmlF1DF15DACQ", + "child_cid": "bafkr4ia7cbisvvwuo46zhu6sqzcyivnaat3zkrm3gmlbqngtanruill6ea", + "child_proof_cid": "bafkr4ich2ybsskzmjef2eexmb5cbqv473wd3hzmkjrtos3ijph4euzoixe" + }, + { + "encoding": "map-order", + "header": "o2FzomFzWEEzStHJIfiJ814D9uou9A7MGqhkAgd2p6gNHtR01DrG1HTn9JldgtspjI5pzMZoeFCGQzfk2BgfJ3ZvebEcFI0LHGF0ZmVpcDE5MWFwqmNhdWR4OGRpZDprZXk6ejZNa21OTDZ4a3NkRUpFTkdrOVoxcXFHc1k1bmpLUWRqVTFVaG5VRVV2V3lRVVZYY2V4cHQyMTAwLTAxLTAxVDAwOjAwOjAwWmNpYXR0MjAyMC0wMS0wMVQwMDowMDowMFpjaXNzeDtkaWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OWVub25jZXF0YzEwNzEtcmV2b2tlZC12MGZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AU11cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmNtVjJiMnRsWkMxMk1DOXJkaUk2ZXlKMGFXNTVZMnh2ZFdRdWEzWXZiR2x6ZENJNlczdDlYWDBzSW5ScGJubGpiRzkxWkRwd2EyZzZaV2x3TVRVMU9qRTZNSGcwWVRZeU16RTJOakl6WVdRME5UZEdNREpqUkVNMVJEazVOMlJsUkRZM1lUTTRNMFZETlRZNU9uSmxkbTlyWldRdGRqQXZjM0JoWTJVaU9uc2lkR2x1ZVdOc2IzVmtMbk53WVdObEwyaHZjM1FpT2x0N2ZWMTlmU3dpY0hKbUlqcGJYWDBpc3RhdGVtZW50eQE7SSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12MC9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12MC9zcGFjZScuYWihYXRnZWlwNDM2MQ==", + "raw_cid": "bafkr4ie7ybmqqynccsui6tymx7y3i3o47y67a77f7ixutqn5xfjxkmhuai", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWU3eWJtcXF5bmNjc3VpNnR5bXg3eTNpM280N3k2N2E3N2Y3aXh1dHFuNXhmanhrbWh1YWkiLCJwcmYiOlsiYmFma3I0aWU3eWJtcXF5bmNjc3VpNnR5bXg3eTNpM280N3k2N2E3N2Y3aXh1dHFuNXhmanhrbWh1YWkiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.mraReiElTBdFY9bPMSfkwNftjOEy5V1KhxqTXROdkj44fdZ_ucP_syT4LlnvUJl7zMKmb4m2lf6pqfaHMXnmDg", + "child_cid": "bafkr4icl3s4ez332k4q5kv7l765ajhwjh2irqd5y6kvtpx53ikru243z2e", + "child_proof_cid": "bafkr4ie7ybmqqynccsui6tymx7y3i3o47y67a77f7ixutqn5xfjxkmhuai" + }, + { + "encoding": "nonminimal", + "header": "uANhaKFhdGdlaXA0MzYxYXCqY2F1ZHg4ZGlkOmtleTp6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlhjZXhwdDIxMDAtMDEtMDFUMDA6MDA6MDBaY2lhdHQyMDIwLTAxLTAxVDAwOjAwOjAwWmNpc3N4O2RpZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5ZW5vbmNlcXRjMTA3MS1yZXZva2VkLXYwZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBTXVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2Y21WMmIydGxaQzEyTUM5cmRpSTZleUowYVc1NVkyeHZkV1F1YTNZdmJHbHpkQ0k2VzN0OVhYMHNJblJwYm5samJHOTFaRHB3YTJnNlpXbHdNVFUxT2pFNk1IZzBZVFl5TXpFMk5qSXpZV1EwTlRkR01ESmpSRU0xUkRrNU4yUmxSRFkzWVRNNE0wVkROVFk1T25KbGRtOXJaV1F0ZGpBdmMzQmhZMlVpT25zaWRHbHVlV05zYjNWa0xuTndZV05sTDJodmMzUWlPbHQ3ZlYxOWZTd2ljSEptSWpwYlhYMGlzdGF0ZW1lbnR5ATtJIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYwL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYwL3NwYWNlJy5hc6Jhc1hBM0rRySH4ifNeA_bqLvQOzBqoZAIHdqeoDR7UdNQ6xtR05_SZXYLbKYyOaczGaHhQhkM35NgYHyd2b3mxHBSNCxxhdGZlaXAxOTE=", + "raw_cid": "bafkr4iaualygicsk3wj6q2pedhpli3wap2ii3q7ef7oxbwswmonjj6bo6y", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWF1YWx5Z2ljc2szd2o2cTJwZWRocGxpM3dhcDJpaTNxN2VmN294Yndzd21vbmpqNmJvNnkiLCJwcmYiOlsiYmFma3I0aWF1YWx5Z2ljc2szd2o2cTJwZWRocGxpM3dhcDJpaTNxN2VmN294Yndzd21vbmpqNmJvNnkiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.rRMOYHNkNlauZgcbc8oZujLqMg_rSKn2ZNHCt72rh-3KXskaXfCcmIEY5lFsD_8x8YjeBdi_5uQ2T-mN1G5nBw", + "child_cid": "bafkr4idtyyda3nk6t7fosq3gwa4d3y7upyj6y2izthg6oldl3piekellpq", + "child_proof_cid": "bafkr4iaualygicsk3wj6q2pedhpli3wap2ii3q7ef7oxbwswmonjj6bo6y" + }, + { + "encoding": "null", + "header": "o2FooWF0Z2VpcDQzNjFhcKtjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1NjljbmJm9mVub25jZXF0YzEwNzEtcmV2b2tlZC12MGZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AU11cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmNtVjJiMnRsWkMxMk1DOXJkaUk2ZXlKMGFXNTVZMnh2ZFdRdWEzWXZiR2x6ZENJNlczdDlYWDBzSW5ScGJubGpiRzkxWkRwd2EyZzZaV2x3TVRVMU9qRTZNSGcwWVRZeU16RTJOakl6WVdRME5UZEdNREpqUkVNMVJEazVOMlJsUkRZM1lUTTRNMFZETlRZNU9uSmxkbTlyWldRdGRqQXZjM0JoWTJVaU9uc2lkR2x1ZVdOc2IzVmtMbk53WVdObEwyaHZjM1FpT2x0N2ZWMTlmU3dpY0hKbUlqcGJYWDBpc3RhdGVtZW50eQE7SSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12MC9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12MC9zcGFjZScuYXOiYXNYQTNK0ckh-InzXgP26i70DswaqGQCB3anqA0e1HTUOsbUdOf0mV2C2ymMjmnMxmh4UIZDN-TYGB8ndm95sRwUjQscYXRmZWlwMTkx", + "raw_cid": "bafkr4idcgrtyubknenl5ue3whw7elkfzmam4r2rfxgdvxrjn5jh632lxwa", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWRjZ3J0eXVia25lbmw1dWUzd2h3N2Vsa2Z6bWFtNHIycmZ4Z2R2eHJqbjVqaDYzMmx4d2EiLCJwcmYiOlsiYmFma3I0aWRjZ3J0eXVia25lbmw1dWUzd2h3N2Vsa2Z6bWFtNHIycmZ4Z2R2eHJqbjVqaDYzMmx4d2EiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.tPb7ZAdKzbQz5k0aWRtnH9xSH09kcGSwjX39sgJH49AWJ23C9tOKziTv5Mz5w-SkA32if7TZmYteQ-R_mSeEAA", + "child_cid": "bafkr4ihxdctnunparnj7bnqqqrh6afvkkhbhjrwxckxuuplqvqbdkqdn3m", + "child_proof_cid": "bafkr4idcgrtyubknenl5ue3whw7elkfzmam4r2rfxgdvxrjn5jh632lxwa" + }, + { + "encoding": "issuer-case", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N2YwMmNkYzVkOTk3ZGVkNjdhMzgzZWM1Njllbm9uY2VxdGMxMDcxLXJldm9rZWQtdjBmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFNdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZjbVYyYjJ0bFpDMTJNQzlyZGlJNmV5SjBhVzU1WTJ4dmRXUXVhM1l2YkdsemRDSTZXM3Q5WFgwc0luUnBibmxqYkc5MVpEcHdhMmc2Wldsd01UVTFPakU2TUhnMFlUWXlNekUyTmpJellXUTBOVGRHTURKalJFTTFSRGs1TjJSbFJEWTNZVE00TTBWRE5UWTVPbkpsZG05clpXUXRkakF2YzNCaFkyVWlPbnNpZEdsdWVXTnNiM1ZrTG5Od1lXTmxMMmh2YzNRaU9sdDdmVjE5ZlN3aWNISm1JanBiWFgwaXN0YXRlbWVudHkBO0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAvc3BhY2UnLmFzomFzWEEzStHJIfiJ814D9uou9A7MGqhkAgd2p6gNHtR01DrG1HTn9JldgtspjI5pzMZoeFCGQzfk2BgfJ3ZvebEcFI0LHGF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4ibkq3meqj6uexlgibl3sgxzy4ynwme6g2yoa3i5wkhc5dhuchalai", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWJrcTNtZXFqNnVleGxnaWJsM3NneHp5NHlud21lNmcyeW9hM2k1d2toYzVkaHVjaGFsYWkiLCJwcmYiOlsiYmFma3I0aWJrcTNtZXFqNnVleGxnaWJsM3NneHp5NHlud21lNmcyeW9hM2k1d2toYzVkaHVjaGFsYWkiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjAva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.uSrxp4hfwGsV1TaDOO1ToOxxZxbfB_998-rb32kEaCaRCMemoDzJeECL_yErQVa7bwXO98PmPwycHfC2AoD5AA", + "child_cid": "bafkr4iboshxxvbpxufdea5w7keeruou3ujpvzculh2v5on5dp2anmlnqum", + "child_proof_cid": "bafkr4ibkq3meqj6uexlgibl3sgxzy4ynwme6g2yoa3i5wkhc5dhuchalai" + } + ], + "revocation_header": "o2FooWF0Z2VpcDQzNjFhcKhjYXVkeEB1Y2FuOmJhZmtyNGljaDJ5YnNza3ptamVmMmVleG1iNWNicXY0NzN3ZDNoem1ranJ0b3MzaWpwaDRldXpvaXhlY2V4cHQyMTAwLTAxLTAxVDAwOjAwOjAwWmNpYXR0MjAyMC0wMS0wMVQwMDowMDowMFpjaXNzeDtkaWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OWVub25jZXN0YzEwNzEtb3duZXItcmV2b2tlZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFhc6Jhc1hBRrUfvn7t2RXJE5Nq_w2P8CyYgQCdjWBy75eypgm9r3J4_NOY5WvAuVkK2iPwg7kFoFR1fWxiw1KlXM4t7bvloRxhdGZlaXAxOTE=", + "revoked_raw_cid": "bafkr4ich2ybsskzmjef2eexmb5cbqv473wd3hzmkjrtos3ijph4euzoixe" + }, + { + "generated_by": "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke", + "database": "revoked-v27.sqlite", + "encrypted": false, + "issuer": "did:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569", + "resource": "tinycloud:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569:revoked-v27/kv", + "session_seed": 11, + "child_seed": 12, + "session_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws", + "kty": "OKP", + "x": "Zr5-Myx6RTMyvZ0Kf32wVfXF7xoGraZtmLOftoEMRzo" + }, + "child_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "kty": "OKP", + "x": "C1E62bSSQBXKCQLtB5BE06xdvsIwbwaUjBDajrbjny0" + }, + "canonical_header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VydGMxMDcxLXJldm9rZWQtdjI3ZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBUHVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2Y21WMmIydGxaQzEyTWpjdmEzWWlPbnNpZEdsdWVXTnNiM1ZrTG10MkwyeHBjM1FpT2x0N2ZWMTlMQ0owYVc1NVkyeHZkV1E2Y0d0b09tVnBjREUxTlRveE9qQjROR0UyTWpNeE5qWXlNMkZrTkRVM1JqQXlZMFJETlVRNU9UZGtaVVEyTjJFek9ETkZRelUyT1RweVpYWnZhMlZrTFhZeU55OXpjR0ZqWlNJNmV5SjBhVzU1WTJ4dmRXUXVjM0JoWTJVdmFHOXpkQ0k2VzN0OVhYMTlMQ0p3Y21ZaU9sdGRmUWlzdGF0ZW1lbnR5AT1JIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYyNy9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12Mjcvc3BhY2UnLmFzomFzWEFH0ELQOIJzdX2mZ8X1Bo6ubJqZDxCwYqwiA6wjp2Rt2AjMgEXc17dA1KtldOU9dcw1uopF7OdHYx2k-K4OG6DaHGF0ZmVpcDE5MQ==", + "rows": [ + { + "encoding": "v27", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VydGMxMDcxLXJldm9rZWQtdjI3ZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBUHVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2Y21WMmIydGxaQzEyTWpjdmEzWWlPbnNpZEdsdWVXTnNiM1ZrTG10MkwyeHBjM1FpT2x0N2ZWMTlMQ0owYVc1NVkyeHZkV1E2Y0d0b09tVnBjREUxTlRveE9qQjROR0UyTWpNeE5qWXlNMkZrTkRVM1JqQXlZMFJETlVRNU9UZGtaVVEyTjJFek9ETkZRelUyT1RweVpYWnZhMlZrTFhZeU55OXpjR0ZqWlNJNmV5SjBhVzU1WTJ4dmRXUXVjM0JoWTJVdmFHOXpkQ0k2VzN0OVhYMTlMQ0p3Y21ZaU9sdGRmUWlzdGF0ZW1lbnR5AT1JIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYyNy9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12Mjcvc3BhY2UnLmFzomFzWEFH0ELQOIJzdX2mZ8X1Bo6ubJqZDxCwYqwiA6wjp2Rt2AjMgEXc17dA1KtldOU9dcw1uopF7OdHYx2k-K4OG6DaHGF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4iet2gsd5jtlobt27tskjvwvzwjah6wbrdz5uid7ie7auj37khus2q", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWV0MmdzZDVqdGxvYnQyN3Rza2p2d3Z6d2phaDZ3YnJkejV1aWQ3aWU3YXVqMzdraHVzMnEiLCJwcmYiOlsiYmFma3I0aWV0MmdzZDVqdGxvYnQyN3Rza2p2d3Z6d2phaDZ3YnJkejV1aWQ3aWU3YXVqMzdraHVzMnEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Ijp7InRpbnljbG91ZC5rdi9saXN0Ijpbe31dfX19.hiM2VKwqnDyfc2NLa07_lVED5-0vimWX3iMkpkiRxvOm7Fr2uGaZ1SOHmhRjjEWXIfaHxvtRnUEUxX5vNRb9AA", + "child_cid": "bafkr4ifeepza3pxa4sa5upwdtb5tyu4s7twpexluwcuxe5bk623436kmcy", + "child_proof_cid": "bafkr4iet2gsd5jtlobt27tskjvwvzwjah6wbrdz5uid7ie7auj37khus2q" + }, + { + "encoding": "v0", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VydGMxMDcxLXJldm9rZWQtdjI3ZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBUHVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2Y21WMmIydGxaQzEyTWpjdmEzWWlPbnNpZEdsdWVXTnNiM1ZrTG10MkwyeHBjM1FpT2x0N2ZWMTlMQ0owYVc1NVkyeHZkV1E2Y0d0b09tVnBjREUxTlRveE9qQjROR0UyTWpNeE5qWXlNMkZrTkRVM1JqQXlZMFJETlVRNU9UZGtaVVEyTjJFek9ETkZRelUyT1RweVpYWnZhMlZrTFhZeU55OXpjR0ZqWlNJNmV5SjBhVzU1WTJ4dmRXUXVjM0JoWTJVdmFHOXpkQ0k2VzN0OVhYMTlMQ0p3Y21ZaU9sdGRmUWlzdGF0ZW1lbnR5AT1JIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYyNy9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12Mjcvc3BhY2UnLmFzomFzWEFH0ELQOIJzdX2mZ8X1Bo6ubJqZDxCwYqwiA6wjp2Rt2AjMgEXc17dA1KtldOU9dcw1uopF7OdHYx2k-K4OG6DaAWF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4icdmivtzmkyu4fts5vglqvabggfuxhmfocrqcjoi24imqvwj22ipa", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWNkbWl2dHpta3l1NGZ0czV2Z2xxdmFiZ2dmdXhobWZvY3JxY2pvaTI0aW1xdndqMjJpcGEiLCJwcmYiOlsiYmFma3I0aWNkbWl2dHpta3l1NGZ0czV2Z2xxdmFiZ2dmdXhobWZvY3JxY2pvaTI0aW1xdndqMjJpcGEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Ijp7InRpbnljbG91ZC5rdi9saXN0Ijpbe31dfX19.rdTQdMdQKhtfvDPljQkuFVQ7Tf6D_Ou0T3eDI413ULlctt6uSZh79ZLlkGj0U9yMgxOG8L-bYKf04pB8meHWBQ", + "child_cid": "bafkr4ia6trz6edigj3c6kgzyu2nf3sxwotjrf57j63h36x4qhcr674jf3e", + "child_proof_cid": "bafkr4icdmivtzmkyu4fts5vglqvabggfuxhmfocrqcjoi24imqvwj22ipa" + }, + { + "encoding": "map-order", + "header": "o2FzomFzWEFH0ELQOIJzdX2mZ8X1Bo6ubJqZDxCwYqwiA6wjp2Rt2AjMgEXc17dA1KtldOU9dcw1uopF7OdHYx2k-K4OG6DaHGF0ZmVpcDE5MWFwqmNhdWR4OGRpZDprZXk6ejZNa21OTDZ4a3NkRUpFTkdrOVoxcXFHc1k1bmpLUWRqVTFVaG5VRVV2V3lRVVZYY2V4cHQyMTAwLTAxLTAxVDAwOjAwOjAwWmNpYXR0MjAyMC0wMS0wMVQwMDowMDowMFpjaXNzeDtkaWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OWVub25jZXJ0YzEwNzEtcmV2b2tlZC12MjdmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFQdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZjbVYyYjJ0bFpDMTJNamN2YTNZaU9uc2lkR2x1ZVdOc2IzVmtMbXQyTDJ4cGMzUWlPbHQ3ZlYxOUxDSjBhVzU1WTJ4dmRXUTZjR3RvT21WcGNERTFOVG94T2pCNE5HRTJNak14TmpZeU0yRmtORFUzUmpBeVkwUkROVVE1T1Rka1pVUTJOMkV6T0RORlF6VTJPVHB5WlhadmEyVmtMWFl5Tnk5emNHRmpaU0k2ZXlKMGFXNTVZMnh2ZFdRdWMzQmhZMlV2YUc5emRDSTZXM3Q5WFgxOUxDSndjbVlpT2x0ZGZRaXN0YXRlbWVudHkBPUkgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYyNy9zcGFjZScuYWihYXRnZWlwNDM2MQ==", + "raw_cid": "bafkr4if2gactlsgkw2g5qbmp6bgrtyof23ib4t5mbj7fob4h2of5imlf2i", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWYyZ2FjdGxzZ2t3Mmc1cWJtcDZiZ3J0eW9mMjNpYjR0NW1iajdmb2I0aDJvZjVpbWxmMmkiLCJwcmYiOlsiYmFma3I0aWYyZ2FjdGxzZ2t3Mmc1cWJtcDZiZ3J0eW9mMjNpYjR0NW1iajdmb2I0aDJvZjVpbWxmMmkiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Ijp7InRpbnljbG91ZC5rdi9saXN0Ijpbe31dfX19.5CiyHezOol9hPNEecIEvn2M4Y4lhLWav6S-qOY8Iy80JfbKSbFB3FdV_JW5qkTIDszTP0gz0klQPMUhcVkfVBg", + "child_cid": "bafkr4ibvr3kdsxotabwsmyknumju5wnounpjt4rkujiwcokjxbfc6fznqe", + "child_proof_cid": "bafkr4if2gactlsgkw2g5qbmp6bgrtyof23ib4t5mbj7fob4h2of5imlf2i" + }, + { + "encoding": "nonminimal", + "header": "uANhaKFhdGdlaXA0MzYxYXCqY2F1ZHg4ZGlkOmtleTp6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlhjZXhwdDIxMDAtMDEtMDFUMDA6MDA6MDBaY2lhdHQyMDIwLTAxLTAxVDAwOjAwOjAwWmNpc3N4O2RpZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5ZW5vbmNlcnRjMTA3MS1yZXZva2VkLXYyN2Zkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AVB1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmNtVjJiMnRsWkMxMk1qY3ZhM1lpT25zaWRHbHVlV05zYjNWa0xtdDJMMnhwYzNRaU9sdDdmVjE5TENKMGFXNTVZMnh2ZFdRNmNHdG9PbVZwY0RFMU5Ub3hPakI0TkdFMk1qTXhOall5TTJGa05EVTNSakF5WTBSRE5VUTVPVGRrWlVRMk4yRXpPRE5GUXpVMk9UcHlaWFp2YTJWa0xYWXlOeTl6Y0dGalpTSTZleUowYVc1NVkyeHZkV1F1YzNCaFkyVXZhRzl6ZENJNlczdDlYWDE5TENKd2NtWWlPbHRkZlFpc3RhdGVtZW50eQE9SSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12Mjcva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L3NwYWNlJy5hc6Jhc1hBR9BC0DiCc3V9pmfF9QaOrmyamQ8QsGKsIgOsI6dkbdgIzIBF3Ne3QNSrZXTlPXXMNbqKReznR2MdpPiuDhug2hxhdGZlaXAxOTE=", + "raw_cid": "bafkr4ibca72m7pqz6hrwennh6pjz3kgr233piiwmo5wbcewd3qavjs4j5q", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWJjYTcybTdwcXo2aHJ3ZW5uaDZwanoza2dyMjMzcGlpd21vNXdiY2V3ZDNxYXZqczRqNXEiLCJwcmYiOlsiYmFma3I0aWJjYTcybTdwcXo2aHJ3ZW5uaDZwanoza2dyMjMzcGlpd21vNXdiY2V3ZDNxYXZqczRqNXEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Ijp7InRpbnljbG91ZC5rdi9saXN0Ijpbe31dfX19.S1dAXYm5U00-1IJWDEkld5q853rIs_CuCPMGXsaBIhuHo1_KOLpf8zklSB34MWqJOFQlyeOBWi3gttnxuA0HDw", + "child_cid": "bafkr4iba3krxcssxawbw3cctekj4qm76fxkaafddx4tkpshhcenilpmwyu", + "child_proof_cid": "bafkr4ibca72m7pqz6hrwennh6pjz3kgr233piiwmo5wbcewd3qavjs4j5q" + }, + { + "encoding": "null", + "header": "o2FooWF0Z2VpcDQzNjFhcKtjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1NjljbmJm9mVub25jZXJ0YzEwNzEtcmV2b2tlZC12MjdmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFQdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZjbVYyYjJ0bFpDMTJNamN2YTNZaU9uc2lkR2x1ZVdOc2IzVmtMbXQyTDJ4cGMzUWlPbHQ3ZlYxOUxDSjBhVzU1WTJ4dmRXUTZjR3RvT21WcGNERTFOVG94T2pCNE5HRTJNak14TmpZeU0yRmtORFUzUmpBeVkwUkROVVE1T1Rka1pVUTJOMkV6T0RORlF6VTJPVHB5WlhadmEyVmtMWFl5Tnk5emNHRmpaU0k2ZXlKMGFXNTVZMnh2ZFdRdWMzQmhZMlV2YUc5emRDSTZXM3Q5WFgxOUxDSndjbVlpT2x0ZGZRaXN0YXRlbWVudHkBPUkgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYyNy9zcGFjZScuYXOiYXNYQUfQQtA4gnN1faZnxfUGjq5smpkPELBirCIDrCOnZG3YCMyARdzXt0DUq2V05T11zDW6ikXs50djHaT4rg4boNocYXRmZWlwMTkx", + "raw_cid": "bafkr4ifwunnetpgkrscldik5y3xqwgkonzz4xfnwjhxvlgw3tnzby7mydu", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWZ3dW5uZXRwZ2tyc2NsZGlrNXkzeHF3Z2tvbnp6NHhmbndqaHh2bGd3M3RuemJ5N215ZHUiLCJwcmYiOlsiYmFma3I0aWZ3dW5uZXRwZ2tyc2NsZGlrNXkzeHF3Z2tvbnp6NHhmbndqaHh2bGd3M3RuemJ5N215ZHUiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Ijp7InRpbnljbG91ZC5rdi9saXN0Ijpbe31dfX19.3DaWwG151yCaJt1bzPR69aoPAphN-gopdxcifCGLk2dbYHgeOs09DzDO4pXP2TvgQoKOOwQd7LCwD88Vfem7CQ", + "child_cid": "bafkr4igqbe2vgwofso3quiqdwxyzkmqvewhbg5hzftxtipbdrwbm5h4d4u", + "child_proof_cid": "bafkr4ifwunnetpgkrscldik5y3xqwgkonzz4xfnwjhxvlgw3tnzby7mydu" + }, + { + "encoding": "issuer-case", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N2YwMmNkYzVkOTk3ZGVkNjdhMzgzZWM1Njllbm9uY2VydGMxMDcxLXJldm9rZWQtdjI3ZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBUHVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2Y21WMmIydGxaQzEyTWpjdmEzWWlPbnNpZEdsdWVXTnNiM1ZrTG10MkwyeHBjM1FpT2x0N2ZWMTlMQ0owYVc1NVkyeHZkV1E2Y0d0b09tVnBjREUxTlRveE9qQjROR0UyTWpNeE5qWXlNMkZrTkRVM1JqQXlZMFJETlVRNU9UZGtaVVEyTjJFek9ETkZRelUyT1RweVpYWnZhMlZrTFhZeU55OXpjR0ZqWlNJNmV5SjBhVzU1WTJ4dmRXUXVjM0JoWTJVdmFHOXpkQ0k2VzN0OVhYMTlMQ0p3Y21ZaU9sdGRmUWlzdGF0ZW1lbnR5AT1JIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpyZXZva2VkLXYyNy9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6cmV2b2tlZC12Mjcvc3BhY2UnLmFzomFzWEFH0ELQOIJzdX2mZ8X1Bo6ubJqZDxCwYqwiA6wjp2Rt2AjMgEXc17dA1KtldOU9dcw1uopF7OdHYx2k-K4OG6DaHGF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4ifdkwgynitwvzbmpomrsi3ybb4jmmy7szujhnbwy6bgti5wbfrjgi", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWZka3dneW5pdHd2emJtcG9tcnNpM3liYjRqbW15N3N6dWpobmJ3eTZiZ3RpNXdiZnJqZ2kiLCJwcmYiOlsiYmFma3I0aWZka3dneW5pdHd2emJtcG9tcnNpM3liYjRqbW15N3N6dWpobmJ3eTZiZ3RpNXdiZnJqZ2kiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnJldm9rZWQtdjI3L2t2Ijp7InRpbnljbG91ZC5rdi9saXN0Ijpbe31dfX19.k50OT7kgryLb0NbYCr7BPG48fURSsUDQReJx3iC1I0Qr8Vi6e8t-IHVeFMqeNOjvOMLHhcn0-SW_Ry7U7crYBQ", + "child_cid": "bafkr4iagi5i3555ju4fnxd4eee4e42xvbfdeglqziogszsxp3j3s47ynx4", + "child_proof_cid": "bafkr4ifdkwgynitwvzbmpomrsi3ybb4jmmy7szujhnbwy6bgti5wbfrjgi" + } + ], + "revocation_header": "o2FooWF0Z2VpcDQzNjFhcKhjYXVkeEB1Y2FuOmJhZmtyNGlldDJnc2Q1anRsb2J0Mjd0c2tqdnd2endqYWg2d2JyZHo1dWlkN2llN2F1ajM3a2h1czJxY2V4cHQyMTAwLTAxLTAxVDAwOjAwOjAwWmNpYXR0MjAyMC0wMS0wMVQwMDowMDowMFpjaXNzeDtkaWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OWVub25jZXN0YzEwNzEtb3duZXItcmV2b2tlZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFhc6Jhc1hBAn8uvVPYmxTvr_R-8VdcAJcqOSBi0X8AwDj7Fybe5i4kTR_ubJyaF-z8-uzvRToiW5_EtikpfiPscGs68sb9pRxhdGZlaXAxOTE=", + "revoked_raw_cid": "bafkr4iet2gsd5jtlobt27tskjvwvzwjah6wbrdz5uid7ie7auj37khus2q" + }, + { + "generated_by": "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke", + "database": "active.sqlite", + "encrypted": false, + "issuer": "did:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569", + "resource": "tinycloud:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569:active/kv", + "session_seed": 11, + "child_seed": 12, + "session_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws", + "kty": "OKP", + "x": "Zr5-Myx6RTMyvZ0Kf32wVfXF7xoGraZtmLOftoEMRzo" + }, + "child_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "kty": "OKP", + "x": "C1E62bSSQBXKCQLtB5BE06xdvsIwbwaUjBDajrbjny0" + }, + "canonical_header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VtdGMxMDcxLWFjdGl2ZWZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AUJ1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNllXTjBhWFpsTDJ0MklqcDdJblJwYm5samJHOTFaQzVyZGk5c2FYTjBJanBiZTMxZGZTd2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2WVdOMGFYWmxMM053WVdObElqcDdJblJwYm5samJHOTFaQzV6Y0dGalpTOW9iM04wSWpwYmUzMWRmWDBzSW5CeVppSTZXMTE5aXN0YXRlbWVudHkBM0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6YWN0aXZlL3NwYWNlJy5hc6Jhc1hBX40pIGdLrZ9xKtVsdykwjGD0tsiKOyMscFZ_lTZh-SUFAyZYYScTeDPsF1ziyfKnYBY5wlKvnXqlZdWssJDHsRthdGZlaXAxOTE=", + "rows": [ + { + "encoding": "v27", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VtdGMxMDcxLWFjdGl2ZWZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AUJ1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNllXTjBhWFpsTDJ0MklqcDdJblJwYm5samJHOTFaQzVyZGk5c2FYTjBJanBiZTMxZGZTd2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2WVdOMGFYWmxMM053WVdObElqcDdJblJwYm5samJHOTFaQzV6Y0dGalpTOW9iM04wSWpwYmUzMWRmWDBzSW5CeVppSTZXMTE5aXN0YXRlbWVudHkBM0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6YWN0aXZlL3NwYWNlJy5hc6Jhc1hBX40pIGdLrZ9xKtVsdykwjGD0tsiKOyMscFZ_lTZh-SUFAyZYYScTeDPsF1ziyfKnYBY5wlKvnXqlZdWssJDHsRthdGZlaXAxOTE=", + "raw_cid": "bafkr4igepr6lpyqz3t6zicxoy26ldxhgn5hjnlgwvuqmcllwsydkgevrru", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWdlcHI2bHB5cXozdDZ6aWN4b3kyNmxkeGhnbjVoam5sZ3d2dXFtY2xsd3N5ZGtnZXZycnUiLCJwcmYiOlsiYmFma3I0aWdlcHI2bHB5cXozdDZ6aWN4b3kyNmxkeGhnbjVoam5sZ3d2dXFtY2xsd3N5ZGtnZXZycnUiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.IKuhZKd9LbNL1aan3QuYsf3ZyVj7ZG0YJKJSfRmQf7YxK9EXAUBg6peMh6dj0flOLjmAQZw_E30k1Sf9svx_DA", + "child_cid": "bafkr4igwd6iq7gs2z5fpfqqtlbkfeezdqyfq3y4jrqmlnomurj2ancvfcu", + "child_proof_cid": "bafkr4igepr6lpyqz3t6zicxoy26ldxhgn5hjnlgwvuqmcllwsydkgevrru" + }, + { + "encoding": "v0", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VtdGMxMDcxLWFjdGl2ZWZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AUJ1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNllXTjBhWFpsTDJ0MklqcDdJblJwYm5samJHOTFaQzVyZGk5c2FYTjBJanBiZTMxZGZTd2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2WVdOMGFYWmxMM053WVdObElqcDdJblJwYm5samJHOTFaQzV6Y0dGalpTOW9iM04wSWpwYmUzMWRmWDBzSW5CeVppSTZXMTE5aXN0YXRlbWVudHkBM0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6YWN0aXZlL3NwYWNlJy5hc6Jhc1hBX40pIGdLrZ9xKtVsdykwjGD0tsiKOyMscFZ_lTZh-SUFAyZYYScTeDPsF1ziyfKnYBY5wlKvnXqlZdWssJDHsQBhdGZlaXAxOTE=", + "raw_cid": "bafkr4igcmrv3kwshf4m737rusvvlyn2smll7rx3oy6trmcyewmc4expofa", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWdjbXJ2M2t3c2hmNG03MzdydXN2dmx5bjJzbWxsN3J4M295NnRybWN5ZXdtYzRleHBvZmEiLCJwcmYiOlsiYmFma3I0aWdjbXJ2M2t3c2hmNG03MzdydXN2dmx5bjJzbWxsN3J4M295NnRybWN5ZXdtYzRleHBvZmEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.t7emrc0AftuwdX7jA2VV3Hmj40RuTAkjFi7ZxAkXAdpcWYXgUygjEAj3IkukQ2wbVeDwTKHpa54oXDXKxWWYAA", + "child_cid": "bafkr4ifvrwxvjnpeo4w6733xh5ohox4wqnqqkblvy6w4jrlb3axc7z6tka", + "child_proof_cid": "bafkr4igcmrv3kwshf4m737rusvvlyn2smll7rx3oy6trmcyewmc4expofa" + }, + { + "encoding": "map-order", + "header": "o2FzomFzWEFfjSkgZ0utn3Eq1Wx3KTCMYPS2yIo7IyxwVn-VNmH5JQUDJlhhJxN4M-wXXOLJ8qdgFjnCUq-deqVl1aywkMexG2F0ZmVpcDE5MWFwqmNhdWR4OGRpZDprZXk6ejZNa21OTDZ4a3NkRUpFTkdrOVoxcXFHc1k1bmpLUWRqVTFVaG5VRVV2V3lRVVZYY2V4cHQyMTAwLTAxLTAxVDAwOjAwOjAwWmNpYXR0MjAyMC0wMS0wMVQwMDowMDowMFpjaXNzeDtkaWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OWVub25jZW10YzEwNzEtYWN0aXZlZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBQnVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2WVdOMGFYWmxMMnQySWpwN0luUnBibmxqYkc5MVpDNXJkaTlzYVhOMElqcGJlMzFkZlN3aWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZZV04wYVhabEwzTndZV05sSWpwN0luUnBibmxqYkc5MVpDNXpjR0ZqWlM5b2IzTjBJanBiZTMxZGZYMHNJbkJ5WmlJNlcxMTlpc3RhdGVtZW50eQEzSSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6YWN0aXZlL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTphY3RpdmUvc3BhY2UnLmFooWF0Z2VpcDQzNjE=", + "raw_cid": "bafkr4ihvjso7w2uw3ynkjduwfvrtpp55mggvkioofdhqf7acsqvxn5aw2a", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWh2anNvN3cydXczeW5ramR1d2Z2cnRwcDU1bWdndmtpb29mZGhxZjdhY3NxdnhuNWF3MmEiLCJwcmYiOlsiYmFma3I0aWh2anNvN3cydXczeW5ramR1d2Z2cnRwcDU1bWdndmtpb29mZGhxZjdhY3NxdnhuNWF3MmEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.XhDO_SfhNg9_zVJieeRCXLJmj0dtY8Ls2Mct1ek5c4dAAAySXJ7qBfzr74j9FFY6D6OvNsK50Bj6uvx3pd_0CA", + "child_cid": "bafkr4iafz57rlhgbh7xqis7wl3m7w4mchenvyvfulrqm5hbfd5k4jqiqay", + "child_proof_cid": "bafkr4ihvjso7w2uw3ynkjduwfvrtpp55mggvkioofdhqf7acsqvxn5aw2a" + }, + { + "encoding": "nonminimal", + "header": "uANhaKFhdGdlaXA0MzYxYXCqY2F1ZHg4ZGlkOmtleTp6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlhjZXhwdDIxMDAtMDEtMDFUMDA6MDA6MDBaY2lhdHQyMDIwLTAxLTAxVDAwOjAwOjAwWmNpc3N4O2RpZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5ZW5vbmNlbXRjMTA3MS1hY3RpdmVmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFCdXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZZV04wYVhabEwydDJJanA3SW5ScGJubGpiRzkxWkM1cmRpOXNhWE4wSWpwYmUzMWRmU3dpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNllXTjBhWFpsTDNOd1lXTmxJanA3SW5ScGJubGpiRzkxWkM1emNHRmpaUzlvYjNOMElqcGJlMzFkZlgwc0luQnlaaUk2VzExOWlzdGF0ZW1lbnR5ATNJIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTphY3RpdmUva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9zcGFjZScuYXOiYXNYQV-NKSBnS62fcSrVbHcpMIxg9LbIijsjLHBWf5U2YfklBQMmWGEnE3gz7Bdc4snyp2AWOcJSr516pWXVrLCQx7EbYXRmZWlwMTkx", + "raw_cid": "bafkr4igpofpucgtiwb2k2i5v7vymdon2a33z2e6ywck5wcfmqt3fth7ymq", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWdwb2ZwdWNndGl3YjJrMmk1djd2eW1kb24yYTMzejJlNnl3Y2s1d2NmbXF0M2Z0aDd5bXEiLCJwcmYiOlsiYmFma3I0aWdwb2ZwdWNndGl3YjJrMmk1djd2eW1kb24yYTMzejJlNnl3Y2s1d2NmbXF0M2Z0aDd5bXEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.hbVfmbELya_ssVNQRkQ227SdlGYR3KUshsU38NkpzZcmbCub6P-KsGsnUhf9nmhjyrDSne_PPwzgdKg5cTQkCQ", + "child_cid": "bafkr4ihsg3b63bldtjuk5l37wysfkvpg6nteygukdjhkbgdfvpxrxine74", + "child_proof_cid": "bafkr4igpofpucgtiwb2k2i5v7vymdon2a33z2e6ywck5wcfmqt3fth7ymq" + }, + { + "encoding": "null", + "header": "o2FooWF0Z2VpcDQzNjFhcKtjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1NjljbmJm9mVub25jZW10YzEwNzEtYWN0aXZlZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBQnVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2WVdOMGFYWmxMMnQySWpwN0luUnBibmxqYkc5MVpDNXJkaTlzYVhOMElqcGJlMzFkZlN3aWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZZV04wYVhabEwzTndZV05sSWpwN0luUnBibmxqYkc5MVpDNXpjR0ZqWlM5b2IzTjBJanBiZTMxZGZYMHNJbkJ5WmlJNlcxMTlpc3RhdGVtZW50eQEzSSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6YWN0aXZlL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTphY3RpdmUvc3BhY2UnLmFzomFzWEFfjSkgZ0utn3Eq1Wx3KTCMYPS2yIo7IyxwVn-VNmH5JQUDJlhhJxN4M-wXXOLJ8qdgFjnCUq-deqVl1aywkMexG2F0ZmVpcDE5MQ==", + "raw_cid": "bafkr4idpkfjdbywpvmvihbewwukacclgi4m7xmfpckhyzf26im2dhbanoq", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWRwa2ZqZGJ5d3B2bXZpaGJld3d1a2FjY2xnaTRtN3htZnBja2h5emYyNmltMmRoYmFub3EiLCJwcmYiOlsiYmFma3I0aWRwa2ZqZGJ5d3B2bXZpaGJld3d1a2FjY2xnaTRtN3htZnBja2h5emYyNmltMmRoYmFub3EiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.7aVu5sW9ghOVCzAI_uclSBuK3v8BbbDNBoV21dotuLOrn1_4HGbDyD5hVTPyiyfc6CC38p3mbB8ksevDC1jBBA", + "child_cid": "bafkr4iec5otgapfa6tgf6c7a7goctuuxew6kas4pwkjhugstnsxvc6zeuq", + "child_proof_cid": "bafkr4idpkfjdbywpvmvihbewwukacclgi4m7xmfpckhyzf26im2dhbanoq" + }, + { + "encoding": "issuer-case", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N2YwMmNkYzVkOTk3ZGVkNjdhMzgzZWM1Njllbm9uY2VtdGMxMDcxLWFjdGl2ZWZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AUJ1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNllXTjBhWFpsTDJ0MklqcDdJblJwYm5samJHOTFaQzVyZGk5c2FYTjBJanBiZTMxZGZTd2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2WVdOMGFYWmxMM053WVdObElqcDdJblJwYm5samJHOTFaQzV6Y0dGalpTOW9iM04wSWpwYmUzMWRmWDBzSW5CeVppSTZXMTE5aXN0YXRlbWVudHkBM0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6YWN0aXZlL3NwYWNlJy5hc6Jhc1hBX40pIGdLrZ9xKtVsdykwjGD0tsiKOyMscFZ_lTZh-SUFAyZYYScTeDPsF1ziyfKnYBY5wlKvnXqlZdWssJDHsRthdGZlaXAxOTE=", + "raw_cid": "bafkr4ifq37jmvwgbqrldmtpw2a44x2xhsnd7msqct65azlgwlth52j3kkq", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWZxMzdqbXZ3Z2JxcmxkbXRwdzJhNDR4Mnhoc25kN21zcWN0NjVhemxnd2x0aDUyajNra3EiLCJwcmYiOlsiYmFma3I0aWZxMzdqbXZ3Z2JxcmxkbXRwdzJhNDR4Mnhoc25kN21zcWN0NjVhemxnd2x0aDUyajNra3EiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmFjdGl2ZS9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.8ObvJ0tQ1RyDZPRavT71gCA_ToiK-mR3Gu36hXgj7VSkMNDYzeWcl--FvmMCaH2iQHEznnBJEqqi3e4IwH9PCg", + "child_cid": "bafkr4iayx6opr2vrxd2vsdg4c4xc2wgmde2to6p46s4ab35yitdntqnp4u", + "child_proof_cid": "bafkr4ifq37jmvwgbqrldmtpw2a44x2xhsnd7msqct65azlgwlth52j3kkq" + } + ], + "revocation_header": null, + "revoked_raw_cid": null + }, + { + "generated_by": "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke", + "database": "encrypted-active.sqlite", + "encrypted": true, + "issuer": "did:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569", + "resource": "tinycloud:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569:encrypted-active/kv", + "session_seed": 11, + "child_seed": 12, + "session_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws", + "kty": "OKP", + "x": "Zr5-Myx6RTMyvZ0Kf32wVfXF7xoGraZtmLOftoEMRzo" + }, + "child_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "kty": "OKP", + "x": "C1E62bSSQBXKCQLtB5BE06xdvsIwbwaUjBDajrbjny0" + }, + "canonical_header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2V3dGMxMDcxLWVuY3J5cHRlZC1hY3RpdmVmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFddXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZaVzVqY25sd2RHVmtMV0ZqZEdsMlpTOXJkaUk2ZXlKMGFXNTVZMnh2ZFdRdWEzWXZiR2x6ZENJNlczdDlYWDBzSW5ScGJubGpiRzkxWkRwd2EyZzZaV2x3TVRVMU9qRTZNSGcwWVRZeU16RTJOakl6WVdRME5UZEdNREpqUkVNMVJEazVOMlJsUkRZM1lUTTRNMFZETlRZNU9tVnVZM0o1Y0hSbFpDMWhZM1JwZG1VdmMzQmhZMlVpT25zaWRHbHVlV05zYjNWa0xuTndZV05sTDJodmMzUWlPbHQ3ZlYxOWZTd2ljSEptSWpwYlhYMGlzdGF0ZW1lbnR5AUdJIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL3NwYWNlJy5hc6Jhc1hBxLTM3jDkcqrwghg-W0277b5pFr1_-Qm9LmTS3iGoRnRB7XcK_C9VF1ltecteALKOEEOSD7sCNHtx7VCJUxnaVhxhdGZlaXAxOTE=", + "rows": [ + { + "encoding": "v27", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2V3dGMxMDcxLWVuY3J5cHRlZC1hY3RpdmVmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFddXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZaVzVqY25sd2RHVmtMV0ZqZEdsMlpTOXJkaUk2ZXlKMGFXNTVZMnh2ZFdRdWEzWXZiR2x6ZENJNlczdDlYWDBzSW5ScGJubGpiRzkxWkRwd2EyZzZaV2x3TVRVMU9qRTZNSGcwWVRZeU16RTJOakl6WVdRME5UZEdNREpqUkVNMVJEazVOMlJsUkRZM1lUTTRNMFZETlRZNU9tVnVZM0o1Y0hSbFpDMWhZM1JwZG1VdmMzQmhZMlVpT25zaWRHbHVlV05zYjNWa0xuTndZV05sTDJodmMzUWlPbHQ3ZlYxOWZTd2ljSEptSWpwYlhYMGlzdGF0ZW1lbnR5AUdJIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL3NwYWNlJy5hc6Jhc1hBxLTM3jDkcqrwghg-W0277b5pFr1_-Qm9LmTS3iGoRnRB7XcK_C9VF1ltecteALKOEEOSD7sCNHtx7VCJUxnaVhxhdGZlaXAxOTE=", + "raw_cid": "bafkr4ie3nlwvowcjklyyarnxanouzr35ztvnsbrhxdq4r2q36aibjhmtsm", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWUzbmx3dm93Y2prbHl5YXJueGFub3V6cjM1enR2bnNicmh4ZHE0cjJxMzZhaWJqaG10c20iLCJwcmYiOlsiYmFma3I0aWUzbmx3dm93Y2prbHl5YXJueGFub3V6cjM1enR2bnNicmh4ZHE0cjJxMzZhaWJqaG10c20iXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.MzaYCn-QGvDFbdJgCxgoJSxAHpds58oux79vmkuijSGFGeFnN66pYgAeHPRo_fmTrgP3d10KtxYarVRSW-Y0Cw", + "child_cid": "bafkr4iczzbo6s3cyge4wxuvzhnfjp74ewirrk3n76lwqoh54l7tsavssaa", + "child_proof_cid": "bafkr4ie3nlwvowcjklyyarnxanouzr35ztvnsbrhxdq4r2q36aibjhmtsm" + }, + { + "encoding": "v0", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2V3dGMxMDcxLWVuY3J5cHRlZC1hY3RpdmVmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFddXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZaVzVqY25sd2RHVmtMV0ZqZEdsMlpTOXJkaUk2ZXlKMGFXNTVZMnh2ZFdRdWEzWXZiR2x6ZENJNlczdDlYWDBzSW5ScGJubGpiRzkxWkRwd2EyZzZaV2x3TVRVMU9qRTZNSGcwWVRZeU16RTJOakl6WVdRME5UZEdNREpqUkVNMVJEazVOMlJsUkRZM1lUTTRNMFZETlRZNU9tVnVZM0o1Y0hSbFpDMWhZM1JwZG1VdmMzQmhZMlVpT25zaWRHbHVlV05zYjNWa0xuTndZV05sTDJodmMzUWlPbHQ3ZlYxOWZTd2ljSEptSWpwYlhYMGlzdGF0ZW1lbnR5AUdJIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL3NwYWNlJy5hc6Jhc1hBxLTM3jDkcqrwghg-W0277b5pFr1_-Qm9LmTS3iGoRnRB7XcK_C9VF1ltecteALKOEEOSD7sCNHtx7VCJUxnaVgFhdGZlaXAxOTE=", + "raw_cid": "bafkr4id4lgzbjkz6dk2adth2vznbvmhjgicy636d3hmik4eeq4rgjhsfqe", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWQ0bGd6YmprejZkazJhZHRoMnZ6bmJ2bWhqZ2ljeTYzNmQzaG1pazRlZXE0cmdqaHNmcWUiLCJwcmYiOlsiYmFma3I0aWQ0bGd6YmprejZkazJhZHRoMnZ6bmJ2bWhqZ2ljeTYzNmQzaG1pazRlZXE0cmdqaHNmcWUiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.1SCmi3hGgd2zkMk_WsEKM2EsK5xMZKzSpefWQYynPZH0KZ7nxoTuUOhh1u7-bsnhQVOldVUDjZuX4ZgYZreLBw", + "child_cid": "bafkr4igwhrj7anxwo7jugo5esmkxztdvqsujdvwk25oqymqsykqugeqbje", + "child_proof_cid": "bafkr4id4lgzbjkz6dk2adth2vznbvmhjgicy636d3hmik4eeq4rgjhsfqe" + }, + { + "encoding": "map-order", + "header": "o2FzomFzWEHEtMzeMORyqvCCGD5bTbvtvmkWvX_5Cb0uZNLeIahGdEHtdwr8L1UXWW15y14Aso4QQ5IPuwI0e3HtUIlTGdpWHGF0ZmVpcDE5MWFwqmNhdWR4OGRpZDprZXk6ejZNa21OTDZ4a3NkRUpFTkdrOVoxcXFHc1k1bmpLUWRqVTFVaG5VRVV2V3lRVVZYY2V4cHQyMTAwLTAxLTAxVDAwOjAwOjAwWmNpYXR0MjAyMC0wMS0wMVQwMDowMDowMFpjaXNzeDtkaWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OWVub25jZXd0YzEwNzEtZW5jcnlwdGVkLWFjdGl2ZWZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AV11cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNlpXNWpjbmx3ZEdWa0xXRmpkR2wyWlM5cmRpSTZleUowYVc1NVkyeHZkV1F1YTNZdmJHbHpkQ0k2VzN0OVhYMHNJblJwYm5samJHOTFaRHB3YTJnNlpXbHdNVFUxT2pFNk1IZzBZVFl5TXpFMk5qSXpZV1EwTlRkR01ESmpSRU0xUkRrNU4yUmxSRFkzWVRNNE0wVkROVFk1T21WdVkzSjVjSFJsWkMxaFkzUnBkbVV2YzNCaFkyVWlPbnNpZEdsdWVXTnNiM1ZrTG5Od1lXTmxMMmh2YzNRaU9sdDdmVjE5ZlN3aWNISm1JanBiWFgwaXN0YXRlbWVudHkBR0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUvc3BhY2UnLmFooWF0Z2VpcDQzNjE=", + "raw_cid": "bafkr4ig6ws4iybyh7eoy2u44thknjowql3vcugty4iamup545g7hbgzmga", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWc2d3M0aXlieWg3ZW95MnU0NHRoa25qb3dxbDN2Y3VndHk0aWFtdXA1NDVnN2hiZ3ptZ2EiLCJwcmYiOlsiYmFma3I0aWc2d3M0aXlieWg3ZW95MnU0NHRoa25qb3dxbDN2Y3VndHk0aWFtdXA1NDVnN2hiZ3ptZ2EiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.gmnF38Gu9tRjM22TgDo8psVLa_u_7RxOWj1KwPxi3SpRSCY9X0IM9_RHLH4qat3lagF4uutnnGdzOmKy1ERJAA", + "child_cid": "bafkr4icctajikgtxkxsmgmxdoow7vwzemhe63yvzz6mksj66tvwmomsyvu", + "child_proof_cid": "bafkr4ig6ws4iybyh7eoy2u44thknjowql3vcugty4iamup545g7hbgzmga" + }, + { + "encoding": "nonminimal", + "header": "uANhaKFhdGdlaXA0MzYxYXCqY2F1ZHg4ZGlkOmtleTp6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlhjZXhwdDIxMDAtMDEtMDFUMDA6MDA6MDBaY2lhdHQyMDIwLTAxLTAxVDAwOjAwOjAwWmNpc3N4O2RpZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5ZW5vbmNld3RjMTA3MS1lbmNyeXB0ZWQtYWN0aXZlZmRvbWFpbm50YzEwNzEuaW52YWxpZGZzY2hlbWVlaHR0cHNndmVyc2lvbgFpcmVzb3VyY2VzgXkBXXVybjpyZWNhcDpleUpoZEhRaU9uc2lkR2x1ZVdOc2IzVmtPbkJyYURwbGFYQXhOVFU2TVRvd2VEUmhOakl6TVRZMk1qTmhaRFExTjBZd01tTkVRelZFT1RrM1pHVkVOamRoTXpnelJVTTFOams2Wlc1amNubHdkR1ZrTFdGamRHbDJaUzlyZGlJNmV5SjBhVzU1WTJ4dmRXUXVhM1l2YkdsemRDSTZXM3Q5WFgwc0luUnBibmxqYkc5MVpEcHdhMmc2Wldsd01UVTFPakU2TUhnMFlUWXlNekUyTmpJellXUTBOVGRHTURKalJFTTFSRGs1TjJSbFJEWTNZVE00TTBWRE5UWTVPbVZ1WTNKNWNIUmxaQzFoWTNScGRtVXZjM0JoWTJVaU9uc2lkR2x1ZVdOc2IzVmtMbk53WVdObEwyaHZjM1FpT2x0N2ZWMTlmU3dpY0hKbUlqcGJYWDBpc3RhdGVtZW50eQFHSSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6ZW5jcnlwdGVkLWFjdGl2ZS9rdicuICgyKSAndGlueWNsb3VkLnNwYWNlJzogJ2hvc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6ZW5jcnlwdGVkLWFjdGl2ZS9zcGFjZScuYXOiYXNYQcS0zN4w5HKq8IIYPltNu-2-aRa9f_kJvS5k0t4hqEZ0Qe13CvwvVRdZbXnLXgCyjhBDkg-7AjR7ce1QiVMZ2lYcYXRmZWlwMTkx", + "raw_cid": "bafkr4iblgf2lftgfg2bfh4acvagvgtevm7uzh6frt6qz5wwpujciytexei", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWJsZ2YybGZ0Z2ZnMmJmaDRhY3ZhZ3ZndGV2bTd1emg2ZnJ0NnF6NXd3cHVqY2l5dGV4ZWkiLCJwcmYiOlsiYmFma3I0aWJsZ2YybGZ0Z2ZnMmJmaDRhY3ZhZ3ZndGV2bTd1emg2ZnJ0NnF6NXd3cHVqY2l5dGV4ZWkiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.YQc8YYRpqcQtAv2ODx6BbRcZj9SEPIdgKewhjStJ1vE_PYLFW3G3EeZoO3m-SD91g7yPjrRX4bUasnziyucxAg", + "child_cid": "bafkr4iarsm5gz4ltb6hbj5naimbxttrez4udk4rztln2eup4kya3coquhi", + "child_proof_cid": "bafkr4iblgf2lftgfg2bfh4acvagvgtevm7uzh6frt6qz5wwpujciytexei" + }, + { + "encoding": "null", + "header": "o2FooWF0Z2VpcDQzNjFhcKtjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1NjljbmJm9mVub25jZXd0YzEwNzEtZW5jcnlwdGVkLWFjdGl2ZWZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AV11cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNlpXNWpjbmx3ZEdWa0xXRmpkR2wyWlM5cmRpSTZleUowYVc1NVkyeHZkV1F1YTNZdmJHbHpkQ0k2VzN0OVhYMHNJblJwYm5samJHOTFaRHB3YTJnNlpXbHdNVFUxT2pFNk1IZzBZVFl5TXpFMk5qSXpZV1EwTlRkR01ESmpSRU0xUkRrNU4yUmxSRFkzWVRNNE0wVkROVFk1T21WdVkzSjVjSFJsWkMxaFkzUnBkbVV2YzNCaFkyVWlPbnNpZEdsdWVXTnNiM1ZrTG5Od1lXTmxMMmh2YzNRaU9sdDdmVjE5ZlN3aWNISm1JanBiWFgwaXN0YXRlbWVudHkBR0kgZnVydGhlciBhdXRob3JpemUgdGhlIHN0YXRlZCBVUkkgdG8gcGVyZm9ybSB0aGUgZm9sbG93aW5nIGFjdGlvbnMgb24gbXkgYmVoYWxmOiAoMSkgJ3RpbnljbG91ZC5rdic6ICdsaXN0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YnLiAoMikgJ3RpbnljbG91ZC5zcGFjZSc6ICdob3N0JyBmb3IgJ3RpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUvc3BhY2UnLmFzomFzWEHEtMzeMORyqvCCGD5bTbvtvmkWvX_5Cb0uZNLeIahGdEHtdwr8L1UXWW15y14Aso4QQ5IPuwI0e3HtUIlTGdpWHGF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4ickblwglcutsmnnoupey5pqetx7e422knpsqz52wuatqdns4n6xsu", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWNrYmx3Z2xjdXRzbW5ub3VwZXk1cHFldHg3ZTQyMmtucHNxejUyd3VhdHFkbnM0bjZ4c3UiLCJwcmYiOlsiYmFma3I0aWNrYmx3Z2xjdXRzbW5ub3VwZXk1cHFldHg3ZTQyMmtucHNxejUyd3VhdHFkbnM0bjZ4c3UiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.8dZag0r5qcV1RESnZLDFbZNxNDhmjDcTbyzHamdL-kHM8Gu4Jcj1jc33h2OXUkq49-OUK4i4CGeg5lLTYe_gDw", + "child_cid": "bafkr4iexc7bu7jfwv5q56aebiixshvm7o2kfps7gexbtdfd4vwag6brnja", + "child_proof_cid": "bafkr4ickblwglcutsmnnoupey5pqetx7e422knpsqz52wuatqdns4n6xsu" + }, + { + "encoding": "issuer-case", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N2YwMmNkYzVkOTk3ZGVkNjdhMzgzZWM1Njllbm9uY2V3dGMxMDcxLWVuY3J5cHRlZC1hY3RpdmVmZG9tYWlubnRjMTA3MS5pbnZhbGlkZnNjaGVtZWVodHRwc2d2ZXJzaW9uAWlyZXNvdXJjZXOBeQFddXJuOnJlY2FwOmV5SmhkSFFpT25zaWRHbHVlV05zYjNWa09uQnJhRHBsYVhBeE5UVTZNVG93ZURSaE5qSXpNVFkyTWpOaFpEUTFOMFl3TW1ORVF6VkVPVGszWkdWRU5qZGhNemd6UlVNMU5qazZaVzVqY25sd2RHVmtMV0ZqZEdsMlpTOXJkaUk2ZXlKMGFXNTVZMnh2ZFdRdWEzWXZiR2x6ZENJNlczdDlYWDBzSW5ScGJubGpiRzkxWkRwd2EyZzZaV2x3TVRVMU9qRTZNSGcwWVRZeU16RTJOakl6WVdRME5UZEdNREpqUkVNMVJEazVOMlJsUkRZM1lUTTRNMFZETlRZNU9tVnVZM0o1Y0hSbFpDMWhZM1JwZG1VdmMzQmhZMlVpT25zaWRHbHVlV05zYjNWa0xuTndZV05sTDJodmMzUWlPbHQ3ZlYxOWZTd2ljSEptSWpwYlhYMGlzdGF0ZW1lbnR5AUdJIGZ1cnRoZXIgYXV0aG9yaXplIHRoZSBzdGF0ZWQgVVJJIHRvIHBlcmZvcm0gdGhlIGZvbGxvd2luZyBhY3Rpb25zIG9uIG15IGJlaGFsZjogKDEpICd0aW55Y2xvdWQua3YnOiAnbGlzdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTplbmNyeXB0ZWQtYWN0aXZlL3NwYWNlJy5hc6Jhc1hBxLTM3jDkcqrwghg-W0277b5pFr1_-Qm9LmTS3iGoRnRB7XcK_C9VF1ltecteALKOEEOSD7sCNHtx7VCJUxnaVhxhdGZlaXAxOTE=", + "raw_cid": "bafkr4id74igjlkdt3z53iuf66yub5wfqywqdhle67isvgiws7swifgpg3u", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWQ3NGlnamxrZHQzejUzaXVmNjZ5dWI1d2ZxeXdxZGhsZTY3aXN2Z2l3czdzd2lmZ3BnM3UiLCJwcmYiOlsiYmFma3I0aWQ3NGlnamxrZHQzejUzaXVmNjZ5dWI1d2ZxeXdxZGhsZTY3aXN2Z2l3czdzd2lmZ3BnM3UiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OmVuY3J5cHRlZC1hY3RpdmUva3YiOnsidGlueWNsb3VkLmt2L2xpc3QiOlt7fV19fX0.iH9cmYZ5VrYrMWkAbiaQSwtXjpP55KwsJ1xaZ1Lt73xaBcsn3K3nXyM2voTwmE76CuFYNEb-uhkei1lKbjtdDQ", + "child_cid": "bafkr4ic5cnlbqye3kjphotkxmmtwdrlp6iamwlkbkq3nvzosggk7c2v5wm", + "child_proof_cid": "bafkr4id74igjlkdt3z53iuf66yub5wfqywqdhle67isvgiws7swifgpg3u" + } + ], + "revocation_header": null, + "revoked_raw_cid": null + }, + { + "generated_by": "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke", + "database": "single-v0.sqlite", + "encrypted": false, + "issuer": "did:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569", + "resource": "tinycloud:pkh:eip155:1:0x4a62316623ad457F02cDC5D997deD67a383EC569:single-v0/kv", + "session_seed": 11, + "child_seed": 12, + "session_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws", + "kty": "OKP", + "x": "Zr5-Myx6RTMyvZ0Kf32wVfXF7xoGraZtmLOftoEMRzo" + }, + "child_jwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "d": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "kty": "OKP", + "x": "C1E62bSSQBXKCQLtB5BE06xdvsIwbwaUjBDajrbjny0" + }, + "canonical_header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VwdGMxMDcxLXNpbmdsZS12MGZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AUp1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmMybHVaMnhsTFhZd0wydDJJanA3SW5ScGJubGpiRzkxWkM1cmRpOXNhWE4wSWpwYmUzMWRmU3dpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmMybHVaMnhsTFhZd0wzTndZV05sSWpwN0luUnBibmxqYkc5MVpDNXpjR0ZqWlM5b2IzTjBJanBiZTMxZGZYMHNJbkJ5WmlJNlcxMTlpc3RhdGVtZW50eQE5SSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6c2luZ2xlLXYwL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpzaW5nbGUtdjAvc3BhY2UnLmFzomFzWEEaCF9Lm_P7pVd3m_jo2MnoVbTC4oVB78PDIHUxfgx-aAbIb4eqlhpy8xlo2kTcU_V2G-76hQfa5akWEGRcAXBLG2F0ZmVpcDE5MQ==", + "rows": [ + { + "encoding": "v0", + "header": "o2FooWF0Z2VpcDQzNjFhcKpjYXVkeDhkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWGNleHB0MjEwMC0wMS0wMVQwMDowMDowMFpjaWF0dDIwMjAtMDEtMDFUMDA6MDA6MDBaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njllbm9uY2VwdGMxMDcxLXNpbmdsZS12MGZkb21haW5udGMxMDcxLmludmFsaWRmc2NoZW1lZWh0dHBzZ3ZlcnNpb24BaXJlc291cmNlc4F5AUp1cm46cmVjYXA6ZXlKaGRIUWlPbnNpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmMybHVaMnhsTFhZd0wydDJJanA3SW5ScGJubGpiRzkxWkM1cmRpOXNhWE4wSWpwYmUzMWRmU3dpZEdsdWVXTnNiM1ZrT25CcmFEcGxhWEF4TlRVNk1Ub3dlRFJoTmpJek1UWTJNak5oWkRRMU4wWXdNbU5FUXpWRU9UazNaR1ZFTmpkaE16Z3pSVU0xTmprNmMybHVaMnhsTFhZd0wzTndZV05sSWpwN0luUnBibmxqYkc5MVpDNXpjR0ZqWlM5b2IzTjBJanBiZTMxZGZYMHNJbkJ5WmlJNlcxMTlpc3RhdGVtZW50eQE5SSBmdXJ0aGVyIGF1dGhvcml6ZSB0aGUgc3RhdGVkIFVSSSB0byBwZXJmb3JtIHRoZSBmb2xsb3dpbmcgYWN0aW9ucyBvbiBteSBiZWhhbGY6ICgxKSAndGlueWNsb3VkLmt2JzogJ2xpc3QnIGZvciAndGlueWNsb3VkOnBraDplaXAxNTU6MToweDRhNjIzMTY2MjNhZDQ1N0YwMmNEQzVEOTk3ZGVENjdhMzgzRUM1Njk6c2luZ2xlLXYwL2t2Jy4gKDIpICd0aW55Y2xvdWQuc3BhY2UnOiAnaG9zdCcgZm9yICd0aW55Y2xvdWQ6cGtoOmVpcDE1NToxOjB4NGE2MjMxNjYyM2FkNDU3RjAyY0RDNUQ5OTdkZUQ2N2EzODNFQzU2OTpzaW5nbGUtdjAvc3BhY2UnLmFzomFzWEEaCF9Lm_P7pVd3m_jo2MnoVbTC4oVB78PDIHUxfgx-aAbIb4eqlhpy8xlo2kTcU_V2G-76hQfa5akWEGRcAXBLAGF0ZmVpcDE5MQ==", + "raw_cid": "bafkr4idd3ftxk42o2n3d4tct5nsojvoxu4it3vxc76kfl6lbvkfgvteddq", + "child_header": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiWnI1LU15eDZSVE15dlowS2YzMndWZlhGN3hvR3JhWnRtTE9mdG9FTVJ6byJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWttTkw2eGtzZEVKRU5HazlaMXFxR3NZNW5qS1FkalUxVWhuVUVVdld5UVVWWCN6Nk1rbU5MNnhrc2RFSkVOR2s5WjFxcUdzWTVuaktRZGpVMVVoblVFVXZXeVFVVlgiLCJhdWQiOiJkaWQ6a2V5Ono2TWtmRFNOUnMyaTlTNkxaNXZkNFJicFo2NzU0SDdSM2J0RHFSVGVIaGFtcXZ1SiIsIm5iZiI6MTU3NzgzNjgwMCwiZXhwIjo0MTAyNDQ0ODAwLCJubmMiOiJ0YzEwNzEtY2hpbGQtYmFma3I0aWRkM2Z0eGs0Mm8ybjNkNHRjdDVuc29qdm94dTRpdDN2eGM3NmtmbDZsYnZrZmd2dGVkZHEiLCJwcmYiOlsiYmFma3I0aWRkM2Z0eGs0Mm8ybjNkNHRjdDVuc29qdm94dTRpdDN2eGM3NmtmbDZsYnZrZmd2dGVkZHEiXSwiYXR0Ijp7InRpbnljbG91ZDpwa2g6ZWlwMTU1OjE6MHg0YTYyMzE2NjIzYWQ0NTdGMDJjREM1RDk5N2RlRDY3YTM4M0VDNTY5OnNpbmdsZS12MC9rdiI6eyJ0aW55Y2xvdWQua3YvbGlzdCI6W3t9XX19fQ.01MWBV0wF7E6wcs6_SiwZ_m1D840FUqr8a5KyXmGXLnsjLGqbnD5HX4mPy5Mt-gPMUEPijxQ2SK997yzV-eiAA", + "child_cid": "bafkr4ibsp3p4zjqg4qjt3hn7oqwwgy26xso4rdf3pejha25hhmj4m32dxy", + "child_proof_cid": "bafkr4idd3ftxk42o2n3d4tct5nsojvoxu4it3vxc76kfl6lbvkfgvteddq" + } + ], + "revocation_header": null, + "revoked_raw_cid": null + } +] \ No newline at end of file diff --git a/tinycloud-core/tests/fixtures/tc1071/generated/revoked-v0.sqlite b/tinycloud-core/tests/fixtures/tc1071/generated/revoked-v0.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..68156e31c35d2983bd71ef13c26aeed2d12a985d GIT binary patch literal 528384 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 Cg)%1q literal 0 HcmV?d00001 diff --git a/tinycloud-core/tests/fixtures/tc1071/generated/revoked-v27.sqlite b/tinycloud-core/tests/fixtures/tc1071/generated/revoked-v27.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..0acc6d5a159f0b51bcb3945a63961ed07a9b4987 GIT binary patch literal 528384 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 z z)sRrWKQUe`q(>8qMkbS{>Zie zY971aqvjGngm*jU@p^qY`Z=h3h`|rB+*;;2Byp|b>F?Ad5Xe#-fSS}ZjcFN6?*y^%Lf7TZ)6@7h)WXjdF9A%lF z&Zo1*Aj%%g2oG^^Q+I$LVmaA8)P4wT-2VTG*@qa**#RqJ$Kn~Y5=y2@xg<{)5{0sC z_e`N+8%#RrXDy|Q1(F;}K+Kf7y)rH&l96h&6BOM}4!tE~CQ_|1cEelKyOId><_yUWS$p`LdKWB310O5WWbJ(H@sF{_)(n>H&2YDGpjB*9zmCAo4nS0-kH zLNJ}Gm(g1gqrE(B2LgJ?>-BV5%#1X8r2vt4YQ2yzX7K{u%}1kx>JK3Q|E2}m{lM>b za`&#k?Hf{^c{p{w`_tY(oHr|-Zv6b&Z@lUaCp=%gOGco_7c1_W$#fl6N;;Nm4e-M;Z!6`nFvPkWwa`L9aXY=oa0sdfTm5fImk^<`2UHCLqwTLXXZ+3FNepHybvX@e2U}DK?9=*3oGa2Od6#ka&mEg zw$hDewZ69<9hByXT9;%Ux79Bv9I6px<)quwC=R4<9>=qxVGi*V7i9OUYu*#rzHr#NmzkTJ;9=w&Dx%Q`npT09SD!T`&hu{Q>5ftGDJ>;)H4-qyt{(n;9 z5Dna%;|q-2Tg`E9Ler=u+M2_-Gi)89G!bIo>?wrcF6r zD9tpuW-@_ZtP?5D1_EqIQ)&UyYh)@>q9fZrf3MKbgwQ*d$fSp})0|^;LL}|XdJurC=Q*k7JtzM2R8bzx}Qo$oTb}KYPj>-tmvW|Hw0yBmYf&bf);O^(Sx}PX5`Eqq2LT_CL!0 zPhn`o5efePLXYLDZMML&3QBN^esNrA_q#i!0!5=&dpBhw8_m_(be3pQTrJv@3(A1Y zqt`~&ovaYES(;^0hIuy17KNgUezAJx<=&ZnsQv#KGj9L?q~zV1jW~n3LOT?!mm71c zQOhy|Dox9#RP7?B3v MC(3iF5FP6Ne_d>6DF6Tf literal 0 HcmV?d00001 diff --git a/tinycloud-core/tests/fixtures/tc1071/generated/single-v0.sqlite b/tinycloud-core/tests/fixtures/tc1071/generated/single-v0.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..13dc768cf568f36bc141ac5ebcab650f7ff4430e GIT binary patch literal 487424 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 literal 0 HcmV?d00001 diff --git a/tinycloud-core/tests/fixtures/tc1071/generator.rs b/tinycloud-core/tests/fixtures/tc1071/generator.rs new file mode 100644 index 00000000..fb932ae0 --- /dev/null +++ b/tinycloud-core/tests/fixtures/tc1071/generator.rs @@ -0,0 +1,113 @@ +// Copy this unchanged into f9d700bf's tinycloud-core/tests/tc1071_generate.rs, +// alongside fixtures/tc1071/support.rs. The output directory must be empty. +#[path = "fixtures/tc1071/support.rs"] +mod support; +use support::*; +use tinycloud_auth::{authorization::{HeaderEncode, TinyCloudDelegation}, resource::SpaceId}; +use tinycloud_core::{encryption::ColumnEncryption, sea_orm::{EntityTrait, PaginatorTrait}, models::delegation}; + +#[tokio::test] +#[ignore = "Run only against unmodified f9d700bf to produce historical SQLite fixtures"] +async fn generate_historical_fixtures() { + let output = std::path::PathBuf::from(std::env::var_os("TC1071_FIXTURE_OUTPUT").expect("set TC1071_FIXTURE_OUTPUT to an empty owned fixture directory")); + std::fs::create_dir_all(&output).unwrap(); + let (_, _, issuer) = owner(); + let session_seed = 11; + let child_seed = 12; + let (jwk, audience, vm) = session(session_seed); + let (child_jwk, _, child_vm) = session(child_seed); + let mut fixtures = Vec::new(); + for (name, revoked_encoding, encrypted) in [ + ("revoked-v0", Some("v0"), false), + ("revoked-v27", Some("v27"), false), + ("active", None, false), + ("encrypted-active", None, true), + ] { + let path = output.join(format!("{name}.sqlite")); + assert!(!path.exists(), "never overwrite historical fixture {}", path.display()); + let db = open(&path).await.with_encryption(encrypted.then(|| ColumnEncryption::new(ENCRYPTION_KEY))); + let resource = SpaceId::new(issuer.parse().unwrap(), name.parse().unwrap()) + .to_resource("kv".parse().unwrap(), None, None, None).to_string(); + let cacao = signed_cacao(&audience, Some(&resource), &format!("tc1071-{name}")); + let canonical_header = TinyCloudDelegation::Cacao(Box::new(cacao.clone())).encode().unwrap(); + let mut rows = Vec::new(); + for (encoding, header) in variants(&cacao) { + let raw_cid = cid(&header); + // This assertion prevents inadvertently generating on fixed code. + let parsed = tinycloud_core::events::Delegation::from_header_ser::(&header).unwrap(); + assert_eq!(parsed.content_hash().to_cid(0x55).to_string(), raw_cid, "generator must run on original f9d700bf decoder"); + register(&db, &header).await; + assert_eq!(list(&db, &resource, &raw_cid, &jwk, &vm).await.unwrap(), Vec::::new()); + let child_header = child_header(&resource, &raw_cid, session_seed, child_seed); + let child_cid = cid(&child_header); + register(&db, &child_header).await; + assert_eq!(list(&db, &resource, &child_cid, &child_jwk, &child_vm).await.unwrap(), Vec::::new()); + println!("TC1071 old {name} {} before revoke: root=kv/list-ok child=kv/list-ok raw={raw_cid}", encoding); + rows.push(SignedRow { encoding, header, raw_cid: raw_cid.clone(), child_header, child_cid, child_proof_cid: raw_cid }); + } + assert_eq!(delegation::Entity::find().count(db.connection()).await.unwrap(), (rows.len() * 2) as u64, "both historical root representations and all real children must be persisted"); + println!("TC1071 old {name}: persisted {} real delegations", rows.len() * 2); + let revoked_raw_cid = revoked_encoding.map(|encoding| rows.iter().find(|row| row.encoding == encoding).unwrap().raw_cid.clone()); + let revocation_header = if let Some(target) = &revoked_raw_cid { + let header = revoke(&db, target).await; + for row in &rows { + let accepted = list(&db, &resource, &row.raw_cid, &jwk, &vm).await; + let child_accepted = list(&db, &resource, &row.child_cid, &child_jwk, &child_vm).await; + println!("TC1071 old {name} {} after revoke {target}: root={} child={}", row.encoding, accepted.is_ok(), child_accepted.is_ok()); + if &row.raw_cid == target { + assert!(accepted.is_err()); + assert!(child_accepted.is_err()); + } else { + assert!(accepted.is_ok(), "pre-fix equivalent {} must demonstrate authorization leak: {accepted:?}", row.encoding); + assert!(child_accepted.is_ok(), "pre-fix child of {} must demonstrate authorization leak: {child_accepted:?}", row.encoding); + } + } + Some(header) + } else { None }; + fixtures.push(Fixture { + generated_by: "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke".into(), + database: format!("{name}.sqlite"), encrypted, issuer: issuer.clone(), resource, + session_seed, child_seed, session_jwk: serde_json::to_value(&jwk).unwrap(), child_jwk: serde_json::to_value(&child_jwk).unwrap(), + canonical_header, rows, revocation_header, revoked_raw_cid, + }); + drop(db); + } + std::fs::write(output.join("manifest.json"), serde_json::to_vec_pretty(&fixtures).unwrap()).unwrap(); +} + +#[tokio::test] +#[ignore = "Append the single-legacy-row control using unmodified f9d700bf only"] +async fn generate_single_historical_fixture() { + let output = std::path::PathBuf::from(std::env::var_os("TC1071_FIXTURE_OUTPUT").unwrap()); + let mut fixtures: Vec = serde_json::from_slice(&std::fs::read(output.join("manifest.json")).unwrap()).unwrap(); + let database = "single-v0.sqlite"; + let path = output.join(database); + assert!(!path.exists(), "never overwrite a historical fixture"); + let db = open(&path).await; + let (_, _, issuer) = owner(); + let (jwk, audience, vm) = session(11); + let (child_jwk, _, child_vm) = session(12); + let resource = SpaceId::new(issuer.parse().unwrap(), "single-v0".parse().unwrap()).to_resource("kv".parse().unwrap(), None, None, None).to_string(); + let cacao = signed_cacao(&audience, Some(&resource), "tc1071-single-v0"); + let canonical_header = TinyCloudDelegation::Cacao(Box::new(cacao.clone())).encode().unwrap(); + let (encoding, header) = variants(&cacao).into_iter().find(|(encoding, _)| encoding == "v0").unwrap(); + let raw_cid = cid(&header); + assert_eq!(tinycloud_core::events::Delegation::from_header_ser::(&header).unwrap().content_hash().to_cid(0x55).to_string(), raw_cid); + register(&db, &header).await; + let child_header = child_header(&resource, &raw_cid, 11, 12); + let child_cid = cid(&child_header); + register(&db, &child_header).await; + assert_eq!(list(&db, &resource, &raw_cid, &jwk, &vm).await.unwrap(), Vec::::new()); + assert_eq!(list(&db, &resource, &child_cid, &child_jwk, &child_vm).await.unwrap(), Vec::::new()); + assert_eq!(delegation::Entity::find().count(db.connection()).await.unwrap(), 2); + println!("TC1071 old single-v0: persisted 2 real delegations; root=kv/list-ok child=kv/list-ok raw={raw_cid}; canonical row absent"); + fixtures.push(Fixture { + generated_by: "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke".into(), + database: database.into(), encrypted: false, issuer, resource, + session_seed: 11, child_seed: 12, session_jwk: serde_json::to_value(&jwk).unwrap(), child_jwk: serde_json::to_value(&child_jwk).unwrap(), + canonical_header, rows: vec![SignedRow { encoding, header, raw_cid: raw_cid.clone(), child_header, child_cid, child_proof_cid: raw_cid }], + revocation_header: None, revoked_raw_cid: None, + }); + drop(db); + std::fs::write(output.join("manifest.json"), serde_json::to_vec_pretty(&fixtures).unwrap()).unwrap(); +} diff --git a/tinycloud-core/tests/fixtures/tc1071/support.rs b/tinycloud-core/tests/fixtures/tc1071/support.rs new file mode 100755 index 00000000..05b7f4c5 --- /dev/null +++ b/tinycloud-core/tests/fixtures/tc1071/support.rs @@ -0,0 +1,331 @@ +#![allow(dead_code)] + +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use k256::ecdsa::SigningKey; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha3::{Digest, Keccak256}; +use std::{ + collections::BTreeMap, + sync::atomic::{AtomicU64, Ordering}, +}; +use tinycloud_auth::{ + authorization::{ + make_invocation, Cid, HeaderEncode, InvocationOptions, TinyCloudDelegation, + TinyCloudInvocation, TinyCloudRevocation, + }, + cacaos::{ + siwe::{Message, Version}, + siwe_cacao::{Header, Signature, SiweCacao}, + }, + ipld_core::ipld::Ipld, + resolver::DID_METHODS, + resource::ResourceId, + siwe_recap::{Ability, Capability}, + ssi::{ + claims::jwt::NumericDate, + jwk::{Algorithm, JWK}, + ucan::Payload, + }, + ucan_capabilities_object::Capabilities, +}; +use tinycloud_core::{ + db::{InvocationOutcome, SpaceDatabase}, + events::{Delegation, Invocation, Revocation}, + hash::{hash, Hash}, + keys::{public_key_to_did_key, Keypair, StaticSecret}, + sea_orm::{ConnectOptions, Database, DatabaseConnection}, + storage::memory::{MemoryStaging, MemoryStore}, +}; + +pub type TestDb = SpaceDatabase; +pub const EXPIRATION: f64 = 4_102_444_800.0; // 2100-01-01 +pub const OWNER_SEED: u8 = 7; +pub const ENCRYPTION_KEY: [u8; 32] = [43; 32]; +static NONCE: AtomicU64 = AtomicU64::new(0); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignedRow { + pub encoding: String, + pub header: String, + pub raw_cid: String, + pub child_header: String, + pub child_cid: String, + pub child_proof_cid: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Fixture { + pub generated_by: String, + pub database: String, + pub encrypted: bool, + pub issuer: String, + pub resource: String, + pub session_seed: u8, + pub child_seed: u8, + pub session_jwk: Value, + pub child_jwk: Value, + pub canonical_header: String, + pub rows: Vec, + pub revocation_header: Option, + pub revoked_raw_cid: Option, +} + +pub fn session(seed: u8) -> (JWK, String, String) { + let key = Keypair::ed25519_from_bytes([seed; 32]).unwrap(); + let ed = key.clone().try_into_ed25519().unwrap(); + let did = public_key_to_did_key(key.public()); + let jwk: JWK = serde_json::from_value(serde_json::json!({ + "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", + "x": URL_SAFE.encode(ed.public().to_bytes()).trim_end_matches('='), + "d": URL_SAFE.encode([seed; 32]).trim_end_matches('='), + })) + .unwrap(); + assert_eq!(DID_METHODS.generate(&jwk, "key").unwrap().to_string(), did); + let vm = format!("{did}#{}", did.trim_start_matches("did:key:")); + (jwk, did, vm) +} + +pub fn owner() -> (SigningKey, [u8; 20], String) { + let key = SigningKey::from_bytes(&[OWNER_SEED; 32].into()).unwrap(); + let public = key.verifying_key().to_encoded_point(false); + let address: [u8; 20] = Keccak256::digest(&public.as_bytes()[1..])[12..] + .try_into() + .unwrap(); + let issuer = format!( + "did:pkh:eip155:1:0x{}", + tinycloud_auth::cacaos::siwe::encode_eip55(&address) + ); + (key, address, issuer) +} + +pub fn signed_cacao(audience: &str, resource: Option<&str>, nonce: &str) -> SiweCacao { + let (key, address, _) = owner(); + let mut message = Message { + scheme: Some("https".parse().unwrap()), + domain: "tc1071.invalid".parse().unwrap(), + address, + statement: None, + uri: audience.parse().unwrap(), + version: Version::V1, + chain_id: 1, + nonce: nonce.into(), + issued_at: "2020-01-01T00:00:00Z".parse().unwrap(), + expiration_time: Some("2100-01-01T00:00:00Z".parse().unwrap()), + not_before: None, + request_id: None, + resources: vec![], + }; + if let Some(resource) = resource { + let mut recap = Capability::::new(); + recap.with_action( + resource.parse().unwrap(), + "tinycloud.kv/list".parse::().unwrap(), + [BTreeMap::new()], + ); + let owner_space = resource.replace("/kv", "/space"); + recap.with_action( + owner_space.parse().unwrap(), + "tinycloud.space/host".parse::().unwrap(), + [BTreeMap::new()], + ); + message = recap.build_message(message).unwrap(); + } + let (signature, recovery) = key + .sign_prehash_recoverable(&message.eip191_hash().unwrap()) + .unwrap(); + let mut bytes = [0; 65]; + bytes[..64].copy_from_slice(signature.to_bytes().as_ref()); + bytes[64] = u8::from(recovery) + 27; + SiweCacao::new(message.into(), Signature::from(bytes), Header) +} + +pub fn cid(header: &str) -> String { + let bytes = if header.contains('.') { + header.as_bytes().to_vec() + } else { + URL_SAFE.decode(header).unwrap() + }; + hash(&bytes).to_cid(0x55).to_string() +} + +pub fn current_cid(header: &str) -> String { + let (_, bytes) = TinyCloudDelegation::decode(header).unwrap(); + hash(&bytes).to_cid(0x55).to_string() +} + +pub fn variants(cacao: &SiweCacao) -> Vec<(String, String)> { + let canonical_header = TinyCloudDelegation::Cacao(Box::new(cacao.clone())) + .encode() + .unwrap(); + let canonical = URL_SAFE.decode(canonical_header).unwrap(); + let value: Ipld = serde_ipld_dagcbor::from_slice(&canonical).unwrap(); + let mut result = vec![("v27".into(), URL_SAFE.encode(&canonical))]; + let mut v0 = value.clone(); + if let Ipld::Map(root) = &mut v0 { + if let Ipld::Map(signature) = root.get_mut("s").unwrap() { + if let Ipld::Bytes(bytes) = signature.get_mut("s").unwrap() { + bytes[64] -= 27; + } else { + panic!("signature must be bytes"); + } + } + } + result.push(( + "v0".into(), + URL_SAFE.encode(serde_ipld_dagcbor::to_vec(&v0).unwrap()), + )); + // Encode the same three root entries in reverse DAG-CBOR order. + if let Ipld::Map(root) = &value { + let mut reordered = vec![0xa3]; + for key in ["s", "p", "h"] { + reordered.extend(serde_ipld_dagcbor::to_vec(&key).unwrap()); + reordered.extend(serde_ipld_dagcbor::to_vec(root.get(key).unwrap()).unwrap()); + } + result.push(("map-order".into(), URL_SAFE.encode(reordered))); + } + assert_eq!(canonical[0], 0xa3); + let mut nonminimal = vec![0xb8, 0x03]; + nonminimal.extend_from_slice(&canonical[1..]); + result.push(("nonminimal".into(), URL_SAFE.encode(nonminimal))); + let mut null = value.clone(); + if let Ipld::Map(root) = &mut null { + if let Ipld::Map(payload) = root.get_mut("p").unwrap() { + payload.insert("nbf".into(), Ipld::Null); + } + } + result.push(( + "null".into(), + URL_SAFE.encode(serde_ipld_dagcbor::to_vec(&null).unwrap()), + )); + let mut lowercase = value; + if let Ipld::Map(root) = &mut lowercase { + if let Ipld::Map(payload) = root.get_mut("p").unwrap() { + if let Ipld::String(issuer) = payload.get_mut("iss").unwrap() { + *issuer = issuer.to_lowercase(); + } + } + } + result.push(( + "issuer-case".into(), + URL_SAFE.encode(serde_ipld_dagcbor::to_vec(&lowercase).unwrap()), + )); + result +} + +pub fn child_header(resource: &str, proof: &str, session_seed: u8, child_seed: u8) -> String { + let (jwk, _, vm) = session(session_seed); + let (_, audience, _) = session(child_seed); + let mut caps = Capabilities::::new(); + caps.with_action( + resource.parse().unwrap(), + "tinycloud.kv/list".parse::().unwrap(), + [BTreeMap::new()], + ); + Payload:: { + issuer: vm.parse().unwrap(), + audience: audience.parse().unwrap(), + not_before: Some(NumericDate::try_from_seconds(1_577_836_800.0).unwrap()), + expiration: NumericDate::try_from_seconds(EXPIRATION).unwrap(), + nonce: Some(format!("tc1071-child-{proof}")), + facts: None, + proof: vec![proof.parse().unwrap()], + attenuation: caps, + } + .sign(Algorithm::EdDSA, &jwk) + .unwrap() + .encode() + .unwrap() +} + +pub async fn connection(path: &std::path::Path) -> DatabaseConnection { + Database::connect( + ConnectOptions::new(format!("sqlite:{}?mode=rwc", path.display())) + .max_connections(1) + .to_owned(), + ) + .await + .unwrap() +} + +pub async fn open(path: &std::path::Path) -> TestDb { + SpaceDatabase::new( + connection(path).await, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + ) + .await + .unwrap() +} + +pub async fn register(db: &TestDb, header: &str) { + db.delegate(Delegation::from_header_ser::(header).unwrap()) + .await + .map_err(|error| error.to_string()) + .unwrap(); +} + +pub async fn revoke(db: &TestDb, target: &str) -> String { + let header = TinyCloudRevocation::Cacao(Box::new(signed_cacao( + &format!("ucan:{target}"), + None, + "tc1071-owner-revoke", + ))) + .encode() + .unwrap(); + db.revoke(Revocation::from_header_ser::(&header).unwrap()) + .await + .map_err(|error| error.to_string()) + .unwrap(); + header +} + +pub async fn list( + db: &TestDb, + resource: &str, + proof: &str, + jwk: &JWK, + vm: &str, +) -> Result, String> { + let invocation = make_invocation( + [( + format!("{resource}/").parse::().unwrap(), + ["tinycloud.kv/list".parse::().unwrap()], + )], + &proof.parse::().unwrap(), + jwk, + vm, + EXPIRATION, + InvocationOptions { + nonce: Some(format!( + "tc1071-{}-{}", + std::process::id(), + NONCE.fetch_add(1, Ordering::Relaxed) + )), + ..Default::default() + }, + ) + .unwrap() + .encode() + .unwrap(); + let (_, outcomes) = db + .invoke::( + Invocation::from_header_ser::(&invocation).unwrap(), + Default::default(), + ) + .await + .map_err(|error| error.to_string())?; + assert_eq!(outcomes.len(), 1, "kv/list must actually execute"); + match outcomes.into_iter().next().unwrap() { + InvocationOutcome::KvList(keys, truncated, cursor) => { + assert!(!truncated); + assert!(cursor.is_none()); + Ok(keys.into_iter().map(|key| key.to_string()).collect()) + } + other => panic!("expected actual kv/list result, got {other:?}"), + } +} + +pub fn hash_from_cid(value: &str) -> Hash { + Hash::from(value.parse::().unwrap()) +} diff --git a/tinycloud-core/tests/fixtures/tc1071/upgrade_checks.rs b/tinycloud-core/tests/fixtures/tc1071/upgrade_checks.rs new file mode 100644 index 00000000..70d51fce --- /dev/null +++ b/tinycloud-core/tests/fixtures/tc1071/upgrade_checks.rs @@ -0,0 +1,396 @@ +use super::*; +use tinycloud_core::{ + db::InvocationOutcome, encryption::ColumnEncryption, events::Delegation, hash::hash, +}; + +fn assert_aliases(path: &Path, fixture: &Fixture) { + let connection = rusqlite::Connection::open(path).unwrap(); + let count: i64 = connection + .query_row("SELECT COUNT(*) FROM delegation_cid_alias", [], |row| { + row.get(0) + }) + .unwrap(); + assert_eq!( + count, + (fixture.rows.len() * 2) as i64, + "every real persisted CACAO and UCAN needs an alias" + ); + for row in &fixture.rows { + for (raw, canonical) in [ + (&row.raw_cid, current_cid(&row.header)), + (&row.child_cid, row.child_cid.clone()), + ] { + let id: Vec = hash_from_cid(raw).into(); + let canonical_bytes: Vec = connection + .query_row( + "SELECT canonical FROM delegation_cid_alias WHERE id = ?1", + [id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!( + canonical_bytes, + Vec::::from(hash_from_cid(&canonical)), + "alias must use exactly the current ingestion decoder bytes" + ); + } + } +} + +#[tokio::test] +async fn encrypted_boot_decrypts_exact_historical_records_and_preserves_revocation_equivalence() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.encrypted) + .unwrap(); + let (_directory, path) = copy_database(&fixture); + let before = signed_snapshot(&path); + let failed = SpaceDatabase::new_with_encryption( + connection(&path).await, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + Some(ColumnEncryption::new([44; 32])), + ) + .await; + assert!( + failed.is_err(), + "wrong encryption key must fail boot, never skip encrypted rows" + ); + assert_eq!(signed_snapshot(&path), before); + let db = SpaceDatabase::new_with_encryption( + connection(&path).await, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + Some(ColumnEncryption::new(ENCRYPTION_KEY)), + ) + .await + .unwrap(); + assert_signed_rows_untouched(&db, &fixture).await; + assert_aliases(&path, &fixture); + let (session, vm, child, child_vm) = signers(&fixture); + for row in &fixture.rows { + assert_eq!( + list(&db, &fixture.resource, &row.raw_cid, &session, &vm) + .await + .unwrap(), + Vec::::new() + ); + assert_eq!( + list(&db, &fixture.resource, &row.child_cid, &child, &child_vm) + .await + .unwrap(), + Vec::::new() + ); + } + assert_eq!( + signed_snapshot(&path), + before, + "decrypting at boot must not reencrypt or rewrite signed records" + ); + let target = &fixture + .rows + .iter() + .find(|row| row.encoding == "v0") + .unwrap() + .raw_cid; + revoke(&db, target).await; + assert_denied(&db, &fixture).await; + let after = signed_snapshot(&path); + assert_eq!(before[..2], after[..2]); + drop(db); + let db = SpaceDatabase::new_with_encryption( + connection(&path).await, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + Some(ColumnEncryption::new(ENCRYPTION_KEY)), + ) + .await + .unwrap(); + assert_denied(&db, &fixture).await; + assert_aliases(&path, &fixture); +} + +fn assert_raw(event: &Delegation, header: &str, advertised_cid: &str) { + let expected = if header.contains('.') { + header.as_bytes().to_vec() + } else { + URL_SAFE.decode(header).unwrap() + }; + assert_eq!( + event.serialized_bytes(), + expected, + "API must return the exact historically signed encoding" + ); + assert_eq!( + event.content_hash().to_cid(0x55).to_string(), + advertised_cid, + "advertised CID must hash the returned raw bytes, never re-encoded normalized CACAO" + ); + let returned_header = if header.contains('.') { + String::from_utf8(event.serialized_bytes().to_vec()).unwrap() + } else { + URL_SAFE.encode(event.serialized_bytes()) + }; + assert_eq!(returned_header, header); + assert_eq!( + hash(event.serialized_bytes()), + hash_from_cid(advertised_cid) + ); +} + +#[tokio::test] +async fn upgraded_list_and_chain_return_exact_historical_raw_headers_for_advertised_cids() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "active.sqlite") + .unwrap(); + let (_directory, path) = copy_database(&fixture); + let db = open(&path).await; + assert_aliases(&path, &fixture); + let (reader, resource) = reader_grant(&fixture); + let proof = current_cid(&reader); + register(&db, &reader).await; + let before = signed_snapshot(&path); + // Both the legacy parameter-free surface and the explicit list variant + // must retain raw bytes alongside their advertised persisted IDs. + for params in [ + None, + Some(serde_json::json!({"type":"list","filters":{"direction":"all"}})), + ] { + let outcomes = read_capabilities(&db, &fixture, resource.clone(), &proof, params).await; + let InvocationOutcome::OpenSessions(sessions) = outcomes.into_iter().next().unwrap() else { + panic!("expected actual capabilities list outcome") + }; + for row in &fixture.rows { + assert_raw( + sessions + .get(&hash_from_cid(&row.raw_cid)) + .expect("legacy root advertised"), + &row.header, + &row.raw_cid, + ); + let child = sessions + .get(&hash_from_cid(&row.child_cid)) + .expect("legacy child advertised"); + assert_raw(child, &row.child_header, &row.child_cid); + assert_eq!(child.0.parents, vec![row.child_proof_cid.parse().unwrap()]); + } + } + for row in &fixture.rows { + let outcomes = read_capabilities( + &db, + &fixture, + resource.clone(), + &proof, + Some(serde_json::json!({"type":"chain","delegation_cid":row.child_cid})), + ) + .await; + let InvocationOutcome::DelegationChain(chain) = outcomes.into_iter().next().unwrap() else { + panic!("expected actual capabilities chain outcome") + }; + assert_eq!(chain.len(), 2); + assert_raw(&chain[0], &row.child_header, &row.child_cid); + assert_raw(&chain[1], &row.header, &row.raw_cid); + assert_eq!( + chain[0].0.parents, + vec![row.child_proof_cid.parse().unwrap()] + ); + } + assert_eq!( + signed_snapshot(&path), + before, + "list and chain are read-only for immutable signed records and edges" + ); + let target = &fixture + .rows + .iter() + .find(|row| row.encoding == "v0") + .unwrap() + .raw_cid; + revoke(&db, target).await; + assert_denied(&db, &fixture).await; + for params in [ + None, + Some(serde_json::json!({"type":"list","filters":{"direction":"all"}})), + ] { + let outcomes = read_capabilities(&db, &fixture, resource.clone(), &proof, params).await; + let InvocationOutcome::OpenSessions(sessions) = outcomes.into_iter().next().unwrap() else { + panic!("expected actual capabilities list outcome") + }; + assert!( + sessions.contains_key(&hash_from_cid(&proof)), + "independent authorized reader grant remains active" + ); + for row in &fixture.rows { + assert!( + !sessions.contains_key(&hash_from_cid(&row.raw_cid)), + "list must hide equivalent revoked roots" + ); + let outcomes = read_capabilities( + &db, + &fixture, + resource.clone(), + &proof, + Some(serde_json::json!({"type":"chain","delegation_cid":row.child_cid})), + ) + .await; + let InvocationOutcome::DelegationChain(chain) = outcomes.into_iter().next().unwrap() + else { + panic!("expected actual capabilities chain outcome") + }; + for returned in chain { + assert!( + fixture + .rows + .iter() + .all(|root| returned.content_hash() != hash_from_cid(&root.raw_cid)), + "chain must not advertise an equivalent revoked root" + ); + assert_raw(&returned, &row.child_header, &row.child_cid); + } + } + } + println!("TC1071 upgraded active: default+filtered capabilities lists and all six chains preserve exact raw-header/CID correspondence; exclude equivalent revoked roots; independent reader grant remains authorized"); +} + +#[tokio::test] +async fn single_legacy_root_without_canonical_row_survives_import_then_revokes_as_one_identity() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "single-v0.sqlite") + .unwrap(); + let (_directory, path) = copy_database(&fixture); + let before = signed_snapshot(&path); + let db = open(&path).await; + assert_signed_rows_untouched(&db, &fixture).await; + assert_aliases(&path, &fixture); + let canonical = current_cid(&fixture.canonical_header); + assert!( + delegation::Entity::find_by_id(hash_from_cid(&canonical)) + .one(db.connection()) + .await + .unwrap() + .is_none(), + "canonical key must not require an existing delegation row" + ); + let row = &fixture.rows[0]; + let (session, vm, child, child_vm) = signers(&fixture); + assert_eq!( + list(&db, &fixture.resource, &row.raw_cid, &session, &vm) + .await + .unwrap(), + Vec::::new() + ); + assert_eq!( + list(&db, &fixture.resource, &row.child_cid, &child, &child_vm) + .await + .unwrap(), + Vec::::new() + ); + assert_eq!(signed_snapshot(&path), before); + register(&db, &fixture.canonical_header).await; + assert_eq!( + list(&db, &fixture.resource, &canonical, &session, &vm) + .await + .unwrap(), + Vec::::new() + ); + assert_eq!( + list(&db, &fixture.resource, &row.child_cid, &child, &child_vm) + .await + .unwrap(), + Vec::::new() + ); + let header = revoke(&db, &row.raw_cid).await; + let signed = Revocation::from_header_ser::(&header).unwrap(); + assert_eq!(signed.0.revoked.to_string(), row.raw_cid); + assert_denied(&db, &fixture).await; + register(&db, &row.header).await; + register(&db, &fixture.canonical_header).await; + let error = list(&db, &fixture.resource, &canonical, &session, &vm) + .await + .unwrap_err(); + assert!(error.contains("delegation-revoked"), "{error}"); + assert_denied(&db, &fixture).await; + assert_redelegation_denied(&db, &fixture).await; + assert_eq!( + delegation::Entity::find() + .count(db.connection()) + .await + .unwrap(), + 3 + ); + let after = signed_snapshot(&path); + for old_row in &before[0] { + assert!( + after[0].contains(old_row), + "old signed delegation row must remain unchanged" + ); + } + assert_eq!(before[1], after[1]); + println!("TC1071 upgraded single-v0: raw={} canonical={} old signed child={} preserved; canonical import and reimports cannot resurrect owner-revoked identity", row.raw_cid, canonical, row.child_cid); +} + +#[tokio::test] +async fn list_and_chain_preserve_the_exact_signed_ucan_proof_cid() { + use tinycloud_auth::authorization::Cid; + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "active.sqlite") + .unwrap(); + let (_directory, path) = copy_database(&fixture); + let db = open(&path).await; + let root = &fixture + .rows + .iter() + .find(|row| row.encoding == "v0") + .unwrap() + .raw_cid; + let root: Cid = root.parse().unwrap(); + // The graph stores hashes, but a signed proof is a complete CID. Changing + // its codec during response projection would rewrite the signed reference. + let signed_proof = Cid::new_v1(0x71, *root.hash()); + let header = child_header( + &fixture.resource, + &signed_proof.to_string(), + fixture.session_seed, + fixture.child_seed, + ); + let child = cid(&header); + register(&db, &header).await; + let (reader, resource) = reader_grant(&fixture); + let proof = current_cid(&reader); + register(&db, &reader).await; + let (_, _, child_key, child_vm) = signers(&fixture); + assert_eq!( + list(&db, &fixture.resource, &child, &child_key, &child_vm) + .await + .unwrap(), + Vec::::new() + ); + let outcomes = read_capabilities(&db, &fixture, resource.clone(), &proof, None).await; + let InvocationOutcome::OpenSessions(sessions) = outcomes.into_iter().next().unwrap() else { + panic!("expected capabilities list") + }; + let returned = sessions.get(&hash_from_cid(&child)).unwrap(); + assert_raw(returned, &header, &child); + assert_eq!(returned.0.parents, vec![signed_proof]); + let outcomes = read_capabilities( + &db, + &fixture, + resource, + &proof, + Some(serde_json::json!({"type":"chain","delegation_cid":child})), + ) + .await; + let InvocationOutcome::DelegationChain(chain) = outcomes.into_iter().next().unwrap() else { + panic!("expected capabilities chain") + }; + assert_raw(&chain[0], &header, &child); + assert_eq!(chain[0].0.parents, vec![signed_proof]); + revoke(&db, &root.to_string()).await; + let error = list(&db, &fixture.resource, &child, &child_key, &child_vm) + .await + .unwrap_err(); + assert!(error.contains("delegation-ancestor-revoked"), "{error}"); +} diff --git a/tinycloud-core/tests/historical_cacao_revocation.rs b/tinycloud-core/tests/historical_cacao_revocation.rs new file mode 100644 index 00000000..38143a80 --- /dev/null +++ b/tinycloud-core/tests/historical_cacao_revocation.rs @@ -0,0 +1,484 @@ +#[path = "fixtures/tc1071/support.rs"] +mod support; +#[path = "fixtures/tc1071/upgrade_checks.rs"] +mod upgrade_checks; + +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use rusqlite::types::Value as SqlValue; +use std::path::{Path, PathBuf}; +use support::*; +use tinycloud_auth::{ + authorization::{HeaderEncode, TinyCloudDelegation, TinyCloudRevocation}, + ssi::jwk::JWK, +}; +use tinycloud_core::{ + db::SpaceDatabase, + events::Revocation, + keys::StaticSecret, + models::{delegation, revocation}, + relationships::parent_delegations, + sea_orm::{ + ColumnTrait, ConnectionTrait, DbBackend, EntityTrait, PaginatorTrait, QueryFilter, + Statement, + }, + storage::memory::MemoryStore, +}; + +fn fixtures() -> Vec { + let bytes = std::fs::read(fixture_dir().join("manifest.json")).expect("real f9d700bf fixture manifest must be checked in; generate it with fixtures/tc1071/generator.rs"); + let fixtures: Vec = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(fixtures.len(), 5); + for fixture in &fixtures { + assert_eq!( + fixture.generated_by, + "f9d700bf public SpaceDatabase::new/delegate/revoke/invoke" + ); + assert_eq!( + fixture.rows.len(), + if fixture.database == "single-v0.sqlite" { + 1 + } else { + 6 + } + ); + for row in &fixture.rows { + assert_eq!(row.child_proof_cid, row.raw_cid); + } + } + fixtures +} + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tc1071/generated") +} + +fn copy_database(fixture: &Fixture) -> (tempfile::TempDir, PathBuf) { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join(&fixture.database); + std::fs::copy(fixture_dir().join(&fixture.database), &path).unwrap(); + (directory, path) +} + +// Snapshot only immutable signed records and edges; new alias/migration rows +// and ordinary new invocation events are deliberately outside this snapshot. +fn signed_snapshot(path: &Path) -> Vec>> { + let db = rusqlite::Connection::open(path).unwrap(); + [ + "SELECT * FROM delegation ORDER BY id", + "SELECT * FROM parent_delegation WHERE child IN (SELECT id FROM delegation) OR child IN (SELECT id FROM revocation) ORDER BY child, parent", + "SELECT * FROM revocation ORDER BY id", + ].into_iter().map(|sql| { + let mut statement = db.prepare(sql).unwrap(); + let columns = statement.column_count(); + let rows = statement.query_map([], |row| (0..columns).map(|column| row.get::<_, SqlValue>(column)).collect::>>()).unwrap(); + rows.collect::>>().unwrap() + }).collect() +} + +fn signers(fixture: &Fixture) -> (JWK, String, JWK, String) { + let (session, _, session_vm) = session(fixture.session_seed); + let (child, _, child_vm) = support::session(fixture.child_seed); + assert_eq!(serde_json::to_value(&session).unwrap(), fixture.session_jwk); + assert_eq!(serde_json::to_value(&child).unwrap(), fixture.child_jwk); + (session, session_vm, child, child_vm) +} + +async fn assert_signed_rows_untouched(db: &TestDb, fixture: &Fixture) { + assert_eq!( + delegation::Entity::find() + .count(db.connection()) + .await + .unwrap(), + (fixture.rows.len() * 2) as u64 + ); + let encryption = fixture + .encrypted + .then(|| tinycloud_core::encryption::ColumnEncryption::new(ENCRYPTION_KEY)); + for row in &fixture.rows { + let root = delegation::Entity::find_by_id(hash_from_cid(&row.raw_cid)) + .one(db.connection()) + .await + .unwrap() + .unwrap(); + let child = delegation::Entity::find_by_id(hash_from_cid(&row.child_cid)) + .one(db.connection()) + .await + .unwrap() + .unwrap(); + let root_bytes = + tinycloud_core::encryption::maybe_decrypt(encryption.as_ref(), &root.serialization) + .unwrap(); + assert_eq!(root_bytes, URL_SAFE.decode(&row.header).unwrap()); + let child_bytes = + tinycloud_core::encryption::maybe_decrypt(encryption.as_ref(), &child.serialization) + .unwrap(); + assert_eq!(child_bytes, row.child_header.as_bytes()); + let parents = parent_delegations::Entity::find() + .filter(parent_delegations::Column::Child.eq(child.id)) + .all(db.connection()) + .await + .unwrap(); + assert_eq!(parents.len(), 1); + assert_eq!(parents[0].parent, hash_from_cid(&row.child_proof_cid)); + let (decoded, bytes) = TinyCloudDelegation::decode(&row.child_header).unwrap(); + assert_eq!(bytes, row.child_header.as_bytes()); + match decoded { + TinyCloudDelegation::Ucan(ucan) => { + assert_eq!( + ucan.payload().proof, + vec![row.child_proof_cid.parse().unwrap()] + ); + assert_eq!(ucan.encode().unwrap(), row.child_header); + } + _ => panic!("historical child must remain its exact signed UCAN"), + } + } + if let (Some(header), Some(target)) = (&fixture.revocation_header, &fixture.revoked_raw_cid) { + let event = Revocation::from_header_ser::(header).unwrap(); + assert_eq!(event.0.revoked.to_string(), *target); + let stored = revocation::Entity::find_by_id(event.content_hash()) + .one(db.connection()) + .await + .unwrap() + .unwrap(); + assert_eq!(stored.revoked, hash_from_cid(target)); + assert_eq!(stored.serialization, URL_SAFE.decode(header).unwrap()); + } +} + +async fn assert_denied(db: &TestDb, fixture: &Fixture) { + let (session, vm, child, child_vm) = signers(fixture); + for row in &fixture.rows { + let error = list(db, &fixture.resource, &row.raw_cid, &session, &vm) + .await + .expect_err(&format!( + "historical equivalent {} still authorized after upgrade", + row.encoding + )); + assert!( + error.contains("delegation-revoked"), + "reject for revocation, not unrelated failure: {error}" + ); + let error = list(db, &fixture.resource, &row.child_cid, &child, &child_vm) + .await + .expect_err(&format!( + "child citing historical {} root still authorized after upgrade", + row.encoding + )); + assert!( + error.contains("delegation-ancestor-revoked"), + "child must reject for root revocation, not unrelated failure: {error}" + ); + for id in [&row.raw_cid, &row.child_cid] { + let status = db + .delegation_status(hash_from_cid(id), &fixture.issuer, &[]) + .await + .map_err(|error| error.to_string()) + .unwrap(); + assert_eq!( + status, + Some(tinycloud_core::db::DelegationStatus::Revoked), + "status must agree with actual invocation rejection for {id}" + ); + } + println!("TC1071 upgraded {} {}: root={} rejected delegation-revoked; child={} rejected delegation-ancestor-revoked; owner status=revoked", fixture.database, row.encoding, row.raw_cid, row.child_cid); + } +} + +#[tokio::test] +async fn upgrade_revoked_historical_equivalents_reject_real_kv_list_both_directions() { + for fixture in fixtures() + .into_iter() + .filter(|fixture| fixture.revoked_raw_cid.is_some()) + { + let (_directory, path) = copy_database(&fixture); + let before = signed_snapshot(&path); + let db = open(&path).await; + assert_signed_rows_untouched(&db, &fixture).await; + assert_denied(&db, &fixture).await; + for row in &fixture.rows { + register(&db, &row.header).await; + } + assert_denied(&db, &fixture).await; + assert_redelegation_denied(&db, &fixture).await; + assert_eq!( + signed_snapshot(&path), + before, + "upgrade and rejection must never rewrite signed historical records" + ); + } +} + +#[tokio::test] +async fn upgrade_active_legacy_children_remain_valid_until_owner_revokes_either_encoding() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "active.sqlite") + .unwrap(); + for encoding in [ + "v0", + "v27", + "map-order", + "nonminimal", + "null", + "issuer-case", + ] { + let (_directory, path) = copy_database(&fixture); + let before = signed_snapshot(&path); + let db = open(&path).await; + assert_signed_rows_untouched(&db, &fixture).await; + let (session, vm, child, child_vm) = signers(&fixture); + for row in &fixture.rows { + assert_eq!( + list(&db, &fixture.resource, &row.raw_cid, &session, &vm) + .await + .unwrap(), + Vec::::new() + ); + assert_eq!( + list(&db, &fixture.resource, &row.child_cid, &child, &child_vm) + .await + .unwrap(), + Vec::::new() + ); + } + assert_eq!( + signed_snapshot(&path), + before, + "valid old signed proof references must survive startup unchanged" + ); + let target = &fixture + .rows + .iter() + .find(|row| row.encoding == encoding) + .unwrap() + .raw_cid; + let header = revoke(&db, target).await; + let signed = Revocation::from_header_ser::(&header).unwrap(); + assert_eq!(signed.0.revoked.to_string(), *target); + assert_denied(&db, &fixture).await; + for row in &fixture.rows { + register(&db, &row.header).await; + } + assert_denied(&db, &fixture).await; + assert_redelegation_denied(&db, &fixture).await; + let after = signed_snapshot(&path); + assert_eq!( + before[..2], + after[..2], + "owner action must not rewrite roots or children" + ); + drop(db); + let db = open(&path).await; + assert_denied(&db, &fixture).await; + } +} + +#[tokio::test] +async fn new_writes_deduplicate_equivalent_cacao_and_revoke_the_single_canonical_row() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "active.sqlite") + .unwrap(); + let (session, vm, _, _) = signers(&fixture); + for encoding in ["v0", "v27"] { + let directory = tempfile::tempdir().unwrap(); + let db = open(&directory.path().join("new.sqlite")).await; + let canonical_cid = current_cid(&fixture.canonical_header); + for row in &fixture.rows { + assert_eq!( + current_cid(&row.header), + canonical_cid, + "same current decode bytes must define equivalence: {}", + row.encoding + ); + let event = tinycloud_core::events::Delegation::from_header_ser::( + &row.header, + ) + .unwrap(); + let receipt = db + .delegate(event) + .await + .map_err(|error| error.to_string()) + .unwrap(); + assert_eq!( + receipt.delegation_cids, + vec![hash_from_cid(&canonical_cid)], + "public ingestion receipt must advertise the retained canonical CID" + ); + } + assert_eq!( + delegation::Entity::find() + .count(db.connection()) + .await + .unwrap(), + 1 + ); + assert_eq!( + list(&db, &fixture.resource, &canonical_cid, &session, &vm) + .await + .unwrap(), + Vec::::new() + ); + let alias_count = db + .connection() + .query_one(Statement::from_string( + DbBackend::Sqlite, + "SELECT COUNT(*) AS n FROM delegation_cid_alias".to_owned(), + )) + .await + .unwrap() + .unwrap() + .try_get::("", "n") + .unwrap(); + assert_eq!(alias_count, 1); + let row = fixture + .rows + .iter() + .find(|row| row.encoding == encoding) + .unwrap(); + // New signatures target the CID the ingestion API actually retained. + assert_eq!(current_cid(&row.header), canonical_cid); + revoke(&db, &canonical_cid).await; + let error = list(&db, &fixture.resource, &canonical_cid, &session, &vm) + .await + .unwrap_err(); + assert!(error.contains("delegation-revoked"), "{error}"); + } +} + +#[tokio::test] +async fn encrypted_historical_rows_require_key_at_startup() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.encrypted) + .unwrap(); + let (_directory, path) = copy_database(&fixture); + let before = signed_snapshot(&path); + let result = SpaceDatabase::new( + connection(&path).await, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + ) + .await; + assert!( + result.is_err(), + "encrypted historical rows must fail boot without their decryption key" + ); + assert_eq!( + signed_snapshot(&path), + before, + "failed startup must not modify historical rows" + ); +} + +fn reader_grant(fixture: &Fixture) -> (String, tinycloud_auth::resource::ResourceId) { + use std::collections::BTreeMap; + use tinycloud_auth::{ + cacaos::{ + siwe::Message, + siwe_cacao::{Header, Signature, SiweCacao}, + }, + siwe_recap::{Ability, Capability}, + }; + let (_, audience, _) = session(fixture.session_seed); + let resource = fixture + .resource + .parse::() + .unwrap() + .space() + .clone() + .to_resource( + "capabilities".parse().unwrap(), + Some("all".parse().unwrap()), + None, + None, + ); + let mut recap = Capability::::new(); + recap.with_action( + resource.as_uri(), + "tinycloud.capabilities/read".parse::().unwrap(), + [BTreeMap::new()], + ); + let message: Message = signed_cacao(&audience, None, "tc1071-query") + .payload() + .clone() + .try_into() + .unwrap(); + let message = recap.build_message(message).unwrap(); + let (key, _, _) = owner(); + let (signature, recovery) = key + .sign_prehash_recoverable(&message.eip191_hash().unwrap()) + .unwrap(); + let mut bytes = [0; 65]; + bytes[..64].copy_from_slice(signature.to_bytes().as_ref()); + bytes[64] = u8::from(recovery) + 27; + let header = TinyCloudDelegation::Cacao(Box::new(SiweCacao::new( + message.into(), + Signature::from(bytes), + Header, + ))) + .encode() + .unwrap(); + (header, resource) +} + +async fn read_capabilities( + db: &TestDb, + fixture: &Fixture, + resource: tinycloud_auth::resource::ResourceId, + proof: &str, + params: Option, +) -> Vec< + tinycloud_core::db::InvocationOutcome< + ::Readable, + >, +> { + use tinycloud_auth::authorization::{make_invocation, InvocationOptions, TinyCloudInvocation}; + use tinycloud_core::{events::Invocation, storage::memory::MemoryStaging}; + let (jwk, _, vm) = session(fixture.session_seed); + let header = make_invocation( + [(resource, ["tinycloud.capabilities/read".parse().unwrap()])], + &proof.parse().unwrap(), + &jwk, + &vm, + EXPIRATION, + InvocationOptions { + facts: params.map(|params| vec![serde_json::json!({"capabilitiesReadParams": params})]), + ..Default::default() + }, + ) + .unwrap() + .encode() + .unwrap(); + let (_, outcomes) = db + .invoke::( + Invocation::from_header_ser::(&header).unwrap(), + Default::default(), + ) + .await + .map_err(|error| error.to_string()) + .unwrap(); + assert_eq!(outcomes.len(), 1); + outcomes +} + +async fn assert_redelegation_denied(db: &TestDb, fixture: &Fixture) { + use tinycloud_core::events::Delegation; + for row in &fixture.rows { + let direct = child_header(&fixture.resource, &row.raw_cid, fixture.session_seed, 13); + let error = db + .delegate(Delegation::from_header_ser::(&direct).unwrap()) + .await + .expect_err("revoked equivalent parent must not allow a new child") + .to_string(); + assert!(error.contains("delegation-parent-revoked"), "{error}"); + let descendant = child_header(&fixture.resource, &row.child_cid, fixture.child_seed, 13); + let error = db + .delegate(Delegation::from_header_ser::(&descendant).unwrap()) + .await + .expect_err("legacy child of revoked root must not allow a descendant") + .to_string(); + assert!(error.contains("delegation-ancestor-revoked"), "{error}"); + } +} diff --git a/tinycloud-node-server/src/auth_guards.rs b/tinycloud-node-server/src/auth_guards.rs index 18fe2980..755ccc1f 100644 --- a/tinycloud-node-server/src/auth_guards.rs +++ b/tinycloud-node-server/src/auth_guards.rs @@ -11,16 +11,9 @@ use rocket::{ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::time::Instant; -use tinycloud_auth::{ - authorization::{EncodingError, HeaderEncode}, - ipld_core::cid::Cid, - resource::SpaceId, -}; +use tinycloud_auth::{authorization::TinyCloudDelegation, ipld_core::cid::Cid, resource::SpaceId}; use tinycloud_core::{ - hash::Hash, - types::Metadata, - util::{Capability, DelegationInfo}, - InvocationOutcome, + events::Delegation, hash::Hash, types::Metadata, util::Capability, InvocationOutcome, }; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{info_span, Instrument}; @@ -250,7 +243,7 @@ where InvocationOutcome::DelegationChain(chain) => Json( chain .into_iter() - .map(|del| Ok(CapJsonRep::from_delegation(del)?)) + .map(CapJsonRep::from_delegation) .collect::>>() .map_err(|_| Status::InternalServerError)?, ) @@ -282,6 +275,93 @@ mod tests { use rocket::{get, http::Header, local::asynchronous::Client, routes}; use tinycloud_core::{hash::hash, KvBatchReadItem, KvBatchReadValue}; + type HistoricalDb = tinycloud_core::db::SpaceDatabase< + tinycloud_core::sea_orm::DatabaseConnection, + tinycloud_core::storage::memory::MemoryStore, + tinycloud_core::keys::StaticSecret, + >; + + #[get("/historical/?")] + async fn historical_delegation_response( + db: &rocket::State, + cid: &str, + chain: bool, + ) -> Result>>, Status> { + let cid: Cid = cid.parse().map_err(|_| Status::BadRequest)?; + let (_, event) = db + .load_signed_delegation(cid) + .await + .map_err(|_| Status::InternalServerError)? + .ok_or(Status::NotFound)?; + Ok(InvOut(if chain { + InvocationOutcome::DelegationChain(vec![event]) + } else { + InvocationOutcome::OpenSessions(HashMap::from([(Hash::from(cid), event)])) + })) + } + + #[tokio::test] + async fn historical_list_and_chain_json_preserve_raw_header_cids() -> Result<()> { + use tinycloud_core::{keys::StaticSecret, sea_orm::Database, storage::memory::MemoryStore}; + let fixture_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../tinycloud-core/tests/fixtures/tc1071/generated"); + let manifest: serde_json::Value = + serde_json::from_slice(&std::fs::read(fixture_dir.join("manifest.json"))?)?; + let fixture = manifest + .as_array() + .unwrap() + .iter() + .find(|fixture| fixture["database"] == "active.sqlite") + .unwrap(); + let directory = tempfile::tempdir()?; + let path = directory.path().join("caps.sqlite"); + std::fs::copy(fixture_dir.join("active.sqlite"), &path)?; + let db = HistoricalDb::new( + Database::connect(format!("sqlite:{}?mode=rwc", path.display())).await?, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + ) + .await?; + let client = Client::tracked( + rocket::build() + .manage(db) + .mount("/", routes![historical_delegation_response]), + ) + .await?; + for row in fixture["rows"].as_array().unwrap() { + for (id, header) in [("raw_cid", "header"), ("child_cid", "child_header")] { + let cid = row[id].as_str().unwrap(); + let header = row[header].as_str().unwrap(); + for chain in [false, true] { + let response = client + .get(format!("/historical/{cid}?chain={chain}")) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let json: serde_json::Value = + serde_json::from_str(&response.into_string().await.unwrap())?; + let record = if chain { &json[0] } else { &json[cid] }; + let raw = record["raw"].as_str().unwrap(); + assert_eq!(raw, header); + let bytes = if raw.contains('.') { + raw.as_bytes().to_vec() + } else { + base64::decode_config(raw, base64::URL_SAFE)? + }; + assert_eq!(hash(&bytes).to_cid(0x55).to_string(), cid); + if id == "child_cid" { + let parents: Vec = serde_json::from_value(record["parents"].clone())?; + assert_eq!( + parents, + vec![row["raw_cid"].as_str().unwrap().parse::()?] + ); + } + } + } + } + Ok(()) + } + #[get("/")] fn conditional_kv_response() -> KVResponse>> { let content = b"hello".to_vec(); @@ -520,13 +600,21 @@ pub struct CapJsonRep { } impl CapJsonRep { - pub fn from_delegation(d: DelegationInfo) -> Result { + /// Return the exact persisted header, whose bytes hash to the advertised + /// CID. Re-encoding a decoded historical CACAO would change that identity. + pub fn from_delegation(d: Delegation) -> Result { + let raw = match &d.0.delegation { + TinyCloudDelegation::Ucan(_) => std::str::from_utf8(d.serialized_bytes())?.to_owned(), + TinyCloudDelegation::Cacao(_) => { + base64::encode_config(d.serialized_bytes(), base64::URL_SAFE) + } + }; Ok(Self { - capabilities: d.capabilities, - delegator: d.delegator, - delegate: d.delegate, - parents: d.parents, - raw: d.delegation.encode()?, + capabilities: d.0.capabilities, + delegator: d.0.delegator, + delegate: d.0.delegate, + parents: d.0.parents, + raw, }) } } diff --git a/tinycloud-node-server/src/lib.rs b/tinycloud-node-server/src/lib.rs index 27d86000..c576932a 100644 --- a/tinycloud-node-server/src/lib.rs +++ b/tinycloud-node-server/src/lib.rs @@ -613,13 +613,13 @@ pub async fn app_with_control( encryption_backend, ); - let tinycloud = TinyCloud::new( + let tinycloud = TinyCloud::new_with_encryption( database_connection, tinycloud_config.storage.blocks.open().await?, key_setup.setup(()).await?, + Some(webhook_encryption.clone()), ) .await? - .with_encryption(Some(webhook_encryption.clone())) .with_sql_sizes(sql_sizes.clone()); let encryption_service = encryption_service.with_sqlite_writer_lock(tinycloud.sqlite_writer_lock()); diff --git a/tinycloud-node-server/src/share_v2.rs b/tinycloud-node-server/src/share_v2.rs index f3673fd6..c301958c 100644 --- a/tinycloud-node-server/src/share_v2.rs +++ b/tinycloud-node-server/src/share_v2.rs @@ -4532,26 +4532,30 @@ mod tests { let runtime = compose_test_runtime(&key_setup, Some(tee_context.clone()), true).await; let conn = runtime.conn.clone(); - actor::ActiveModel { - id: Set("did:key:tc349-restart".to_owned()), - } - .insert(&conn) - .await - .expect("actor row"); - let delegation_id = hash(b"tc349-restart-delegation"); - delegation::ActiveModel { - id: Set(delegation_id), - delegator: Set("did:key:tc349-restart".to_owned()), - delegatee: Set("did:key:tc349-restart".to_owned()), - expiry: Set(None), - issued_at: Set(None), - not_before: Set(None), - facts: Set(None), - serialization: Set(delegation_id.as_ref().to_vec()), - } - .insert(&conn) - .await - .expect("delegation row"); + use tinycloud_auth::{ + resolver::DID_METHODS, + ssi::jwk::{Algorithm, JWK}, + }; + let jwk = JWK::generate_ed25519().expect("local test signer"); + let did = DID_METHODS.generate(&jwk, "key").unwrap().to_string(); + let issuer = format!("{did}#{}", did.trim_start_matches("did:key:")); + let signed = crate::did_resolution_test_support::signed_ucan_with_proof( + &issuer, + &jwk, + Algorithm::EdDSA, + false, + ); + let delegation = + tinycloud_core::events::Delegation::from_header_ser::( + &signed.encode().unwrap(), + ) + .unwrap(); + runtime + .tinycloud + .delegate(delegation) + .await + .map_err(|error| error.to_string()) + .expect("valid signed delegation persisted before restart"); // Re-compose over the same, now-populated connection: the restart shape. let directory = tempfile::tempdir().expect("tempdir for local block store"); diff --git a/tinycloud-node-server/src/webhook_dispatcher.rs b/tinycloud-node-server/src/webhook_dispatcher.rs index fb70859b..02379bff 100644 --- a/tinycloud-node-server/src/webhook_dispatcher.rs +++ b/tinycloud-node-server/src/webhook_dispatcher.rs @@ -313,13 +313,13 @@ mod tests { let storage = NodeFileSystemConfig::new(tempdir.path()).open().await?; let _persisted = tempdir.keep(); let encryption = ColumnEncryption::new([9u8; 32]); - let tinycloud = TinyCloud::new( + let tinycloud = TinyCloud::new_with_encryption( db.clone().into(), Either::B(storage), StaticSecret::new(vec![0u8; 32]).unwrap(), + Some(encryption.clone()), ) - .await? - .with_encryption(Some(encryption.clone())); + .await?; Ok((tinycloud, db, encryption)) } @@ -540,13 +540,13 @@ mod tests { let tempdir = TempDir::new()?; let storage = NodeFileSystemConfig::new(tempdir.path()).open().await?; let _persisted = tempdir.keep(); - let tinycloud = TinyCloud::new( + let tinycloud = TinyCloud::new_with_encryption( db.clone().into(), Either::B(storage), StaticSecret::new(vec![0u8; 32]).unwrap(), + Some(encryption.clone()), ) - .await? - .with_encryption(Some(encryption.clone())); + .await?; let dispatcher = WebhookDispatcher::new(tinycloud, HooksConfig::default(), encryption)?; dispatcher.dispatch_due_once().await?; @@ -762,14 +762,14 @@ mod tests { .unwrap(); let _persisted = tempdir.keep(); let encryption = ColumnEncryption::new([9u8; 32]); - let tinycloud = TinyCloud::new( + let tinycloud = TinyCloud::new_with_encryption( conn, Either::B(storage), StaticSecret::new(vec![0u8; 32]).unwrap(), + Some(encryption.clone()), ) .await - .unwrap() - .with_encryption(Some(encryption.clone())); + .unwrap(); let mut setup_options = ConnectOptions::new(url.to_owned()); setup_options From 5c1c2487056080624505d6318cfd46db879cdab7 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 20:56:14 +0000 Subject: [PATCH 06/12] fix(sdk): validate signature arrays without temporary allocations --- dependencies/cacao/src/siwe_cacao.rs | 6 +++- .../tests/sdk_session_cid.rs | 32 +++++++------------ tinycloud-sdk-rs/src/serde_siwe.rs | 4 +-- tinycloud-sdk-wasm/src/session.rs | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/dependencies/cacao/src/siwe_cacao.rs b/dependencies/cacao/src/siwe_cacao.rs index e5a4abf7..03ccf645 100755 --- a/dependencies/cacao/src/siwe_cacao.rs +++ b/dependencies/cacao/src/siwe_cacao.rs @@ -222,7 +222,11 @@ impl AsRef<[u8]> for Signature { } impl Signature { - fn decode(mut bytes: [u8; 65]) -> Result { + /// Validate a fixed-size EIP-191 signature and canonicalize its recovery ID. + /// + /// Low-s signatures accept `0/1` and `27/28`; invalid scalars, high-s values + /// and other recovery IDs are rejected without allocating a byte buffer. + pub fn decode(mut bytes: [u8; 65]) -> Result { let signature = k256::ecdsa::Signature::from_slice(&bytes[..64]) .map_err(|_| SIWESignatureDecodeError::InvalidSignature)?; if signature.normalize_s().is_some() { diff --git a/tinycloud-node-server/tests/sdk_session_cid.rs b/tinycloud-node-server/tests/sdk_session_cid.rs index 000a1a13..66c0fc8c 100644 --- a/tinycloud-node-server/tests/sdk_session_cid.rs +++ b/tinycloud-node-server/tests/sdk_session_cid.rs @@ -255,36 +255,28 @@ fn wallet_import_and_rust_setup_reject_noncanonical_signatures() -> Result<()> { cases.push(("short".into(), bytes[..64].to_vec())); cases.push(("long".into(), [bytes.as_slice(), &[0]].concat())); for (label, bytes) in cases { - let node_error = Signature::try_from(bytes.clone()) - .expect_err("node decoder rejects fixture") - .to_string(); for prefix in ["", "0x"] { let signed = json_signed(&prepared, &format!("{prefix}{}", hex::encode(&bytes)))?; - let error = serde_json::from_value::(signed) - .err() - .with_context(|| format!("SDK accepted {label}"))?; ensure!( - error.to_string().contains(&node_error), - "{label}: SDK={error}; node={node_error}" + serde_json::from_value::(signed).is_err(), + "SDK accepted {label}" ); let host = json!({"siwe": prepared.siwe.to_string(), "signature": format!("{prefix}{}", hex::encode(&bytes))}); - let error = serde_json::from_value::(host) - .err() - .with_context(|| format!("host SDK accepted {label}"))?; ensure!( - error.to_string().contains(&node_error), - "host {label}: SDK={error}; node={node_error}" + serde_json::from_value::(host).is_err(), + "host SDK accepted {label}" ); } if let Ok(bytes) = <[u8; 65]>::try_from(bytes) { - let error = complete_session_setup(SignedSession { - session: prepared.clone(), - signature: Signature::from(bytes), - }) - .expect_err("Rust setup must validate before hashing"); ensure!( - error.to_string().contains(&node_error), - "Rust {label}: SDK={error}; node={node_error}" + matches!( + complete_session_setup(SignedSession { + session: prepared.clone(), + signature: Signature::from(bytes), + }), + Err(tinycloud_sdk_wasm::session::Error::InvalidSIWESignature(_)) + ), + "Rust setup accepted {label}" ); } } diff --git a/tinycloud-sdk-rs/src/serde_siwe.rs b/tinycloud-sdk-rs/src/serde_siwe.rs index 41e7dba5..5bfb79d9 100644 --- a/tinycloud-sdk-rs/src/serde_siwe.rs +++ b/tinycloud-sdk-rs/src/serde_siwe.rs @@ -49,8 +49,8 @@ pub mod signature { D: Deserializer<'de>, { let sig = Cow::<'_, str>::deserialize(d)?; - let bytes = >::from_hex(sig.strip_prefix("0x").unwrap_or(&sig)) + let bytes = <[u8; 65]>::from_hex(sig.strip_prefix("0x").unwrap_or(&sig)) .map_err(|e| D::Error::custom(format!("failed to parse SIWE signature: {e}")))?; - Signature::try_from(bytes).map_err(D::Error::custom) + Signature::decode(bytes).map_err(D::Error::custom) } } diff --git a/tinycloud-sdk-wasm/src/session.rs b/tinycloud-sdk-wasm/src/session.rs index 9b2811f2..3437b188 100644 --- a/tinycloud-sdk-wasm/src/session.rs +++ b/tinycloud-sdk-wasm/src/session.rs @@ -708,7 +708,7 @@ pub fn prepare_session(config: SessionConfig) -> Result /// The node still verifies the signature against the SIWE message and checks /// the delegation's authority; setup only validates the signature encoding. pub fn complete_session_setup(signed_session: SignedSession) -> Result { - let signature = Signature::try_from(signed_session.signature.as_ref().to_vec())?; + let signature = Signature::decode(*signed_session.signature)?; let delegation = SiweCacao::new(signed_session.session.siwe.into(), signature, SiweHeader); let serialised = serde_ipld_dagcbor::to_vec(&delegation)?; let hash = Code::Blake3_256.digest(&serialised); From 3af478809e97ae542aab06b10eafd94434b767cf Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 20:56:28 +0000 Subject: [PATCH 07/12] fix(core): serialize historical CID bootstrap across database boots --- CHANGELOG.md | 3 + docs/delegation-flow.md | 30 +++ .../m20261014_000000_delegation_cid_alias.rs | 24 ++- .../src/models/delegation_cid_alias.rs | 203 ++++++++++++++++-- 4 files changed, 229 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c970a561..d27d60a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Synchronize the standalone M1, W5 and load-signer Cargo locks with CACAO's `k256` dependency so `--locked` dependency resolution succeeds (TC-1071). - Historical CACAO revocations now apply to every stored encoding of the same delegation, in both directions (TC-1071). A persisted CID-alias index derives identities from current ingestion's canonical bytes, including recovery-ID, issuer-address/chain, and accepted CBOR representation normalization. Startup backfills aliases transactionally after loading the column encryption key and fails closed on undecodable or mismatched history or missing/wrong keys. Historical IDs, signed bytes, revocation targets, and signed child proof references remain unchanged; legacy list/chain responses retain their exact raw-header/CID relationship. This closes reuse of an already owner-signed grant under another accepted encoding; it does not involve forging new rights. +- CACAO issuer address casing and numeric chain spelling now serialize to the verifier's same EIP-55/u64 identity without changing the signed SIWE message. SDK session setup checks and normalizes wallet signatures before deriving its proof CID, including direct Rust sessions; low-v wallet signatures no longer produce `MissingParents` (TC-1071). +- Serialize concurrent PostgreSQL/MySQL historical backfills before their first history read; failed startup still rolls back aliases and preserves signed records. MySQL alias columns retain full variable-length multihashes, and encrypted-key validation avoids its unsupported limited `IN` subquery. Native alias checks do not waive the separately reproduced older MySQL 8.0.46 full-schema error 1074 (TC-1071). +- Rust API cutover: pass encryption through `SpaceDatabase::new_with_encryption`; list/chain outcomes carry serialized delegation wrappers. SDK exhaustive error matches must handle the new `session::Error::InvalidSIWESignature` variant (TC-1071). ## [1.20.6] - 2026-10-14 diff --git a/docs/delegation-flow.md b/docs/delegation-flow.md index b477d801..ddc62f98 100644 --- a/docs/delegation-flow.md +++ b/docs/delegation-flow.md @@ -409,6 +409,18 @@ failures, and plaintext hash mismatches fail startup rather than silently leaving an active alias. Completed backfills do not re-decode indexed history; one indexed encrypted record is checked on each boot to validate the key. +Concurrent PostgreSQL and MySQL boot transactions lock this migration's history +row before their first history read. This also prevents a waiting MySQL boot +from establishing a stale repeatable-read snapshot. SQLite keeps its existing +`BEGIN IMMEDIATE` writer serialization. Failures roll back the boot transaction; +alias conflicts are not ignored. MySQL stores complete, unpadded multihashes in +indexed `VARBINARY(255)` columns, and the one-row encrypted-key check uses a join +with an outer limit rather than MySQL's unsupported limited `IN` subquery. + +Stop old-version writers before upgrading the shared database. Older binaries +do not maintain the alias index; mixed-version writes are not a supported +security cutover. Do not accept requests until the new constructors complete. + Revocation status, invocation authorization, and ancestor checks union owner revocations across equivalent stored CIDs through indexed lookups. Historical delegation IDs, signed bytes, revocation targets, and child proof edges are @@ -428,6 +440,24 @@ parent CID fields retain the signed child references. Existing legacy list ancestor-visibility behavior is unchanged; authorization always checks the complete chain. +Rust callers must supply column encryption at construction through +`SpaceDatabase::new_with_encryption`, not a post-open setter. +`InvocationOutcome::OpenSessions` and `DelegationChain` retain serialized +delegation wrappers so callers can preserve the original headers rather than +re-encode them. SDK session setup validates and normalizes wallet signatures +before deriving its delegation CID, including direct Rust `SignedSession` +values that bypass JSON deserialization. Recovery IDs 0/1 and 27/28 therefore +produce the node's same proof CID; invalid encodings and high-s signatures are +rejected. Rust exhaustive matches must account for +`session::Error::InvalidSIWESignature`. + +MySQL coverage remains bounded: native MySQL 8.0.46 alias-migration, backfill, +concurrent-boot, and key-validation checks do not establish full-constructor +support. On the same `utf8mb4`/default-strict configuration, unmodified +`f9d700bf` fails the older initial schema with error 1074 on `id`, before the +alias migration exists. That separate full-schema prerequisite must be resolved +and exercised before claiming end-to-end support for that configuration. + ## Child Delegation (UCAN) Creation For sharing, child UCANs are created that delegate from the session key to another DID. diff --git a/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs b/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs index ed6484e9..349e3dd0 100644 --- a/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs +++ b/tinycloud-core/src/migrations/m20261014_000000_delegation_cid_alias.rs @@ -8,22 +8,24 @@ pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let mut id = ColumnDef::new(delegation_cid_alias::Column::Id); + let mut canonical = ColumnDef::new(delegation_cid_alias::Column::Canonical); + if manager.get_database_backend() == sea_orm::DbBackend::MySql { + // Encoded multihashes have variable length; MySQL's bare BINARY + // defaults to one byte, and fixed-width BINARY would pad the ID. + id.var_binary(255); + canonical.var_binary(255); + } else { + id.binary(); + canonical.binary(); + } manager .create_table( Table::create() .table(delegation_cid_alias::Entity) .if_not_exists() - .col( - ColumnDef::new(delegation_cid_alias::Column::Id) - .binary() - .not_null() - .primary_key(), - ) - .col( - ColumnDef::new(delegation_cid_alias::Column::Canonical) - .binary() - .not_null(), - ) + .col(id.not_null().primary_key()) + .col(canonical.not_null()) .col( ColumnDef::new(delegation_cid_alias::Column::Encrypted) .boolean() diff --git a/tinycloud-core/src/models/delegation_cid_alias.rs b/tinycloud-core/src/models/delegation_cid_alias.rs index 985d0f6f..9f320a24 100644 --- a/tinycloud-core/src/models/delegation_cid_alias.rs +++ b/tinycloud-core/src/models/delegation_cid_alias.rs @@ -4,13 +4,15 @@ use std::borrow::Cow; use base64::{engine::general_purpose::URL_SAFE, Engine as _}; use sea_orm::{ - entity::prelude::*, sea_query::Query, ActiveValue::Set, ConnectionTrait, QueryOrder, + entity::prelude::*, sea_query::Query, ActiveValue::Set, ConnectionTrait, JoinType, QueryOrder, QuerySelect, }; +use sea_orm_migration::{seaql_migrations, MigrationName}; use tinycloud_auth::authorization::{HeaderEncode, TinyCloudDelegation}; use crate::encryption::{maybe_decrypt, ColumnEncryption}; use crate::hash::{hash, Hash}; +use crate::migrations::m20261014_000000_delegation_cid_alias::Migration; use crate::models::delegation; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] @@ -64,17 +66,12 @@ async fn validate_encryption( db: &C, encryption: Option<&ColumnEncryption>, ) -> Result<(), DbErr> { - let encrypted_id = Query::select() - .column(Column::Id) - .from(Entity) - .and_where(Expr::col(Column::Encrypted).eq(true)) - .limit(1) - .to_owned(); let Some((id, stored)) = delegation::Entity::find() .select_only() .column(delegation::Column::Id) .column(delegation::Column::Serialization) - .filter(delegation::Column::Id.in_subquery(encrypted_id)) + .join_rev(JoinType::InnerJoin, Relation::Delegation.def()) + .filter(Column::Encrypted.eq(true)) .into_tuple::<(Hash, Vec)>() .one(db) .await? @@ -109,6 +106,24 @@ pub(crate) async fn backfill( encryption: Option<&ColumnEncryption>, ) -> Result<(), DbErr> { const PAGE_SIZE: u64 = 128; + if db.get_database_backend() != sea_orm::DbBackend::Sqlite { + // Serialize boot transactions before the first history read. In + // particular, MySQL must not establish a stale repeatable-read snapshot + // before waiting for another boot's aliases. SQLite's caller already + // holds its BEGIN IMMEDIATE writer lock. + let migration = seaql_migrations::Entity::find_by_id(Migration.name()) + .select_only() + .column(seaql_migrations::Column::AppliedAt) + .lock_exclusive() + .into_tuple::() + .one(db) + .await?; + if migration.is_none() { + return Err(DbErr::Custom( + "delegation CID alias backfill: migration record is missing".into(), + )); + } + } let mut after = None; loop { let mut query = delegation::Entity::find() @@ -274,13 +289,9 @@ mod tests { ) .await; - let missing = backfill(&db, None).await.unwrap_err(); - assert!(missing - .to_string() - .contains("requires a column encryption key")); + assert!(backfill(&db, None).await.is_err()); let wrong = ColumnEncryption::new([0x43; 32]); - let rejected = backfill(&db, Some(&wrong)).await.unwrap_err(); - assert!(rejected.to_string().contains("decryption failed")); + assert!(backfill(&db, Some(&wrong)).await.is_err()); assert_eq!(Entity::find().count(&db).await.unwrap(), 0); } @@ -288,10 +299,7 @@ mod tests { async fn mismatched_plaintext_hash_is_not_aliased() { let db = database().await; insert_delegation(&db, hash(b"different content"), ucan_serialization()).await; - let error = backfill(&db, None).await.unwrap_err(); - assert!(error - .to_string() - .contains("plaintext content hash does not match stored ID")); + assert!(backfill(&db, None).await.is_err()); assert_eq!(Entity::find().count(&db).await.unwrap(), 0); } @@ -301,8 +309,7 @@ mod tests { let serialization = vec![0xa0]; insert_delegation(&db, hash(&serialization), serialization).await; let transaction = db.begin().await.unwrap(); - let error = backfill(&transaction, None).await.unwrap_err(); - assert!(error.to_string().contains("delegation decoding failed")); + assert!(backfill(&transaction, None).await.is_err()); transaction.rollback().await.unwrap(); assert_eq!(Entity::find().count(&db).await.unwrap(), 0); } @@ -321,4 +328,160 @@ mod tests { backfill(&db, None).await.unwrap(); assert_eq!(Entity::find().count(&db).await.unwrap(), 129); } + + #[tokio::test] + async fn postgres_concurrent_historical_backfills_produce_ready_databases() { + use std::time::Duration; + + use crate::{ + db::{DelegationStatus, SpaceDatabase}, + keys::StaticSecret, + models::{abilities, revocation, space}, + relationships::parent_delegations, + storage::memory::MemoryStore, + test_support::PostgresTestSchema, + }; + use sea_orm::{IntoActiveModel, Statement}; + use tinycloud_auth::authorization::Cid; + + let Some(fixture) = PostgresTestSchema::new( + "postgres_concurrent_historical_backfills_produce_ready_databases", + ) + .await + else { + return; + }; + let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/tc1071/generated"); + let manifest: serde_json::Value = + serde_json::from_slice(&std::fs::read(source.join("manifest.json")).unwrap()).unwrap(); + let historical = manifest + .as_array() + .unwrap() + .iter() + .find(|row| row["database"] == "revoked-v0.sqlite") + .unwrap(); + let old = Database::connect(format!( + "sqlite:{}?mode=ro", + source.join("revoked-v0.sqlite").display() + )) + .await + .unwrap(); + macro_rules! copy_records { + ($entity:path) => { + let records = <$entity>::find().all(&old).await.unwrap(); + if !records.is_empty() { + <$entity>::insert_many(records.into_iter().map(|row| row.into_active_model())) + .exec(&fixture.conn) + .await + .unwrap(); + } + }; + } + copy_records!(actor::Entity); + copy_records!(space::Entity); + copy_records!(delegation::Entity); + copy_records!(abilities::Entity); + copy_records!(parent_delegations::Entity); + copy_records!(revocation::Entity); + old.close().await.unwrap(); + let original = delegation::Entity::find() + .order_by_asc(delegation::Column::Id) + .all(&fixture.conn) + .await + .unwrap(); + + // Force both constructors into overlapping database work. Following + // the blocker graph permits either a serialized backfill or a + // conflict-safe insert, without pinning the implementation's lock. + let blocker = fixture.conn.begin().await.unwrap(); + let blocker_pid: i32 = blocker + .query_one(Statement::from_string( + sea_orm::DbBackend::Postgres, + "SELECT pg_backend_pid() AS pid", + )) + .await + .unwrap() + .unwrap() + .try_get("", "pid") + .unwrap(); + blocker + .execute_unprepared("LOCK TABLE delegation_cid_alias IN EXCLUSIVE MODE") + .await + .unwrap(); + let workers = std::array::from_fn::<_, 2, _>(|_| { + let connection = fixture.conn.clone(); + tokio::spawn(async move { + SpaceDatabase::new( + connection, + MemoryStore::default(), + StaticSecret::new(vec![31; 32]).unwrap(), + ) + .await + }) + }); + let waiting = tokio::time::timeout(Duration::from_secs(30), async { + loop { + let blocked: i64 = fixture + .conn + .query_one(Statement::from_sql_and_values( + sea_orm::DbBackend::Postgres, + "WITH RECURSIVE blocked(pid) AS (SELECT $1::int UNION SELECT activity.pid FROM pg_stat_activity AS activity INNER JOIN blocked ON blocked.pid = ANY(pg_blocking_pids(activity.pid))) SELECT COUNT(*) AS blocked FROM blocked WHERE pid <> $1", + [blocker_pid.into()], + )) + .await? + .ok_or_else(|| DbErr::Custom("missing PostgreSQL wait observation".into()))? + .try_get("", "blocked")?; + if blocked == 2 { + return Ok::<(), DbErr>(()); + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await; + blocker.commit().await.unwrap(); + let [first, second] = workers; + let results = [first.await.unwrap(), second.await.unwrap()]; + let roots = historical["rows"].as_array().unwrap(); + let mut statuses = Vec::with_capacity(roots.len() * results.len()); + let mut errors = Vec::with_capacity(results.len()); + for result in results { + match result { + Ok(db) => { + for row in roots { + let cid: Cid = row["raw_cid"].as_str().unwrap().parse().unwrap(); + statuses.push( + db.delegation_status( + Hash::from(cid), + historical["issuer"].as_str().unwrap(), + &[], + ) + .await, + ); + } + } + Err(error) => errors.push(error), + } + } + let current = delegation::Entity::find() + .order_by_asc(delegation::Column::Id) + .all(&fixture.conn) + .await; + fixture.conn.clone().close().await.unwrap(); + waiting + .expect("both constructors must overlap under the database blocker") + .unwrap(); + assert!(errors.is_empty(), "concurrent startup failed: {errors:?}"); + for status in statuses { + assert!( + matches!(status, Ok(Some(DelegationStatus::Revoked))), + "each historical equivalent must remain revoked after concurrent startup", + ); + } + assert_eq!( + current.unwrap(), + original, + "concurrent startup must preserve every historical signed row", + ); + } } From 34caaaaa408c25a0af81496f9b3185a14d9500a5 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 22:40:53 +0000 Subject: [PATCH 08/12] fix: enforce canonical revocations in policy admission --- CHANGELOG.md | 1 + docs/delegation-flow.md | 10 + tinycloud-core/src/db.rs | 19 + tinycloud-core/src/models/revocation.rs | 9 +- tinycloud-node-server/src/policy_v3.rs | 27 +- .../src/policy_v3/account_alias_tests.rs | 542 ++++++++++++++++++ tinycloud-node-server/src/share_v2.rs | 18 +- .../share_v2/tests/alias_revocation_tests.rs | 196 +++++++ 8 files changed, 793 insertions(+), 29 deletions(-) create mode 100644 tinycloud-node-server/src/policy_v3/account_alias_tests.rs create mode 100644 tinycloud-node-server/src/share_v2/tests/alias_revocation_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d27d60a8..97f93f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Historical CACAO revocations now apply to every stored encoding of the same delegation, in both directions (TC-1071). A persisted CID-alias index derives identities from current ingestion's canonical bytes, including recovery-ID, issuer-address/chain, and accepted CBOR representation normalization. Startup backfills aliases transactionally after loading the column encryption key and fails closed on undecodable or mismatched history or missing/wrong keys. Historical IDs, signed bytes, revocation targets, and signed child proof references remain unchanged; legacy list/chain responses retain their exact raw-header/CID relationship. This closes reuse of an already owner-signed grant under another accepted encoding; it does not involve forging new rights. - CACAO issuer address casing and numeric chain spelling now serialize to the verifier's same EIP-55/u64 identity without changing the signed SIWE message. SDK session setup checks and normalizes wallet signatures before deriving its proof CID, including direct Rust sessions; low-v wallet signatures no longer produce `MissingParents` (TC-1071). - Serialize concurrent PostgreSQL/MySQL historical backfills before their first history read; failed startup still rolls back aliases and preserves signed records. MySQL alias columns retain full variable-length multihashes, and encrypted-key validation avoids its unsupported limited `IN` subquery. Native alias checks do not waive the separately reproduced older MySQL 8.0.46 full-schema error 1074 (TC-1071). +- Policy v3 account-owner admission, its transactional recheck, and legacy Share v2 revocation consumers now use the same effective historical-CID revocation lookup. Account-owner proofs retain shared canonical chain guards through commit or rollback, so a same-registry sibling revocation cannot pass an in-flight admission; a separately committed sibling revocation fails the transaction recheck (TC-1071). - Rust API cutover: pass encryption through `SpaceDatabase::new_with_encryption`; list/chain outcomes carry serialized delegation wrappers. SDK exhaustive error matches must handle the new `session::Error::InvalidSIWESignature` variant (TC-1071). ## [1.20.6] - 2026-10-14 diff --git a/docs/delegation-flow.md b/docs/delegation-flow.md index ddc62f98..c7c6288b 100644 --- a/docs/delegation-flow.md +++ b/docs/delegation-flow.md @@ -434,6 +434,16 @@ authorization without an extra request round trip. New delegations and their alias commit together; PostgreSQL uses a dependent insert CTE to preserve the existing registration round-trip budget. +Policy v3 account-owner credential admission, its locked transactional recheck, +and legacy Share v2 ancestry/root checks use the same effective revocation +predicate, not an exact stored-ID comparison. Account-owner authentication takes +shared canonical ancestor guards with `TinyCloud::guard_delegation_chains` +before per-space guards or the SQL transaction and retains them in the proof +through commit or rollback. These guards coordinate the database instance and +its clones through their shared graph-lock registry; they are not distributed +locks. The transaction still rechecks effective revocation, including a sibling +revocation committed by a separately constructed database instance. + Legacy capability list/chain responses advertise the stored CID and return the exact stored raw header, not a re-encoding of the decoded CACAO. Their parent CID fields retain the signed child references. Existing legacy list diff --git a/tinycloud-core/src/db.rs b/tinycloud-core/src/db.rs index 72565071..0aa7f314 100644 --- a/tinycloud-core/src/db.rs +++ b/tinycloud-core/src/db.rs @@ -1316,6 +1316,25 @@ where Ok(self.acquire_exclusive_chain_guards_for_keys(keys).await) } + /// Hold shared guards for the canonical ancestor identities of `roots`. + /// + /// These coordinate admission with delegation/revocation writers using + /// this database and its clones. Acquire before space-sequence guards or + /// SQL transactions, and retain through commit or rollback. Callers must + /// still recheck effective revocation inside their admission transaction. + pub async fn guard_delegation_chains( + &self, + roots: &[Hash], + ) -> Result, DbErr> { + let keys = crate::auth_graph::load_chain_lock_ids(&self.conn, roots) + .await + .map_err(|error| match error { + revocation::ChainTraversalError::Db(error) => error, + error => DbErr::Custom(error.to_string()), + })?; + Ok(self.acquire_shared_chain_guards_for_keys(keys).await) + } + /// Resolve `keys` to their per-delegation locks in a stable global order. /// /// Sorting and deduplicating here is the deadlock discipline: every diff --git a/tinycloud-core/src/models/revocation.rs b/tinycloud-core/src/models/revocation.rs index 02137040..a37d5034 100644 --- a/tinycloud-core/src/models/revocation.rs +++ b/tinycloud-core/src/models/revocation.rs @@ -164,10 +164,11 @@ pub(crate) async fn effective_rows( .collect() } -pub(crate) async fn is_revoked( - db: &C, - delegation_id: &Hash, -) -> Result { +/// Whether this delegation or any persisted equivalent CID has been revoked. +/// +/// Use the caller's transaction for an admission-time recheck; signed targets +/// and stored delegation IDs are never rewritten. +pub async fn is_revoked(db: &C, delegation_id: &Hash) -> Result { Ok(Entity::find() .filter(target_condition(Expr::value(*delegation_id))) .one(db) diff --git a/tinycloud-node-server/src/policy_v3.rs b/tinycloud-node-server/src/policy_v3.rs index d307a625..24313081 100644 --- a/tinycloud-node-server/src/policy_v3.rs +++ b/tinycloud-node-server/src/policy_v3.rs @@ -2912,11 +2912,8 @@ async fn is_root_generically_revoked( ) -> Result { let cid = tinycloud_auth::ipld_core::cid::Cid::try_from(root_cid) .map_err(|_| "policy-root-invalid")?; - revocation::Entity::find() - .filter(revocation::Column::Revoked.eq(tinycloud_core::hash::Hash::from(cid))) - .one(db) + revocation::is_revoked(db, &tinycloud_core::hash::Hash::from(cid)) .await - .map(|record| record.is_some()) .map_err(|_| "root-revocation-unavailable") } @@ -5345,6 +5342,9 @@ struct AccountOwnerProof { owner_did: String, space: SpaceId, expires_at: Option, + // Canonical identities, not the chosen legacy row, remain guarded until + // admission commits or rolls back. Revoke uses these same guards exclusive. + _chain_guards: Vec>, } /// Authenticate the policy/v2 credential owner from an already-admitted @@ -5457,12 +5457,13 @@ async fn authenticate_account_owner( "account-authorization-capability-mismatch".into(), )); } - if revocation::Entity::find() - .filter(revocation::Column::Revoked.eq(row.id)) - .one(&runtime.conn) + let chain_guards = tinycloud + .guard_delegation_chains(&[row.id]) + .await + .map_err(db_error)?; + if revocation::is_revoked(&runtime.conn, &row.id) .await .map_err(db_error)? - .is_some() || space::Entity::find_by_id(SpaceIdWrap(credentials_space.clone())) .one(&runtime.conn) .await @@ -5477,6 +5478,7 @@ async fn authenticate_account_owner( owner_did, space: credentials_space, expires_at: event.0.expiry, + _chain_guards: chain_guards, }) } @@ -5495,12 +5497,9 @@ async fn validate_locked_account_owner( || proof .expires_at .is_some_and(|expires_at| expires_at <= OffsetDateTime::now_utc()) - || revocation::Entity::find() - .filter(revocation::Column::Revoked.eq(locked.id)) - .one(transaction) + || revocation::is_revoked(transaction, &locked.id) .await .map_err(db_error)? - .is_some() || space::Entity::find_by_id(SpaceIdWrap(proof.space.clone())) .one(transaction) .await @@ -10181,3 +10180,7 @@ mod tests { .is_err()); } } + +#[cfg(test)] +#[path = "policy_v3/account_alias_tests.rs"] +mod account_alias_tests; diff --git a/tinycloud-node-server/src/policy_v3/account_alias_tests.rs b/tinycloud-node-server/src/policy_v3/account_alias_tests.rs new file mode 100644 index 00000000..6a26666a --- /dev/null +++ b/tinycloud-node-server/src/policy_v3/account_alias_tests.rs @@ -0,0 +1,542 @@ +//! Actual account-owner admission boundaries over signed historical CACAO siblings. +//! +//! The generic historical-wire decoder preserves valid pre-normalization bytes. +//! This constructs disposable fixtures; it is not an f9-generated credentials +//! database, the current HTTP ingestion path, or an end-to-end mint HTTP test. + +use super::*; +use k256::ecdsa::SigningKey; +use sha3::Keccak256; +use std::time::Duration as WaitDuration; +use tinycloud_auth::{ + authorization::{EncodingError, HeaderEncode, TinyCloudRevocation}, + cacaos::{ + siwe::{encode_eip55, Message, Version}, + siwe_cacao::{Eip191, Payload, Signature}, + }, + resolver::DID_METHODS, + siwe_recap::Capability, +}; +use tinycloud_core::{ + db::{begin_write_transaction, DelegationStatus}, + events::Revocation, + hash::Hash, + sea_orm::Database, + storage::StorageConfig as _, +}; +use tokio::{sync::OwnedRwLockReadGuard, time::timeout}; + +const WAIT: WaitDuration = WaitDuration::from_secs(10); + +/// Preserve the signed wire identity using the public generic event decoder, +/// instead of inserting inconsistent hashes or changing persisted graph rows. +struct HistoricalWire(TinyCloudDelegation); + +impl HeaderEncode for HistoricalWire { + fn encode(&self) -> Result { + self.0.encode() + } + + fn decode(header: &str) -> Result<(Self, Vec), EncodingError> { + let bytes = + decode_config(header, base64::URL_SAFE).expect("valid historical fixture base64"); + Ok((Self(TinyCloudDelegation::from_bytes(&bytes)?), bytes)) + } +} + +impl TryFrom for DelegationInfo { + type Error = >::Error; + + fn try_from(value: HistoricalWire) -> Result { + Self::try_from(value.0) + } +} + +struct SignedGrant { + id: Hash, + bytes: Vec, +} + +impl SignedGrant { + fn from_signature(message: Message, signature: [u8; 65]) -> anyhow::Result { + let cacao = Payload::from(message).sign::(Signature::from(signature)); + let header = TinyCloudDelegation::Cacao(Box::new(cacao)).encode()?; + let bytes = decode_config(header, base64::URL_SAFE)?; + Ok(Self { + id: hash(&bytes), + bytes, + }) + } + + fn event(&self) -> anyhow::Result { + Ok(Delegation::from_header_ser::( + &encode_config(&self.bytes, base64::URL_SAFE), + )?) + } +} + +struct AccountAliasFixture { + node: crate::TinyCloud, + runtime: PolicyV3Runtime, + key: SigningKey, + address: [u8; 20], + holder: String, + owner: String, + credentials_space: SpaceId, + // Exactly the same payload/r/s, with raw v=0 and conventional v=27. + aliases: [SignedGrant; 2], + unrelated: SignedGrant, + database_url: String, + directory: tempfile::TempDir, +} + +fn wallet_message(address: [u8; 20], audience: &str, nonce: &str) -> anyhow::Result { + let now = OffsetDateTime::now_utc(); + Ok(Message { + scheme: Some("https".parse()?), + domain: "account-alias.tc1071.test".parse()?, + address, + statement: None, + uri: audience.parse()?, + version: Version::V1, + chain_id: 1, + nonce: nonce.into(), + issued_at: (now - Duration::minutes(1)).into(), + expiration_time: Some((now + Duration::hours(1)).into()), + not_before: None, + request_id: None, + resources: vec![], + }) +} + +fn account_message( + address: [u8; 20], + holder: &str, + space: &SpaceId, + nonce: &str, +) -> anyhow::Result { + let mut capability = Capability::<()>::default(); + capability.with_actions_convert(format!("{space}/space"), [("tinycloud.space/host", [])])?; + Ok(capability.build_message(wallet_message(address, holder, nonce)?)?) +} + +fn sign_conventional(key: &SigningKey, message: Message) -> anyhow::Result { + let (signature, recovery) = key.sign_prehash_recoverable(&message.eip191_hash()?)?; + let mut bytes = [0_u8; 65]; + bytes[..64].copy_from_slice(signature.to_bytes().as_ref()); + bytes[64] = recovery.to_byte() + 27; + SignedGrant::from_signature(message, bytes) +} + +fn authentication_error(error: (Status, String)) -> anyhow::Error { + anyhow::anyhow!("{}: {}", error.0, error.1) +} + +impl AccountAliasFixture { + async fn new() -> anyhow::Result { + let directory = tempfile::tempdir()?; + let database_url = format!( + "sqlite:{}?mode=rwc", + directory.path().join("caps.db").display() + ); + let conn = Database::connect(&database_url).await?; + let storage = crate::BlockConfig::B(crate::storage::file_system::FileSystemConfig::new( + directory.path(), + )) + .open() + .await?; + let secret = StaticSecret::new(vec![31; 32]) + .map_err(|_| anyhow::anyhow!("invalid disposable node secret"))?; + let node = crate::TinyCloud::new(conn.clone().into(), storage, secret.clone()).await?; + let runtime = PolicyV3Runtime::new(conn, secret.node_did(), secret) + .with_sqlite_writer_lock(node.sqlite_writer_lock()); + let key = SigningKey::from_bytes(&[7_u8; 32].into())?; + let point = key.verifying_key().to_encoded_point(false); + let address: [u8; 20] = Keccak256::digest(&point.as_bytes()[1..])[12..].try_into()?; + let holder_key = JWK::generate_ed25519()?; + let holder = DID_METHODS.generate(&holder_key, "key")?.to_string(); + let owner = format!("did:pkh:eip155:1:0x{}", encode_eip55(&address)); + let credentials_space: SpaceId = format!( + "tinycloud:pkh:eip155:1:0x{}:credentials", + encode_eip55(&address) + ) + .parse()?; + + // Select a genuine recoverable signature whose raw recovery byte is + // exactly zero, rather than forcing a recovery byte onto another key. + let mut nonce = 0_u64; + let aliases = loop { + let message = account_message( + address, + &holder, + &credentials_space, + &format!("tc1071account{nonce}"), + )?; + let (signature, recovery) = key.sign_prehash_recoverable(&message.eip191_hash()?)?; + if recovery.to_byte() == 0 { + let mut bytes = [0_u8; 65]; + bytes[..64].copy_from_slice(signature.to_bytes().as_ref()); + let historical = SignedGrant::from_signature(message.clone(), bytes)?; + bytes[64] = 27; + break [historical, SignedGrant::from_signature(message, bytes)?]; + } + nonce = nonce + .checked_add(1) + .ok_or_else(|| anyhow::anyhow!("nonce overflow"))?; + }; + assert_ne!(aliases[0].id, aliases[1].id); + let unrelated = sign_conventional( + &key, + account_message(address, &holder, &credentials_space, "tc1071unrelated")?, + )?; + let fixture = Self { + node, + runtime, + key, + address, + holder, + owner, + credentials_space, + aliases, + unrelated, + database_url, + directory, + }; + for grant in fixture.aliases.iter().chain([&fixture.unrelated]) { + fixture + .node + .delegate(grant.event()?) + .await + .map_err(|error| anyhow::anyhow!("{error}"))?; + fixture.assert_signed_row(grant).await?; + fixture + .assert_status(grant.id, DelegationStatus::Active) + .await?; + } + // The real signed space/host grant, not a manually inserted space row, + // is what establishes the hosted credentials-space authority. + assert!( + space::Entity::find_by_id(SpaceIdWrap(fixture.credentials_space.clone())) + .one(&fixture.runtime.conn) + .await? + .is_some() + ); + Ok(fixture) + } + + fn grant(&self, nonce: &str) -> anyhow::Result { + sign_conventional( + &self.key, + account_message(self.address, &self.holder, &self.credentials_space, nonce)?, + ) + } + + fn revocation(&self, target: Hash) -> anyhow::Result { + let signed = sign_conventional( + &self.key, + wallet_message( + self.address, + &format!("ucan:{}", target.to_cid(0x55)), + &format!("tc1071revoke{}", target.to_cid(0x55)), + )?, + )?; + Ok(Revocation::from_header_ser::( + &encode_config(signed.bytes, base64::URL_SAFE), + )?) + } + + async fn authenticate(&self, id: Hash) -> Result { + authenticate_account_owner( + Some(&id.to_cid(0x55).to_string()), + Some(&self.credentials_space.to_string()), + &self.holder, + &self.runtime, + &self.node, + ) + .await + } + + async fn assert_signed_row(&self, grant: &SignedGrant) -> anyhow::Result<()> { + let (row, event) = self + .node + .load_signed_delegation(grant.id.to_cid(0x55)) + .await + .map_err(anyhow::Error::msg)? + .ok_or_else(|| anyhow::anyhow!("missing signed account row"))?; + assert_eq!(row.id, hash(&grant.bytes)); + assert_eq!(row.serialization, grant.bytes); + assert_eq!(event.content_hash(), grant.id); + let TinyCloudDelegation::Cacao(cacao) = event.0.delegation else { + anyhow::bail!("account fixture is not CACAO"); + }; + cacao.verify().await?; + Ok(()) + } + + async fn assert_status(&self, id: Hash, expected: DelegationStatus) -> anyhow::Result<()> { + assert_eq!( + self.node + .delegation_status(id, &self.owner, &[]) + .await + .map_err(|error| anyhow::anyhow!("{error}"))?, + Some(expected), + "core status for {}", + id.to_cid(0x55) + ); + Ok(()) + } + + async fn assert_active_admission(&self, id: Hash) -> anyhow::Result<()> { + self.assert_status(id, DelegationStatus::Active).await?; + let proof = self.authenticate(id).await.map_err(authentication_error)?; + assert_eq!(proof.row.id, id); + assert_eq!(proof.cid, id.to_cid(0x55)); + assert_eq!(proof.owner_did, self.owner); + assert_eq!(proof.space, self.credentials_space); + let transaction = begin_write_transaction(&self.runtime.conn, None).await?; + validate_locked_account_owner(&transaction, &proof) + .await + .map_err(authentication_error)?; + transaction.commit().await?; + drop(proof); + Ok(()) + } + + async fn assert_aliases_denied(&self) -> anyhow::Result<()> { + for grant in &self.aliases { + self.assert_status(grant.id, DelegationStatus::Revoked) + .await?; + let error = self + .authenticate(grant.id) + .await + .err() + .expect("effectively revoked account owner must not authenticate"); + assert_eq!(error.0, Status::Forbidden); + self.assert_signed_row(grant).await?; + } + Ok(()) + } + + async fn independent_node(&self) -> anyhow::Result { + let conn = Database::connect(&self.database_url).await?; + let storage = crate::BlockConfig::B(crate::storage::file_system::FileSystemConfig::new( + self.directory.path(), + )) + .open() + .await?; + // Same disposable DB, but a new SpaceDatabase and lock registry. This + // models a writer outside this node's in-process admission guard. + Ok(crate::TinyCloud::new(conn.into(), storage, self.runtime.signer.clone()).await?) + } +} + +#[tokio::test] +async fn account_owner_rejects_revoked_cacao_aliases_in_both_directions() -> anyhow::Result<()> { + for target_index in 0..2 { + let fixture = timeout(WAIT, AccountAliasFixture::new()).await??; + for grant in fixture.aliases.iter().chain([&fixture.unrelated]) { + fixture.assert_active_admission(grant.id).await?; + } + let target = fixture.aliases[target_index].id; + timeout(WAIT, fixture.node.revoke(fixture.revocation(target)?)) + .await? + .map_err(|error| anyhow::anyhow!("{error}"))?; + fixture.assert_aliases_denied().await?; + // Equivalent grants are revoked, not every valid grant by this wallet. + fixture + .assert_active_admission(fixture.unrelated.id) + .await?; + let rows = revocation::Entity::find() + .all(&fixture.runtime.conn) + .await?; + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].revoked, target); + + // Positive revoke control, after the unrelated admission proof drops. + timeout( + WAIT, + fixture + .node + .revoke(fixture.revocation(fixture.unrelated.id)?), + ) + .await? + .map_err(|error| anyhow::anyhow!("{error}"))?; + fixture + .assert_status(fixture.unrelated.id, DelegationStatus::Revoked) + .await?; + assert_eq!( + fixture + .authenticate(fixture.unrelated.id) + .await + .err() + .expect("revoked control") + .0, + Status::Forbidden + ); + } + Ok(()) +} + +#[tokio::test] +async fn locked_account_owner_recheck_rejects_committed_sibling_revocations_in_both_directions( +) -> anyhow::Result<()> { + for target_index in 0..2 { + let fixture = timeout(WAIT, AccountAliasFixture::new()).await??; + let target = fixture.aliases[target_index].id; + let proof = fixture + .authenticate(fixture.aliases[1 - target_index].id) + .await + .map_err(authentication_error)?; + let active_transaction = begin_write_transaction(&fixture.runtime.conn, None).await?; + validate_locked_account_owner(&active_transaction, &proof) + .await + .map_err(authentication_error)?; + active_transaction.commit().await?; + + let other_node = timeout(WAIT, fixture.independent_node()).await??; + timeout(WAIT, other_node.revoke(fixture.revocation(target)?)) + .await? + .map_err(|error| anyhow::anyhow!("{error}"))?; + // Confirm a durable core transition while the original proof is still + // alive; a same-registry writer would intentionally wait for it. + for grant in &fixture.aliases { + assert_eq!( + other_node + .delegation_status(grant.id, &fixture.owner, &[]) + .await + .map_err(|error| anyhow::anyhow!("{error}"))?, + Some(DelegationStatus::Revoked) + ); + } + // No changed projection, expired proof, or removed hosted space can + // account for the Conflict: only effective sibling revocation changed. + assert_eq!( + delegation_model::Entity::find_by_id(proof.row.id) + .one(&fixture.runtime.conn) + .await?, + Some(proof.row.clone()) + ); + assert!(proof + .expires_at + .is_some_and(|expiry| expiry > OffsetDateTime::now_utc())); + assert!(space::Entity::find_by_id(SpaceIdWrap(proof.space.clone())) + .one(&fixture.runtime.conn) + .await? + .is_some()); + let transaction = begin_write_transaction(&fixture.runtime.conn, None).await?; + let error = validate_locked_account_owner(&transaction, &proof) + .await + .expect_err("transactional recheck must reject a committed sibling revocation"); + assert_eq!(error.0, Status::Conflict); + transaction.rollback().await?; + drop(proof); + fixture.assert_aliases_denied().await?; + fixture + .assert_active_admission(fixture.unrelated.id) + .await?; + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn account_owner_proofs_hold_canonical_guards_through_commit_in_both_directions( +) -> anyhow::Result<()> { + for target_index in 0..2 { + let fixture = timeout(WAIT, AccountAliasFixture::new()).await??; + let target = fixture.aliases[target_index].id; + let proof = fixture + .authenticate(fixture.aliases[1 - target_index].id) + .await + .map_err(authentication_error)?; + // Both actual return values coexist: equivalent account proofs take + // shared, not exclusive, canonical guards. + let second_proof = timeout(WAIT, fixture.authenticate(target)) + .await? + .map_err(authentication_error)?; + let locks: Vec<_> = proof + ._chain_guards + .iter() + .map(|guard| OwnedRwLockReadGuard::rwlock(guard).clone()) + .collect(); + let revoke_node = fixture.node.clone(); + let revocation = fixture.revocation(target)?; + let mut revoke = tokio::spawn(async move { + revoke_node + .revoke(revocation) + .await + .map_err(|error| anyhow::anyhow!("{error}")) + }); + + // Tokio's write-preferring RwLock refuses a new reader once the real + // revoke writer queues. Observe that state on the authentic proof's + // lock: unlike a started-task signal or sleep, this proves contention. + // No admission SQL transaction or writer/sequence guard exists yet. + timeout(WAIT, async { + loop { + if locks.iter().any(|lock| lock.try_read().is_err()) { + break; + } + tokio::select! { + result = &mut revoke => panic!("revocation escaped a live account proof: {result:?}"), + _ = tokio::task::yield_now() => {} + } + } + }) + .await?; + for grant in &fixture.aliases { + assert!(!revocation::is_revoked(&fixture.runtime.conn, &grant.id).await?); + } + + // A real unrelated signed graph write commits while that writer is + // queued. SQLite or a global writer gate cannot explain its wait. + let write_control = fixture.grant("tc1071sqlwritecontrol")?; + timeout(WAIT, fixture.node.delegate(write_control.event()?)) + .await? + .map_err(|error| anyhow::anyhow!("{error}"))?; + fixture + .assert_status(write_control.id, DelegationStatus::Active) + .await?; + + let admitted = fixture.grant("tc1071guardedadmission")?; + let sequence_guards = fixture + .node + .acquire_space_sequence_guards(std::slice::from_ref(&fixture.credentials_space)) + .await; + let writer = match &fixture.runtime.sqlite_writer_lock { + Some(lock) => Some(lock.lock().await), + None => None, + }; + let transaction = begin_write_transaction(&fixture.runtime.conn, None).await?; + validate_locked_account_owner(&transaction, &proof) + .await + .map_err(authentication_error)?; + validate_locked_account_owner(&transaction, &second_proof) + .await + .map_err(authentication_error)?; + fixture + .node + .delegate_batch_in_transaction(&transaction, vec![admitted.event()?]) + .await + .map_err(|error| anyhow::anyhow!("{error}"))?; + transaction.commit().await?; + drop(writer); + drop(sequence_guards); + fixture.assert_signed_row(&admitted).await?; + assert!(!revoke.is_finished()); + assert!(!revocation::is_revoked(&fixture.runtime.conn, &target).await?); + + // Commit alone cannot release the proof's lifetime protection, nor + // can dropping just one of the two concurrently held shared proofs. + drop(proof); + assert!(locks.iter().any(|lock| lock.try_read().is_err())); + assert!(!revocation::is_revoked(&fixture.runtime.conn, &target).await?); + drop(second_proof); + timeout(WAIT, revoke).await???; + fixture.assert_aliases_denied().await?; + fixture + .assert_active_admission(fixture.unrelated.id) + .await?; + fixture.assert_active_admission(admitted.id).await?; + } + Ok(()) +} diff --git a/tinycloud-node-server/src/share_v2.rs b/tinycloud-node-server/src/share_v2.rs index c301958c..710e2fd3 100644 --- a/tinycloud-node-server/src/share_v2.rs +++ b/tinycloud-node-server/src/share_v2.rs @@ -1402,13 +1402,7 @@ async fn revocation_in_ancestry( if visited.len() > MAX_GRAPH_NODES { return Err(()); } - if revocation::Entity::find() - .filter(revocation::Column::Revoked.eq(current)) - .count(db) - .await - .map_err(|_| ())? - > 0 - { + if revocation::is_revoked(db, ¤t).await.map_err(|_| ())? { return Ok(true); } let parents = parent_delegations::Entity::find() @@ -2023,12 +2017,7 @@ async fn cid_revoked(db: &C, cid: &str) -> Result .parse::() .map_err(|_| ())? .into(); - Ok(revocation::Entity::find() - .filter(revocation::Column::Revoked.eq(id)) - .count(db) - .await - .map_err(|_| ())? - > 0) + revocation::is_revoked(db, &id).await.map_err(|_| ()) } fn typed_scope( @@ -3420,6 +3409,9 @@ mod tests { use rocket::local::asynchronous::Client; use time::Duration; + #[path = "alias_revocation_tests.rs"] + mod alias_revocation_tests; + #[test] fn policy_domain_matches_the_sdk_wire_contract() { assert_eq!(POLICY_DOMAIN, "xyz.tinycloud.share/policy/v2\0"); diff --git a/tinycloud-node-server/src/share_v2/tests/alias_revocation_tests.rs b/tinycloud-node-server/src/share_v2/tests/alias_revocation_tests.rs new file mode 100644 index 00000000..66703c46 --- /dev/null +++ b/tinycloud-node-server/src/share_v2/tests/alias_revocation_tests.rs @@ -0,0 +1,196 @@ +use super::*; +use k256::ecdsa::SigningKey; +use sha3::Keccak256; +use std::str::FromStr; +use tinycloud_auth::{ + authorization::{EncodingError, HeaderEncode, TinyCloudRevocation}, + cacaos::{ + siwe::{encode_eip55, Message}, + siwe_cacao::{Eip191, Payload, Signature}, + }, + siwe_recap::Capability, +}; +use tinycloud_core::{ + events::{Delegation, Revocation}, + util::DelegationInfo, +}; + +// Public historical-wire ingestion preserves the signed 0/1 recovery byte. +// This is not evidence of a retained credentials database or a full HTTP flow. +struct HistoricalWire(TinyCloudDelegation); + +impl HeaderEncode for HistoricalWire { + fn encode(&self) -> Result { + self.0.encode() + } + + fn decode(value: &str) -> Result<(Self, Vec), EncodingError> { + let bytes = decode_config(value, base64::URL_SAFE).expect("fixture header encoding"); + Ok((Self(TinyCloudDelegation::from_bytes(&bytes)?), bytes)) + } +} + +impl TryFrom for DelegationInfo { + type Error = >::Error; + + fn try_from(value: HistoricalWire) -> Result { + Self::try_from(value.0) + } +} + +fn message(address: &[u8; 20], audience: &str, nonce: &str, expiry: OffsetDateTime) -> Message { + Message::from_str(&format!( + "localhost wants you to sign in with your Ethereum account:\n0x{}\n\n\nURI: {}\nVersion: 1\nChain ID: 1\nNonce: {}\nIssued At: {}\nExpiration Time: {}", + encode_eip55(address), audience, nonce, + timestamp(OffsetDateTime::now_utc() - Duration::minutes(1)), timestamp(expiry) + )).expect("valid SIWE fixture") +} + +fn signed(key: &SigningKey, message: Message, historical: bool) -> Vec { + let (signature, recovery) = key + .sign_prehash_recoverable(&message.eip191_hash().unwrap()) + .unwrap(); + let mut bytes = [0u8; 65]; + bytes[..64].copy_from_slice(&signature.to_bytes()); + bytes[64] = recovery.to_byte() + if historical { 0 } else { 27 }; + let cacao = Payload::from(message).sign::(Signature::from(bytes)); + decode_config( + TinyCloudDelegation::Cacao(Box::new(cacao)) + .encode() + .unwrap(), + base64::URL_SAFE, + ) + .unwrap() +} + +async fn assert_consumers( + runtime: &ShareV2Runtime, + policy: &PolicyDocument, + bytes: &[u8], + revoked: bool, +) { + let cid = raw_blake3_cid(bytes); + let input = OwnerDelegationInput { + cid: cid.clone(), + dag_cbor: encode_config(bytes, URL_SAFE_NO_PAD), + }; + assert_eq!( + verify_owner_delegation(runtime, policy, &input) + .await + .is_err(), + revoked, + "actual owner verifier must agree with effective alias revocation" + ); + assert_eq!(cid_revoked(&runtime.conn, &cid).await.unwrap(), revoked); + assert_eq!( + revocation_in_ancestry(&runtime.conn, hash(bytes)) + .await + .unwrap(), + revoked + ); +} + +async fn signed_alias_consumers(reverse: bool) { + let secret = StaticSecret::new(vec![0x42; 32]).unwrap(); + let runtime = + compose_test_runtime(&secret, Some(TeeContext::derive_local(&secret)), true).await; + let key = SigningKey::from_bytes(&[7u8; 32].into()).unwrap(); + let point = key.verifying_key().to_encoded_point(false); + let address: [u8; 20] = Keccak256::digest(&point.as_bytes()[1..])[12..] + .try_into() + .unwrap(); + let (_, share_did) = tamper_keypair_and_did(); + let owner = format!("did:pkh:eip155:1:0x{}", encode_eip55(&address)); + let space = format!( + "tinycloud:pkh:eip155:1:0x{}:default", + encode_eip55(&address) + ); + let expiry = OffsetDateTime::now_utc() + Duration::hours(1); + let mut caps = Capability::<()>::default(); + caps.with_actions_convert( + format!("{space}/kv/shares/alias/document.md"), + [("tinycloud.kv/get", [])], + ) + .unwrap(); + let grant = caps + .build_message(message(&address, &share_did, "sharealias1071", expiry)) + .unwrap(); + let historical = signed(&key, grant.clone(), true); + let canonical = signed(&key, grant, false); + let unrelated = signed( + &key, + caps.build_message(message(&address, &share_did, "unrelated1071", expiry)) + .unwrap(), + false, + ); + assert_ne!(hash(&historical), hash(&canonical)); + for bytes in [&historical, &canonical, &unrelated] { + let parsed = TinyCloudDelegation::from_bytes(bytes).unwrap(); + verify_delegation_signature(&parsed).await.unwrap(); + let event = + Delegation::from_header_ser::(&encode_config(bytes, base64::URL_SAFE)) + .unwrap(); + runtime + .tinycloud + .delegate(event) + .await + .map_err(|error| error.to_string()) + .unwrap(); + let row = delegation::Entity::find_by_id(hash(bytes)) + .one(&runtime.conn) + .await + .unwrap() + .unwrap(); + assert_eq!( + maybe_decrypt(Some(&runtime.policy_encryption), &row.serialization).unwrap(), + *bytes, + "stored content and its hash must retain the actual signed wire bytes" + ); + } + let (mut envelope, _, _) = sample_policy(); + envelope.policy.owner_did = owner; + envelope.policy.share_key_did = share_did; + envelope.policy.target.space_id = space; + envelope.policy.resource.path = "shares/alias/document.md".into(); + envelope.policy.actions = vec!["tinycloud.kv/get".into()]; + envelope.policy.expires_at = timestamp(expiry - Duration::minutes(1)); + for bytes in [&historical, &canonical, &unrelated] { + assert_consumers(&runtime, &envelope.policy, bytes, false).await; + } + let target = if reverse { &canonical } else { &historical }; + let revocation_bytes = signed( + &key, + message( + &address, + &format!("ucan:{}", raw_blake3_cid(target)), + "revokeshare1071", + expiry, + ), + false, + ); + let event = Revocation::from_header_ser::(&encode_config( + revocation_bytes, + base64::URL_SAFE, + )) + .unwrap(); + runtime + .tinycloud + .revoke(event) + .await + .map_err(|error| error.to_string()) + .unwrap(); + for bytes in [&historical, &canonical] { + assert_consumers(&runtime, &envelope.policy, bytes, true).await; + } + assert_consumers(&runtime, &envelope.policy, &unrelated, false).await; +} + +#[tokio::test] +async fn signed_legacy_revocation_denies_canonical_share_owner_consumers() { + signed_alias_consumers(false).await; +} + +#[tokio::test] +async fn signed_canonical_revocation_denies_legacy_share_owner_consumers() { + signed_alias_consumers(true).await; +} From bed0cf9ca53d748846d6d411fbfa280516b36b7f Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Wed, 14 Oct 2026 23:47:07 +0000 Subject: [PATCH 09/12] test: register valid signed grants before W5 restart --- .../tests/sql_isolation.rs | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/test/w5-policy-runtime-node-e2e/tests/sql_isolation.rs b/test/w5-policy-runtime-node-e2e/tests/sql_isolation.rs index 46e53ad1..336ee63d 100644 --- a/test/w5-policy-runtime-node-e2e/tests/sql_isolation.rs +++ b/test/w5-policy-runtime-node-e2e/tests/sql_isolation.rs @@ -178,6 +178,7 @@ async fn legacy_web_artifacts_load_via_invoke_after_alias_cutover_and_restart() let duck_resource = db_resource(&space_id, "duckdb", "appA/")?; let connectors_a = db_resource(&space_id, "sql", "xyz.tinycloud.tinychat/connectors")?; let connectors_b = db_resource(&space_id, "sql", "another.app/connectors")?; + let mut grant_cids = Vec::with_capacity(5); for (index, resource) in [ &sql_resource, &canvas_resource, @@ -193,15 +194,41 @@ async fn legacy_web_artifacts_load_via_invoke_after_alias_cutover_and_restart() } else { "tinycloud.duckdb/read" }; - persist_grant( - &conn, - tinycloud_core::hash::hash(format!("n3-grant-{index}").as_bytes()), - &owner_did, - &holder_did, - resource, - ability, - ) - .await?; + // Restart validates stored signed bytes and their content IDs. + // Register real grants rather than metadata-only synthetic rows. + let mut caps = Capabilities::new(); + caps.with_action( + resource.as_uri(), + ability.parse::()?, + [BTreeMap::::new()], + ); + let grant = Payload { + issuer: owner_vm.parse::()?, + audience: holder_did.parse::()?, + not_before: None, + expiration: NumericDate::try_from_seconds(FAR_FUTURE_SECONDS)?, + nonce: Some(format!("tc780-restart-grant-{index}")), + facts: Some(Vec::::new()), + proof: vec![], + attenuation: caps, + } + .sign(owner_jwk.get_algorithm().unwrap_or_default(), &owner_jwk)? + .encode()?; + let response = client + .post("/delegate") + .header(Header::new("Authorization", grant)) + .dispatch() + .await; + let status = response.status(); + let body = response.into_string().await.unwrap_or_default(); + assert_eq!(status, Status::Ok, "register signed grant: {body}"); + let registered: serde_json::Value = serde_json::from_str(&body)?; + grant_cids.push( + registered["cid"] + .as_str() + .context("registered delegation CID")? + .parse::()?, + ); } let items = database_migration::inventory(&conn, &datadir).await?; @@ -266,24 +293,18 @@ async fn legacy_web_artifacts_load_via_invoke_after_alias_cutover_and_restart() "13", ), ] { - let grant = tinycloud_core::hash::hash( - format!( - "n3-grant-{}", - if suffix == "11" { - 0 - } else if suffix == "12" { - 1 - } else { - 2 - } - ) - .as_bytes(), - ); + let grant = grant_cids[if suffix == "11" { + 0 + } else if suffix == "12" { + 1 + } else { + 2 + }]; let header = invocation( &holder_jwk, &holder_did, &holder_vm, - vec![grant.to_cid(0x55)], + vec![grant], resource, ability, &format!("urn:uuid:00000000-0000-4000-8000-0000000000{suffix}"), @@ -426,7 +447,7 @@ async fn legacy_web_artifacts_load_via_invoke_after_alias_cutover_and_restart() &holder_jwk, &holder_did, &holder_vm, - vec![tinycloud_core::hash::hash(b"n3-grant-3").to_cid(0x55)], + vec![grant_cids[3]], &connectors_a, "tinycloud.sql/read", "urn:uuid:00000000-0000-4000-8000-000000000019", @@ -443,12 +464,11 @@ async fn legacy_web_artifacts_load_via_invoke_after_alias_cutover_and_restart() ) .await?; database_migration::set_fence(&conn, false).await?; - assert_eq!( + assert!( database_migration::resolve(&conn, "sql", &space_id.to_string(), Some("connectors")) .await - .unwrap_err() - .to_string(), - "quarantined legacy artifact is unreachable without an explicit alias" + .is_err(), + "clearing the alias must not expose the quarantined legacy artifact" ); drop(client); let restarted = restart_node(&tempdir, false).await?; @@ -456,7 +476,7 @@ async fn legacy_web_artifacts_load_via_invoke_after_alias_cutover_and_restart() &holder_jwk, &holder_did, &holder_vm, - vec![tinycloud_core::hash::hash(b"n3-grant-0").to_cid(0x55)], + vec![grant_cids[0]], &sql_resource, "tinycloud.sql/read", "urn:uuid:00000000-0000-4000-8000-000000000017", From ef256dcd804dcd861b4fa0087f3b20bc04930f66 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Thu, 15 Oct 2026 14:06:47 +0000 Subject: [PATCH 10/12] TC-821 Enable direct owner revoke --- CHANGELOG.md | 1 + tinycloud-core/src/models/delegation.rs | 13 ++ tinycloud-core/src/models/revocation.rs | 135 +++++++++++--- .../tests/historical_cacao_revocation.rs | 151 +++++++++++++++- tinycloud-node-server/src/routes/mod.rs | 165 +++++++++++++++++- 5 files changed, 439 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1561ad5b..6199f73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- Owners can activate a single-target `urn:cid:` revocation grant without the target's signed artifact when its persisted abilities name their space. Direct revocation accepts the stored target or its historical canonical CID, and alias-equivalent proof targets use the same effective identity; unrelated owners and unknown targets remain denied (TC-721, TC-821). - CACAO/SIWE delegation ingestion normalizes ECDSA recovery IDs 0/1 to 27/28 before hashing and persisting the canonical DAG-CBOR bytes (TC-1071). Equivalent low-s signatures now share one delegation CID, including the CID used for revocation lookup. High-s signatures are rejected during decoding, matching the existing EIP-191 verifier and EIP-2 low-s convention; UCAN/JWS bytes remain unchanged. Existing stored delegation IDs are not rewritten. - Synchronize the standalone M1, W5 and load-signer Cargo locks with CACAO's `k256` dependency so `--locked` dependency resolution succeeds (TC-1071). diff --git a/tinycloud-core/src/models/delegation.rs b/tinycloud-core/src/models/delegation.rs index 98fc2c03..4c265f0f 100644 --- a/tinycloud-core/src/models/delegation.rs +++ b/tinycloud-core/src/models/delegation.rs @@ -253,6 +253,19 @@ async fn validate( // ordered sibling pair. Keeping the graph row here is what lets the // ordinary transaction validate an S0's proof CIDs. (false, true) if is_policy_root(&delegation.delegation) => Ok(()), + // A space owner may mint a narrowly scoped control grant for a + // persisted target without possessing the target's signed bytes. + (false, true) + if delegation.capabilities.len() == 1 + && revocation::owner_controls_target( + db, + &delegation.delegator, + dependant_caps[0], + ) + .await? => + { + Ok(()) + } // dependant caps, no parents, invalid (false, true) => Err(DelegationError::MissingParents.into()), // dependant caps, parents, check parents diff --git a/tinycloud-core/src/models/revocation.rs b/tinycloud-core/src/models/revocation.rs index a37d5034..979787a3 100644 --- a/tinycloud-core/src/models/revocation.rs +++ b/tinycloud-core/src/models/revocation.rs @@ -78,7 +78,11 @@ fn target_condition(id: SimpleExpr) -> Condition { let canonical = Query::select() .column(AliasColumn::Canonical) .from(AliasEntity) - .and_where(Expr::col(AliasColumn::Id).eq(id.clone())) + .cond_where( + Condition::any() + .add(Expr::col(AliasColumn::Id).eq(id.clone())) + .add(Expr::col(AliasColumn::Canonical).eq(id.clone())), + ) .to_owned(); let equivalents = Query::select() .column(AliasColumn::Id) @@ -304,13 +308,35 @@ pub(crate) async fn control_proof_decision( .filter(abilities::Column::Ability.eq(requested_action)) .all(db) .await?; - let target_resource = format!("urn:cid:{}", target.to_cid(0x55)); - let has_control_ability = control_abilities - .iter() - .any(|ability| match &ability.resource { - Resource::Other(resource) => resource.as_str() == target_resource, + let target_canonical = delegation_cid_alias::Entity::find_by_id(*target) + .one(db) + .await? + .map(|alias| alias.canonical) + .unwrap_or(*target); + let mut has_control_ability = false; + for ability in &control_abilities { + match &ability.resource { + Resource::Other(resource) => { + let Some(control_target) = resource + .as_str() + .strip_prefix("urn:cid:") + .and_then(|cid| cid.parse::().ok()) + .map(Hash::from) + else { + continue; + }; + let control_canonical = delegation_cid_alias::Entity::find_by_id(control_target) + .one(db) + .await? + .map(|alias| alias.canonical) + .unwrap_or(control_target); + if control_canonical == target_canonical { + has_control_ability = true; + break; + } + } Resource::TinyCloud(resource) => { - resource.service().as_str() == "delegation" + if resource.service().as_str() == "delegation" && resource .path() .map(|path| path.as_str().is_empty()) @@ -318,14 +344,72 @@ pub(crate) async fn control_proof_decision( && resource.query().is_none() && resource.fragment().is_none() && resource.space().did().as_str() == parent.delegator + { + has_control_ability = true; + break; + } } - }); + } + } if !has_control_ability { return Ok(ControlProofDecision::Denied); } Ok(ControlProofDecision::PersistentPrincipal(parent.delegator)) } +/// A canonical CID can name a historical stored row without being its primary +/// key. Keep the signed CID on the wire; use the stored row for authorization. +async fn target_for_cid( + db: &C, + target: Hash, +) -> Result, DbErr> { + if let Some(row) = delegation::Entity::find_by_id(target).one(db).await? { + return Ok(Some(row)); + } + let Some(alias) = delegation_cid_alias::Entity::find() + .filter(delegation_cid_alias::Column::Canonical.eq(target)) + .one(db) + .await? + else { + return Ok(None); + }; + delegation::Entity::find_by_id(alias.id).one(db).await +} + +/// Root CID control is available only to an owner of a space in the target's +/// persisted abilities. A delegator or delegatee alone is not an owner. +pub(crate) async fn owner_controls_target( + db: &C, + owner: &str, + cap: &crate::util::Capability, +) -> Result { + if cap.ability.as_ref().as_ref() != "tinycloud.delegation/revoke" { + return Ok(false); + } + let Resource::Other(resource) = &cap.resource else { + return Ok(false); + }; + let Some(target) = resource + .as_str() + .strip_prefix("urn:cid:") + .and_then(|cid| cid.parse::().ok()) + else { + return Ok(false); + }; + let Some(target) = target_for_cid(db, Hash::from(target)).await? else { + return Ok(false); + }; + let abilities = abilities::Entity::find() + .filter(abilities::Column::Delegation.eq(target.id)) + .all(db) + .await?; + Ok(abilities.iter().any(|row| { + row.resource + .space() + .is_some_and(|space| did_principal_matches(space.did().as_str(), owner)) + })) +} + #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] @@ -390,8 +474,7 @@ pub(crate) async fn process( }; let hash: Hash = hash(&serialization); - let delegation = delegation::Entity::find_by_id(Hash::from(r.revoked)) - .one(db) + let delegation = target_for_cid(db, Hash::from(r.revoked)) .await? .ok_or(RevocationError::MissingParents)?; @@ -404,7 +487,15 @@ pub(crate) async fn process( // Any one of these is sufficient. The Cacao revoker == delegator // path stays as the cheapest check; owner-authorized falls back to // walking the ability rows. - if !revoker_is_authorized(db, &delegation, &r.revoker, &r.parents).await? { + if !revoker_is_authorized( + db, + &delegation, + &r.revoker, + &r.parents, + &Hash::from(r.revoked), + ) + .await? + { return Err(RevocationError::UnauthorizedRevoker(r.revoker).into()); }; @@ -453,20 +544,16 @@ async fn revoker_is_authorized( delegation: &delegation::Model, revoker: &str, proofs: &[Cid], + target: &Hash, ) -> Result { - let principal = match control_proof_decision( - db, - revoker, - proofs, - "tinycloud.delegation/revoke", - &delegation.id, - ) - .await? - { - ControlProofDecision::DirectSigner(principal) - | ControlProofDecision::PersistentPrincipal(principal) => principal, - ControlProofDecision::Denied => return Ok(false), - }; + let principal = + match control_proof_decision(db, revoker, proofs, "tinycloud.delegation/revoke", target) + .await? + { + ControlProofDecision::DirectSigner(principal) + | ControlProofDecision::PersistentPrincipal(principal) => principal, + ControlProofDecision::Denied => return Ok(false), + }; if did_principal_matches(&delegation.delegator, &principal) { return Ok(true); } diff --git a/tinycloud-core/tests/historical_cacao_revocation.rs b/tinycloud-core/tests/historical_cacao_revocation.rs index 38143a80..d16881a4 100644 --- a/tinycloud-core/tests/historical_cacao_revocation.rs +++ b/tinycloud-core/tests/historical_cacao_revocation.rs @@ -13,7 +13,7 @@ use tinycloud_auth::{ }; use tinycloud_core::{ db::SpaceDatabase, - events::Revocation, + events::{Delegation, Revocation}, keys::StaticSecret, models::{delegation, revocation}, relationships::parent_delegations, @@ -274,6 +274,155 @@ async fn upgrade_active_legacy_children_remain_valid_until_owner_revokes_either_ } } +fn owner_cid_control(target: &str, audience: &str) -> String { + use tinycloud_auth::{ + cacaos::{ + siwe::Message, + siwe_cacao::{Header, Signature, SiweCacao}, + }, + siwe_recap::{Ability, Capability}, + }; + let mut recap = Capability::::new(); + recap.with_action( + format!("urn:cid:{target}").parse().unwrap(), + "tinycloud.delegation/revoke".parse::().unwrap(), + [std::collections::BTreeMap::new()], + ); + let message: Message = signed_cacao(audience, None, "tc821-cid-control") + .payload() + .clone() + .try_into() + .unwrap(); + let message = recap.build_message(message).unwrap(); + let (key, _, _) = owner(); + let (signature, recovery) = key + .sign_prehash_recoverable(&message.eip191_hash().unwrap()) + .unwrap(); + let mut bytes = [0; 65]; + bytes[..64].copy_from_slice(signature.to_bytes().as_ref()); + bytes[64] = u8::from(recovery) + 27; + TinyCloudDelegation::Cacao(Box::new(SiweCacao::new( + message.into(), + Signature::from(bytes), + Header, + ))) + .encode() + .unwrap() +} + +fn session_cid_revocation(target: &str, control: &str, seed: u8) -> Revocation { + use tinycloud_auth::{ + ssi::{claims::jwt::NumericDate, jwk::Algorithm, ucan::Payload}, + ucan_capabilities_object::Capabilities, + }; + let (key, did, vm) = session(seed); + let mut caps = Capabilities::new(); + caps.with_action( + format!("urn:cid:{target}").parse().unwrap(), + "tinycloud.delegation/revoke".parse().unwrap(), + [std::collections::BTreeMap::::new()], + ); + let header = TinyCloudRevocation::Ucan(Box::new( + Payload { + issuer: vm.parse().unwrap(), + audience: did.parse().unwrap(), + not_before: None, + expiration: NumericDate::try_from_seconds(EXPIRATION).unwrap(), + nonce: Some("tc821-session-revoke".into()), + facts: None, + proof: vec![control.parse().unwrap()], + attenuation: caps, + } + .sign(Algorithm::EdDSA, &key) + .unwrap(), + )) + .encode() + .unwrap(); + Revocation::from_header_ser::(&header).unwrap() +} + +#[tokio::test] +async fn owner_cid_control_revokes_historical_equivalents_without_target_artifact() { + for database in ["active.sqlite", "single-v0.sqlite"] { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == database) + .unwrap(); + for target in &fixture.rows { + let (_directory, path) = copy_database(&fixture); + let before = signed_snapshot(&path); + let db = open(&path).await; + let (_, audience, _) = session(fixture.session_seed); + let header = owner_cid_control(&target.raw_cid, &audience); + db.delegate(Delegation::from_header_ser::(&header).unwrap()) + .await + .map_err(|error| error.to_string()) + .unwrap(); + let signed = + session_cid_revocation(&target.raw_cid, &cid(&header), fixture.session_seed); + db.revoke(signed) + .await + .map_err(|error| error.to_string()) + .unwrap(); + for equivalent in &fixture.rows { + assert_eq!( + db.delegation_status(hash_from_cid(&equivalent.raw_cid), &fixture.issuer, &[]) + .await + .map_err(|error| error.to_string()) + .unwrap(), + Some(tinycloud_core::db::DelegationStatus::Revoked), + "{database} {} via {}", + equivalent.encoding, + target.encoding, + ); + } + let after = signed_snapshot(&path); + assert!(before[0].iter().all(|row| after[0].contains(row))); + assert!(before[1].iter().all(|row| after[1].contains(row))); + } + } +} + +#[tokio::test] +async fn owner_cid_control_resolves_unstored_canonical_target() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "single-v0.sqlite") + .unwrap(); + let canonical = current_cid(&fixture.canonical_header); + let raw = &fixture.rows[0].raw_cid; + assert_ne!(canonical, *raw); + for (control_target, signed_target) in [(&canonical, raw), (raw, &canonical)] { + let (_directory, path) = copy_database(&fixture); + let db = open(&path).await; + let (_, audience, _) = session(fixture.session_seed); + let header = owner_cid_control(control_target, &audience); + db.delegate(Delegation::from_header_ser::(&header).unwrap()) + .await + .map_err(|error| error.to_string()) + .unwrap(); + tokio::time::timeout( + std::time::Duration::from_secs(5), + db.revoke(session_cid_revocation( + signed_target, + &cid(&header), + fixture.session_seed, + )), + ) + .await + .expect("same-registry root/alias revoke must not deadlock") + .map_err(|error| error.to_string()) + .unwrap(); + assert_eq!( + db.delegation_status(hash_from_cid(raw), &fixture.issuer, &[]) + .await + .map_err(|error| error.to_string()) + .unwrap(), + Some(tinycloud_core::db::DelegationStatus::Revoked), + ); + } +} + #[tokio::test] async fn new_writes_deduplicate_equivalent_cacao_and_revoke_the_single_canonical_row() { let fixture = fixtures() diff --git a/tinycloud-node-server/src/routes/mod.rs b/tinycloud-node-server/src/routes/mod.rs index 94cf9f1d..abdadfe9 100644 --- a/tinycloud-node-server/src/routes/mod.rs +++ b/tinycloud-node-server/src/routes/mod.rs @@ -8266,7 +8266,7 @@ mod tests { DuckDbService::new(path, u64::MAX, 300, "256MB".into(), repo) }; let rocket = rocket::build() - .mount("/", rocket::routes![invoke, info, delegate]) + .mount("/", rocket::routes![invoke, info, delegate, revoke]) .attach(crate::tracing::TracingFairing::new( &Config::default().log.tracing, )) @@ -9723,6 +9723,169 @@ mod tests { Ok(()) } + #[tokio::test] + async fn owner_can_activate_cid_root_revoke_without_target_artifact() -> Result<()> { + use rocket::http::{Header, Status}; + use tinycloud_auth::ssi::{ + dids::{DIDBuf, DIDURLBuf}, + ucan::Payload, + }; + use tinycloud_auth::ucan_capabilities_object::Capabilities; + + let fixture = KvSyncFixture::new().await?; + let target = fixture + .signed_root_grant( + "owner-cid-target", + "tinycloud.kv/sync", + whole_seconds_from_now(3600), + false, + ) + .await?; + let target_cid = target.to_cid(0x55); + assert_eq!( + fixture.sync("notes/", target, &[]).await?.status, + Status::Ok + ); + + let mut capabilities = Capabilities::new(); + capabilities.with_action( + format!("urn:cid:{target_cid}").parse()?, + "tinycloud.delegation/revoke".parse::()?, + [std::collections::BTreeMap::::new()], + ); + let root = Payload { + issuer: fixture.owner_vm.parse::()?, + audience: fixture.device_did().parse::()?, + not_before: None, + expiration: test_invocation_expiration(), + nonce: Some(fixture.next_nonce()), + facts: Some(Vec::::new()), + proof: vec![], + attenuation: capabilities.clone(), + } + .sign( + fixture.owner.get_algorithm().unwrap_or_default(), + &fixture.owner, + )? + .encode()?; + let activated = fixture + .client + .post("/delegate") + .header(Header::new("Authorization", root)) + .dispatch() + .await; + let status = activated.status(); + let body = activated.into_string().await.unwrap_or_default(); + assert_eq!(status, Status::Ok, "{body}"); + let control_cid = serde_json::from_str::(&body)?["cid"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("missing control CID"))? + .parse::()?; + + let revoke = Payload { + issuer: fixture.device_vm.parse::()?, + audience: fixture.device_did().parse::()?, + not_before: None, + expiration: test_invocation_expiration(), + nonce: Some(fixture.next_nonce()), + facts: Some(Vec::::new()), + proof: vec![control_cid], + attenuation: capabilities, + } + .sign( + fixture.device.get_algorithm().unwrap_or_default(), + &fixture.device, + )? + .encode()?; + let revoked = fixture + .client + .post("/revoke") + .header(Header::new("Authorization", revoke)) + .dispatch() + .await; + let status = revoked.status(); + let body = revoked.into_string().await.unwrap_or_default(); + assert_eq!(status, Status::Ok, "{body}"); + assert_eq!( + fixture.sync("notes/", target, &[]).await?.status, + Status::Unauthorized + ); + Ok(()) + } + + #[tokio::test] + async fn cid_root_revoke_rejects_foreign_signer_and_unknown_target() -> Result<()> { + use rocket::http::{Header, Status}; + use tinycloud_auth::ssi::{ + dids::{DIDBuf, DIDURLBuf}, + ucan::Payload, + }; + use tinycloud_auth::ucan_capabilities_object::Capabilities; + + let fixture = KvSyncFixture::new().await?; + let target = fixture + .signed_root_grant( + "owner-only-revoke", + "tinycloud.kv/sync", + whole_seconds_from_now(3600), + false, + ) + .await?; + let unknown = tinycloud_core::hash::hash(b"unknown-revoke-target"); + let foreign = JWK::generate_ed25519()?; + let foreign_vm = tc341_verification_method(&foreign)?; + let owner_did = fixture.space.did().to_string(); + let device_did = fixture.device_did(); + let empty = fixture + .grant( + "owner-revoke-no-abilities", + &owner_did, + &device_did, + &[], + None, + Some(whole_seconds_from_now(3600)), + &[], + ) + .await?; + for (key, issuer, cid) in [ + (&foreign, foreign_vm.as_str(), target), + (&fixture.device, fixture.device_vm.as_str(), target), + (&fixture.owner, fixture.owner_vm.as_str(), unknown), + (&fixture.owner, fixture.owner_vm.as_str(), empty), + ] { + let mut capabilities = Capabilities::new(); + capabilities.with_action( + format!("urn:cid:{}", cid.to_cid(0x55)).parse()?, + "tinycloud.delegation/revoke".parse::()?, + [std::collections::BTreeMap::::new()], + ); + let header = Payload { + issuer: issuer.parse::()?, + audience: fixture.device_did().parse::()?, + not_before: None, + expiration: test_invocation_expiration(), + nonce: Some(fixture.next_nonce()), + facts: Some(Vec::::new()), + proof: vec![], + attenuation: capabilities, + } + .sign(key.get_algorithm().unwrap_or_default(), key)? + .encode()?; + let response = fixture + .client + .post("/delegate") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + assert_eq!(response.status(), Status::Unauthorized, "target {cid:?}"); + } + assert_eq!( + fixture.sync("notes/", target, &[]).await?.status, + Status::Ok + ); + Ok(()) + } + #[tokio::test] async fn kv_sync_revoked_grant_is_401_delegation_revoked() -> Result<()> { use rocket::http::Status; From 8e1b3ff4db4e59792cc458b8641ae06238e1d378 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Thu, 15 Oct 2026 15:48:48 +0000 Subject: [PATCH 11/12] Align short-lived grant fixture timing Locked M1 tests, strict lint and formatting passed. TC-1071. --- test/m1-realdata-e2e/README.md | 7 ++++++- test/m1-realdata-e2e/tests/cross_layer_contract.rs | 12 +++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/test/m1-realdata-e2e/README.md b/test/m1-realdata-e2e/README.md index 524cbd78..599c9193 100644 --- a/test/m1-realdata-e2e/README.md +++ b/test/m1-realdata-e2e/README.md @@ -19,6 +19,11 @@ named-statement caveats, and the minimum/maximum TTL bounds. Producer and audit cases remain explicitly assigned to m1-g-07 in `vendor/grant-output/ SKIP_MANIFEST.json`. +The one-second live grant is signed for the next whole Unix second and +submitted once its validity window opens, matching the emitter's integer +`nbf`/`exp` timestamps. Successful real-node use and denial after expiry are +both required; frozen vectors and production clocks are unchanged. + The pinned node has no production route for initial space creation. Following the W5 recipe, this test directly provisions one space row and its storage directory solely as a node-storage precondition, then proves delegation and @@ -34,7 +39,7 @@ correspondence with every frozen case. Run it explicitly with the pinned private git dependency: ```sh -CARGO_NET_GIT_FETCH_WITH_CLI=true cargo test --manifest-path test/m1-realdata-e2e/Cargo.toml +CARGO_NET_GIT_FETCH_WITH_CLI=true cargo test --locked --manifest-path test/m1-realdata-e2e/Cargo.toml ``` ## Live M1 gate runner and independent verifier diff --git a/test/m1-realdata-e2e/tests/cross_layer_contract.rs b/test/m1-realdata-e2e/tests/cross_layer_contract.rs index 0f2758fa..c220dfb7 100644 --- a/test/m1-realdata-e2e/tests/cross_layer_contract.rs +++ b/test/m1-realdata-e2e/tests/cross_layer_contract.rs @@ -1366,7 +1366,14 @@ async fn observe_grant_output_on_real_node( ]; for (case_name, capabilities, ttl_seconds) in interop_cases { let issued_capabilities = capabilities.clone(); - let issued_at = Utc::now(); + // The issuer encodes nbf/exp as whole Unix seconds. Prepare the + // one-second grant before its window, not at an arbitrary fraction. + let issued_at = if ttl_seconds == 1 { + DateTime::from_timestamp(Utc::now().timestamp() + 1, 0) + .context("minimum-TTL whole-second issuance")? + } else { + Utc::now() + }; let mut engine_policy = policy(&space_id, &grant_issuer_did); engine_policy.policy_id = format!("pol_m1_g08_{case_name}"); engine_policy.resource.permissions_ceiling = capabilities.clone(); @@ -1383,6 +1390,9 @@ async fn observe_grant_output_on_real_node( evidence_provenance: Vec::new(), policy: engine_policy, })?; + if let Ok(until_valid) = (issued_at - Utc::now()).to_std() { + tokio::time::sleep(until_valid).await; + } let emitted_response = client .post("/delegate") .header(Header::new("Authorization", emitted.encoded.clone())) From 803dae66a19d29cbef025ff1c724cc3407b6ac31 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 22:41:17 +0000 Subject: [PATCH 12/12] TC-821: Preserve scoped alias revocation --- CHANGELOG.md | 2 +- tinycloud-core/src/db.rs | 12 +- tinycloud-core/src/models/revocation.rs | 105 ++++++++++++------ .../tests/historical_cacao_revocation.rs | 26 +++++ .../src/routes/tests/revocation_scope.rs | 85 +++++++++++++- 5 files changed, 187 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecdd3ee..c68932d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## [Unreleased] -- Owners can activate a single-target `urn:cid:` revocation grant without the target's signed artifact when its persisted abilities name their space. Direct revocation accepts the stored target or its historical canonical CID, and alias-equivalent proof targets use the same effective identity; unrelated owners and unknown targets remain denied (TC-721, TC-821). +- Owners can activate a single-target `urn:cid:` revocation grant without the target's signed artifact when its persisted abilities name their space. Direct revocation accepts the stored target or its historical canonical CID, and alias-equivalent proof targets use the same effective identity. Status resolves an unstored canonical CID to its persisted row while preserving the requested CID's authority scope; unrelated owners and unknown targets remain denied (TC-721, TC-821). - CACAO/SIWE delegation ingestion normalizes ECDSA recovery IDs 0/1 to 27/28 before hashing and persisting the canonical DAG-CBOR bytes (TC-1071). Equivalent low-s signatures now share one delegation CID, including the CID used for revocation lookup. High-s signatures are rejected during decoding, matching the existing EIP-191 verifier and EIP-2 low-s convention; UCAN/JWS bytes remain unchanged. Existing stored delegation IDs are not rewritten. - Synchronize the standalone M1, W5 and load-signer Cargo locks with CACAO's `k256` dependency so `--locked` dependency resolution succeeds (TC-1071). Keep the load signer compatible with the SDK's default invocation lifetime. diff --git a/tinycloud-core/src/db.rs b/tinycloud-core/src/db.rs index 7d41a9e1..2f5035a5 100644 --- a/tinycloud-core/src/db.rs +++ b/tinycloud-core/src/db.rs @@ -1796,18 +1796,18 @@ where if proofs.len() > 1 { return Ok(None); } - let Some(delegation) = delegation::Entity::find_by_id(target) - .one(&self.conn) - .await? - else { + let Some(delegation) = revocation::target_for_cid(&self.conn, target).await? else { return Ok(None); }; let abilities = abilities::Entity::find() - .filter(abilities::Column::Delegation.eq(target)) + .filter(abilities::Column::Delegation.eq(delegation.id)) .all(&self.conn) .await?; let mut roots = vec![target]; + if delegation.id != target { + roots.push(delegation.id); + } roots.extend(proofs.iter().copied().map(Hash::from)); let _chain_guards = match self.acquire_chain_guards(&roots).await { Ok(guards) => guards, @@ -1849,7 +1849,7 @@ where if revocation::is_revoked(&self.conn, &target).await? { return Ok(Some(DelegationStatus::Revoked)); } - match revocation::first_revoked_ancestor(&self.conn, &target).await { + match revocation::first_revoked_ancestor(&self.conn, &delegation.id).await { Ok(Some(_)) => return Ok(Some(DelegationStatus::Revoked)), Ok(None) => {} Err(revocation::ChainTraversalError::LimitExceeded) => { diff --git a/tinycloud-core/src/models/revocation.rs b/tinycloud-core/src/models/revocation.rs index f8957a12..bb443353 100644 --- a/tinycloud-core/src/models/revocation.rs +++ b/tinycloud-core/src/models/revocation.rs @@ -371,20 +371,60 @@ pub(crate) async fn control_proof_decision( .filter(abilities::Column::Ability.eq(requested_action)) .all(db) .await?; - let target_resource = format!("urn:cid:{}", target.to_cid(0x55)); - control_abilities.retain(|ability| match &ability.resource { - Resource::Other(resource) => resource.as_str() == target_resource, - Resource::TinyCloud(resource) => { - resource.service().as_str() == "delegation" - && resource - .path() - .map(|path| path.as_str().is_empty()) - .unwrap_or(true) - && resource.query().is_none() - && resource.fragment().is_none() - && did_principal_matches(resource.space().did().as_str(), &parent.delegator) + let mut canonical_target = None; + let mut index = 0; + while index < control_abilities.len() { + let matches = match &control_abilities[index].resource { + Resource::Other(resource) => { + let control_target = resource + .as_str() + .strip_prefix("urn:cid:") + .and_then(|cid| cid.parse::().ok()) + .map(Hash::from); + if let Some(control_target) = control_target { + if control_target == *target { + true + } else { + let target_canonical = if let Some(canonical) = canonical_target { + canonical + } else { + let canonical = delegation_cid_alias::Entity::find_by_id(*target) + .one(db) + .await? + .map(|alias| alias.canonical) + .unwrap_or(*target); + canonical_target = Some(canonical); + canonical + }; + let control_canonical = + delegation_cid_alias::Entity::find_by_id(control_target) + .one(db) + .await? + .map(|alias| alias.canonical) + .unwrap_or(control_target); + control_canonical == target_canonical + } + } else { + false + } + } + Resource::TinyCloud(resource) => { + resource.service().as_str() == "delegation" + && resource + .path() + .map(|path| path.as_str().is_empty()) + .unwrap_or(true) + && resource.query().is_none() + && resource.fragment().is_none() + && did_principal_matches(resource.space().did().as_str(), &parent.delegator) + } + }; + if matches { + index += 1; + } else { + control_abilities.swap_remove(index); } - }); + } if control_abilities.is_empty() { return Ok(ControlProofDecision::Denied); } @@ -397,7 +437,7 @@ pub(crate) async fn control_proof_decision( /// A canonical CID can name a historical stored row without being its primary /// key. Keep the signed CID on the wire; use the stored row for authorization. -async fn target_for_cid( +pub(crate) async fn target_for_cid( db: &C, target: Hash, ) -> Result, DbErr> { @@ -584,28 +624,23 @@ async fn revoker_is_authorized( proofs: &[Cid], target: &Hash, ) -> Result { - let principal = match control_proof_decision( - db, - revoker, - proofs, - "tinycloud.delegation/revoke", - &delegation.id, - ) - .await? - { - ControlProofDecision::DirectSigner(principal) => principal, - ControlProofDecision::Scoped(control) => { - if control.directly_authorizes_cid(&delegation.id, delegation) { - return Ok(true); + let principal = + match control_proof_decision(db, revoker, proofs, "tinycloud.delegation/revoke", target) + .await? + { + ControlProofDecision::DirectSigner(principal) => principal, + ControlProofDecision::Scoped(control) => { + if control.directly_authorizes_cid(target, delegation) { + return Ok(true); + } + let target_abilities = abilities::Entity::find() + .filter(abilities::Column::Delegation.eq(delegation.id)) + .all(db) + .await?; + return Ok(control.authorizes_target(target, delegation, &target_abilities)); } - let target_abilities = abilities::Entity::find() - .filter(abilities::Column::Delegation.eq(delegation.id)) - .all(db) - .await?; - return Ok(control.authorizes_target(&delegation.id, delegation, &target_abilities)); - } - ControlProofDecision::Denied => return Ok(false), - }; + ControlProofDecision::Denied => return Ok(false), + }; if did_principal_matches(&delegation.delegator, &principal) { return Ok(true); } diff --git a/tinycloud-core/tests/historical_cacao_revocation.rs b/tinycloud-core/tests/historical_cacao_revocation.rs index d16881a4..7592e3e0 100644 --- a/tinycloud-core/tests/historical_cacao_revocation.rs +++ b/tinycloud-core/tests/historical_cacao_revocation.rs @@ -383,6 +383,25 @@ async fn owner_cid_control_revokes_historical_equivalents_without_target_artifac } } +#[tokio::test] +async fn unstored_canonical_status_resolves_historical_grant() { + let fixture = fixtures() + .into_iter() + .find(|fixture| fixture.database == "single-v0.sqlite") + .unwrap(); + let canonical = current_cid(&fixture.canonical_header); + assert_ne!(canonical, fixture.rows[0].raw_cid); + let (_directory, path) = copy_database(&fixture); + let db = open(&path).await; + assert_eq!( + db.delegation_status(hash_from_cid(&canonical), &fixture.issuer, &[]) + .await + .map_err(|error| error.to_string()) + .unwrap(), + Some(tinycloud_core::db::DelegationStatus::Active), + ); +} + #[tokio::test] async fn owner_cid_control_resolves_unstored_canonical_target() { let fixture = fixtures() @@ -420,6 +439,13 @@ async fn owner_cid_control_resolves_unstored_canonical_target() { .unwrap(), Some(tinycloud_core::db::DelegationStatus::Revoked), ); + assert_eq!( + db.delegation_status(hash_from_cid(&canonical), &fixture.issuer, &[]) + .await + .map_err(|error| error.to_string()) + .unwrap(), + Some(tinycloud_core::db::DelegationStatus::Revoked), + ); } } diff --git a/tinycloud-node-server/src/routes/tests/revocation_scope.rs b/tinycloud-node-server/src/routes/tests/revocation_scope.rs index 90cac741..638b9281 100644 --- a/tinycloud-node-server/src/routes/tests/revocation_scope.rs +++ b/tinycloud-node-server/src/routes/tests/revocation_scope.rs @@ -23,7 +23,7 @@ async fn stored_revoked( } async fn scoped_fixture() -> Result { - KvSyncFixture::new_with(|rocket| rocket.mount("/", routes![revoke])).await + KvSyncFixture::new().await } #[tokio::test] @@ -373,6 +373,89 @@ async fn space_control_cannot_revoke_a_grant_with_an_uncovered_space() -> Result .await } +#[tokio::test] +async fn space_control_covering_both_owner_spaces_revokes_a_mixed_space_grant() -> Result<()> { + use tinycloud_core::models::space as space_model; + use tinycloud_core::types::SpaceIdWrap; + + let fixture = scoped_fixture().await?; + let other = SpaceId::new(fixture.space.did().to_string().parse()?, "other".parse()?); + space_model::ActiveModel { + id: Set(SpaceIdWrap(other.clone())), + } + .insert(&fixture.conn) + .await?; + let target = fixture + .grant_in( + "covered-mixed-space-target", + &fixture.space.did().to_string(), + &fixture.device_did(), + &[ + (&fixture.space, "notes/", "tinycloud.kv/get"), + (&other, "notes/", "tinycloud.kv/get"), + ], + None, + None, + &[], + ) + .await?; + + let mut capabilities = Capabilities::new(); + for space in [&fixture.space, &other] { + capabilities.with_action( + fixture.resource_in(space, "delegation", "")?.as_uri(), + "tinycloud.delegation/revoke".parse::()?, + [std::collections::BTreeMap::::new()], + ); + } + let header = Payload { + issuer: fixture.owner_vm.parse::()?, + audience: fixture.device_did().parse()?, + not_before: None, + expiration: NumericDate::try_from_seconds( + whole_seconds_from_now(3600).unix_timestamp() as f64 + )?, + nonce: Some(fixture.next_nonce()), + facts: Some(Vec::::new()), + proof: vec![], + attenuation: capabilities, + } + .sign( + fixture.owner.get_algorithm().unwrap_or_default(), + &fixture.owner, + )? + .encode()?; + let response = fixture + .client + .post("/delegate") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + let status = response.status(); + let body = response.into_string().await.unwrap_or_default(); + assert_eq!(status, Status::Ok, "control activation: {body}"); + let control = Hash::from( + serde_json::from_str::(&body)?["cid"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("missing control CID"))? + .parse::()?, + ); + let response = fixture + .client + .post("/revoke") + .header(Header::new( + "Authorization", + fixture.scoped_revoke_header(target, Some(control))?, + )) + .dispatch() + .await; + let status = response.status(); + let body = response.into_string().await.unwrap_or_default(); + assert_eq!(status, Status::Ok, "covered mixed-space revoke: {body}"); + assert!(stored_revoked(&fixture.conn, &target).await?); + Ok(()) +} + #[tokio::test] async fn narrowed_control_path_or_action_cannot_revoke_a_target() -> Result<()> { let fixture = scoped_fixture().await?;