From dba6ead8349e79ee0ee9715086b0dfcac5724477 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:35:47 +0000 Subject: [PATCH 1/3] test: pin owner-signed renewal lease vector --- docs/specs/renewal-lease-v1.md | 22 +++ .../tests/fixtures/renewal-lease-v1.json | 21 +++ .../tests/renewal_lease_v1_vectors.rs | 164 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 docs/specs/renewal-lease-v1.md create mode 100644 tinycloud-node-server/tests/fixtures/renewal-lease-v1.json create mode 100644 tinycloud-node-server/tests/renewal_lease_v1_vectors.rs diff --git a/docs/specs/renewal-lease-v1.md b/docs/specs/renewal-lease-v1.md new file mode 100644 index 00000000..97c8e4b2 --- /dev/null +++ b/docs/specs/renewal-lease-v1.md @@ -0,0 +1,22 @@ +# Renewal lease v1 signed UCAN vector + +This is the real signed-lease companion to `renewal-stamp-v1.md`. It pins an existing TinyCloud UCAN representation for the approved exact-CID renewal grant; it does not introduce a new lease encoding or any runtime behavior. + +## Vector contract + +`tinycloud-node-server/tests/fixtures/renewal-lease-v1.json` contains two signed UCAN tokens, their exact serialized bytes as ASCII/UTF-8 strings, and their raw-Blake3 CIDs. `tinycloud-node-server/tests/renewal_lease_v1_vectors.rs` deterministically rebuilds both tokens using the repository's `Payload::sign(Algorithm::EdDSA, …).encode()` path and compares every field with the fixture. + +The original token is signed by the test owner and grants `tinycloud.kv/get` to the test holder on the fixed `urn:cid:` content resource. The owner-signed lease token grants the derived node-renewer DID exactly one capability: + +- Ability: `tinycloud.delegation/renew` +- Resource: `urn:cid:bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4` +- Original delegation CID: `bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4` +- Lease UCAN CID: `bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq` +- Lease audience / renewer DID: `did:key:z6MkjS5oMpzSoPXDb7kCoRYCDRSNZBuijD8NjVntygpKxdwz` +- Owner issuer DID: `did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S` + +The original and lease CIDs are CIDv1 raw (`0x55`) with Blake3-256 over the exact token bytes returned by `TinyCloudDelegation::decode`. The lease's resource is derived from the canonical CID of the signed original UCAN, not from a client-provided digest. The full lease bytes are the exact `leaseToken` string in the fixture encoded as UTF-8; the original's exact bytes are its `originalToken` string encoded as UTF-8. + +The audience key is derived from the fixed test-only node static secret using `StaticSecret::derive_key(b"tinycloud/node/renewer/v1")`; its resulting `did:key` is pinned in the fixture. Owner, holder, and node-secret seed values in that fixture are public test data, not credentials. The timestamps are deterministic serialization-vector inputs, not a currently active grant or a production lease. + +This vector and the frozen stamp vector together pin signed token representation, owner-to-renewer audience, exact ability/resource, stamp bytes, and their CIDs. They do not assert that the production node accepts or stores a lease; that behavior is covered only by the later runtime implementation and tests. diff --git a/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json b/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json new file mode 100644 index 00000000..166abdb1 --- /dev/null +++ b/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json @@ -0,0 +1,21 @@ +{ + "schema": "tinycloud/node/renewal-lease-vector/v1", + "testOnlyOwnerSeedHex": "1111111111111111111111111111111111111111111111111111111111111111", + "testOnlyHolderSeedHex": "2222222222222222222222222222222222222222222222222222222222222222", + "testOnlyNodeStaticSecretHex": "4242424242424242424242424242424242424242424242424242424242424242", + "renewerKeyContext": "tinycloud/node/renewer/v1", + "issuedAt": 1700000000, + "originalExpiration": 1702592000, + "leaseExpiration": 1731536000, + "ownerDid": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "holderDid": "did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK", + "renewerDid": "did:key:z6MkjS5oMpzSoPXDb7kCoRYCDRSNZBuijD8NjVntygpKxdwz", + "originalResource": "urn:cid:bafkr4ieo6wzrrkvitu3mmgh24iziogohcw5tlfsvkmylymmqhqvvfqdot4", + "originalAbility": "tinycloud.kv/get", + "originalToken": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWt0VUx1ZFR0QXNBaFJlZ1lQaVo2NjMxUlYzdml2MTJxZDRHUUY4ejF4QjIyUyN6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMiLCJhdWQiOiJkaWQ6a2V5Ono2TWtxR0MzbldaaFlpZUVWVFZES1c1djU4OENpR2ZzRFNtUlZHOVp3d1dUdkxTSyIsIm5iZiI6MTcwMDAwMDAwMCwiZXhwIjoxNzAyNTkyMDAwLCJubmMiOiJyZW5ld2FsLWxlYXNlLXZlY3Rvci1vcmlnaW5hbC12MSIsInByZiI6W10sImF0dCI6eyJ1cm46Y2lkOmJhZmtyNGllbzZ3enJya3ZpdHUzbW1naDI0aXppb2dvaGN3NXRsZnN2a215bHltbXFocXZ2ZnFkb3Q0Ijp7InRpbnljbG91ZC5rdi9nZXQiOlt7fV19fX0.aiEIMKOTJz8fCyi5u0plLyk2IJJ3xCLfzoqBsYuyJRQDlBX_HYtRnPs5mh0zaIsnIaqZsJoVwHCZDLdzRarhBg", + "originalCid": "bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4", + "renewAbility": "tinycloud.delegation/renew", + "renewResource": "urn:cid:bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4", + "leaseToken": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWt0VUx1ZFR0QXNBaFJlZ1lQaVo2NjMxUlYzdml2MTJxZDRHUUY4ejF4QjIyUyN6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMiLCJhdWQiOiJkaWQ6a2V5Ono2TWtqUzVvTXB6U29QWERiN2tDb1JZQ0RSU05aQnVpakQ4TmpWbnR5Z3BLeGR3eiIsIm5iZiI6MTcwMDAwMDAwMCwiZXhwIjoxNzMxNTM2MDAwLCJubmMiOiJyZW5ld2FsLWxlYXNlLXZlY3Rvci1sZWFzZS12MSIsInByZiI6W10sImF0dCI6eyJ1cm46Y2lkOmJhZmtyNGliZHR3eGhvYXVjY3d1a25tZjdjZDVtM283NHJ3ZWs3YWw0d2dzMzNlY295Z3VicG1vdXI0Ijp7InRpbnljbG91ZC5kZWxlZ2F0aW9uL3JlbmV3Ijpbe31dfX19.0M8SrJcilZD1Bdf8sFiAPoTF4o8jxke3uRA3Zsg813UXCKWsQjmI1EDOH5TBH8qYofWj4Bu4_KEVcC4Z8kx1CA", + "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq" +} diff --git a/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs b/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs new file mode 100644 index 00000000..bd44daba --- /dev/null +++ b/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs @@ -0,0 +1,164 @@ +//! Test-only known-answer vector for an owner-signed UCAN renewal lease. +//! +//! This pins a real signed lease token and canonical raw-CID binding. Fixed keys +//! are public test data only; no runtime renewal code is exercised here. + +use base64::{encode_config, URL_SAFE}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use tinycloud_auth::{ + authorization::{HeaderEncode, TinyCloudDelegation}, + resolver::DID_METHODS, + siwe_recap::Ability, + ssi::{ + claims::jwt::NumericDate, + jwk::{Algorithm, JWK}, + ucan::Payload, + }, + ucan_capabilities_object::Capabilities, +}; +use tinycloud_core::{ + hash::hash, + keys::{public_key_to_did_key, Keypair, StaticSecret}, +}; + +const OWNER_SEED: [u8; 32] = [0x11; 32]; +const HOLDER_SEED: [u8; 32] = [0x22; 32]; +const NODE_STATIC_SECRET: [u8; 32] = [0x42; 32]; +const RENEWER_CONTEXT: &[u8] = b"tinycloud/node/renewer/v1"; +const ISSUED_AT: f64 = 1_700_000_000.0; +const ORIGINAL_EXPIRY: f64 = 1_702_592_000.0; +const LEASE_EXPIRY: f64 = 1_731_536_000.0; + +fn fixed_ed25519_jwk(seed: [u8; 32]) -> JWK { + let signer = ed25519_dalek::SigningKey::from_bytes(&seed); + let x = encode_config(signer.verifying_key().to_bytes(), URL_SAFE) + .trim_end_matches('=') + .to_owned(); + let d = encode_config(seed, URL_SAFE) + .trim_end_matches('=') + .to_owned(); + serde_json::from_value(json!({ + "kty": "OKP", + "crv": "Ed25519", + "alg": "EdDSA", + "x": x, + "d": d, + })) + .expect("fixed Ed25519 JWK") +} + +fn did_and_verification_method(jwk: &JWK) -> (String, String) { + let did = DID_METHODS + .generate(jwk, "key") + .expect("did:key resolves from JWK") + .to_string(); + let method = format!("{did}#{}", did.trim_start_matches("did:key:")); + (did, method) +} + +fn signed_ucan( + issuer_jwk: &JWK, + issuer_method: &str, + audience: &str, + resource: &str, + ability: &str, + nonce: &str, + expiration: f64, +) -> String { + let mut capabilities = Capabilities::::new(); + capabilities.with_action( + resource.parse().expect("test resource parses"), + ability.parse::().expect("test ability parses"), + [BTreeMap::::new()], + ); + Payload:: { + issuer: issuer_method + .parse() + .expect("issuer verification method parses"), + audience: audience.parse().expect("audience DID parses"), + not_before: Some(NumericDate::try_from_seconds(ISSUED_AT).unwrap()), + expiration: NumericDate::try_from_seconds(expiration).unwrap(), + nonce: Some(nonce.to_owned()), + facts: None, + proof: Vec::new(), + attenuation: capabilities, + } + .sign(Algorithm::EdDSA, issuer_jwk) + .expect("UCAN signs with the owner key") + .encode() + .expect("UCAN encodes as its signed token") +} + +fn canonical_ucan_cid(token: &str) -> String { + let (delegation, bytes) = TinyCloudDelegation::decode(token).expect("signed UCAN decodes"); + assert!(matches!(delegation, TinyCloudDelegation::Ucan(_))); + assert_eq!(bytes, token.as_bytes()); + hash(&bytes).to_cid(0x55).to_string() +} + +#[test] +fn renewal_lease_v1_pins_real_owner_signed_ucan_and_exact_original_cid() { + let owner_jwk = fixed_ed25519_jwk(OWNER_SEED); + let holder_jwk = fixed_ed25519_jwk(HOLDER_SEED); + let (owner_did, owner_method) = did_and_verification_method(&owner_jwk); + let (holder_did, _) = did_and_verification_method(&holder_jwk); + + let node_secret = StaticSecret::new(NODE_STATIC_SECRET.to_vec()).unwrap(); + let renewer_seed = node_secret.derive_key(RENEWER_CONTEXT); + let renewer_key = Keypair::ed25519_from_bytes(renewer_seed).unwrap(); + let renewer_did = public_key_to_did_key(renewer_key.public()); + let renewer_jwk = fixed_ed25519_jwk(renewer_seed); + let (derived_renewer_did, _) = did_and_verification_method(&renewer_jwk); + assert_eq!(renewer_did, derived_renewer_did); + + let resource_cid = hash(b"renewal-lease-vector-content-v1").to_cid(0x55); + let original_resource = format!("urn:cid:{resource_cid}"); + let original_token = signed_ucan( + &owner_jwk, + &owner_method, + &holder_did, + &original_resource, + "tinycloud.kv/get", + "renewal-lease-vector-original-v1", + ORIGINAL_EXPIRY, + ); + let original_cid = canonical_ucan_cid(&original_token); + let lease_resource = format!("urn:cid:{original_cid}"); + let lease_token = signed_ucan( + &owner_jwk, + &owner_method, + &renewer_did, + &lease_resource, + "tinycloud.delegation/renew", + "renewal-lease-vector-lease-v1", + LEASE_EXPIRY, + ); + let lease_cid = canonical_ucan_cid(&lease_token); + + let vector = json!({ + "schema": "tinycloud/node/renewal-lease-vector/v1", + "testOnlyOwnerSeedHex": hex::encode(OWNER_SEED), + "testOnlyHolderSeedHex": hex::encode(HOLDER_SEED), + "testOnlyNodeStaticSecretHex": hex::encode(NODE_STATIC_SECRET), + "renewerKeyContext": String::from_utf8_lossy(RENEWER_CONTEXT), + "issuedAt": ISSUED_AT as u64, + "originalExpiration": ORIGINAL_EXPIRY as u64, + "leaseExpiration": LEASE_EXPIRY as u64, + "ownerDid": owner_did, + "holderDid": holder_did, + "renewerDid": renewer_did, + "originalResource": original_resource, + "originalAbility": "tinycloud.kv/get", + "originalToken": original_token, + "originalCid": original_cid, + "renewAbility": "tinycloud.delegation/renew", + "renewResource": lease_resource, + "leaseToken": lease_token, + "leaseCid": lease_cid, + }); + + let fixture: Value = serde_json::from_str(include_str!("fixtures/renewal-lease-v1.json")) + .expect("renewal lease fixture parses"); + assert_eq!(vector, fixture, "pin the complete signed UCAN vector"); +} From bd2f95ca02ff25f4c67890ad0770ff0451c25295 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:02:38 +0000 Subject: [PATCH 2/3] test: pin linked renewal stamp vector --- docs/specs/renewal-lease-v1.md | 10 +- .../tests/fixtures/renewal-lease-v1.json | 27 ++++- .../tests/renewal_lease_v1_vectors.rs | 108 +++++++++++++++++- 3 files changed, 141 insertions(+), 4 deletions(-) diff --git a/docs/specs/renewal-lease-v1.md b/docs/specs/renewal-lease-v1.md index 97c8e4b2..96f709ae 100644 --- a/docs/specs/renewal-lease-v1.md +++ b/docs/specs/renewal-lease-v1.md @@ -4,7 +4,7 @@ This is the real signed-lease companion to `renewal-stamp-v1.md`. It pins an exi ## Vector contract -`tinycloud-node-server/tests/fixtures/renewal-lease-v1.json` contains two signed UCAN tokens, their exact serialized bytes as ASCII/UTF-8 strings, and their raw-Blake3 CIDs. `tinycloud-node-server/tests/renewal_lease_v1_vectors.rs` deterministically rebuilds both tokens using the repository's `Payload::sign(Algorithm::EdDSA, …).encode()` path and compares every field with the fixture. +`tinycloud-node-server/tests/fixtures/renewal-lease-v1.json` pins the signed original and lease UCANs, their exact serialized bytes as ASCII/UTF-8 strings and raw-Blake3 CIDs, and a first signed renewal stamp linked to the real lease CID. `tinycloud-node-server/tests/renewal_lease_v1_vectors.rs` deterministically rebuilds all three using the repository signing/encoding paths and compares every field, byte sequence, signature, and CID with the fixture. The original token is signed by the test owner and grants `tinycloud.kv/get` to the test holder on the fixed `urn:cid:` content resource. The owner-signed lease token grants the derived node-renewer DID exactly one capability: @@ -17,6 +17,12 @@ The original token is signed by the test owner and grants `tinycloud.kv/get` to The original and lease CIDs are CIDv1 raw (`0x55`) with Blake3-256 over the exact token bytes returned by `TinyCloudDelegation::decode`. The lease's resource is derived from the canonical CID of the signed original UCAN, not from a client-provided digest. The full lease bytes are the exact `leaseToken` string in the fixture encoded as UTF-8; the original's exact bytes are its `originalToken` string encoded as UTF-8. +The period in v1 is the owner's signed original UCAN `exp - nbf`; if `nbf` is absent, the signed issued-at value is the anchor. This vector's original period is 30 days (`2,592,000` seconds). V1 has no lease period override. The owner-signed lease expiry is a separate upper bound: this vector's lease lifetime is 365 days (`31,536,000` seconds). The test derives both values from the signed UCAN claims and checks that `newEnd = min(now + period, lease expiry)` respects the lease cap. + +The `firstStamp` entry is signed by the derived renewer key and pins canonical DAG-CBOR bytes, detached signature, and CID. Its `leaseCid` and non-null `previousStampCid` both equal the actual signed lease CID `bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq`; the test asserts both links. + The audience key is derived from the fixed test-only node static secret using `StaticSecret::derive_key(b"tinycloud/node/renewer/v1")`; its resulting `did:key` is pinned in the fixture. Owner, holder, and node-secret seed values in that fixture are public test data, not credentials. The timestamps are deterministic serialization-vector inputs, not a currently active grant or a production lease. -This vector and the frozen stamp vector together pin signed token representation, owner-to-renewer audience, exact ability/resource, stamp bytes, and their CIDs. They do not assert that the production node accepts or stores a lease; that behavior is covered only by the later runtime implementation and tests. +`nodeIdentityDid` is the stable node identity from `StaticSecret::node_did()`, serialized as the stamp's `nodeDid`. It is intentionally distinct from `renewerDid`, which is the lease audience and the DID of the separate key derived with `tinycloud/node/renewer/v1`; that renewer key signs the detached stamp. + +The signed lease vector and its linked first-stamp vector, together with the frozen stamp vector, pin the lease authority, signer/identity distinction, stamp bytes, and their CIDs. They do not assert that the production node accepts or stores a lease; that behavior is covered only by the later runtime implementation and tests. diff --git a/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json b/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json index 166abdb1..de33092a 100644 --- a/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json +++ b/tinycloud-node-server/tests/fixtures/renewal-lease-v1.json @@ -7,6 +7,9 @@ "issuedAt": 1700000000, "originalExpiration": 1702592000, "leaseExpiration": 1731536000, + "renewalPeriodSource": "owner-signed original exp - nbf", + "renewalPeriodSeconds": 2592000, + "leaseLifetimeSeconds": 31536000, "ownerDid": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", "holderDid": "did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK", "renewerDid": "did:key:z6MkjS5oMpzSoPXDb7kCoRYCDRSNZBuijD8NjVntygpKxdwz", @@ -17,5 +20,27 @@ "renewAbility": "tinycloud.delegation/renew", "renewResource": "urn:cid:bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4", "leaseToken": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJpc3MiOiJkaWQ6a2V5Ono2TWt0VUx1ZFR0QXNBaFJlZ1lQaVo2NjMxUlYzdml2MTJxZDRHUUY4ejF4QjIyUyN6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMiLCJhdWQiOiJkaWQ6a2V5Ono2TWtqUzVvTXB6U29QWERiN2tDb1JZQ0RSU05aQnVpakQ4TmpWbnR5Z3BLeGR3eiIsIm5iZiI6MTcwMDAwMDAwMCwiZXhwIjoxNzMxNTM2MDAwLCJubmMiOiJyZW5ld2FsLWxlYXNlLXZlY3Rvci1sZWFzZS12MSIsInByZiI6W10sImF0dCI6eyJ1cm46Y2lkOmJhZmtyNGliZHR3eGhvYXVjY3d1a25tZjdjZDVtM283NHJ3ZWs3YWw0d2dzMzNlY295Z3VicG1vdXI0Ijp7InRpbnljbG91ZC5kZWxlZ2F0aW9uL3JlbmV3Ijpbe31dfX19.0M8SrJcilZD1Bdf8sFiAPoTF4o8jxke3uRA3Zsg813UXCKWsQjmI1EDOH5TBH8qYofWj4Bu4_KEVcC4Z8kx1CA", - "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq" + "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq", + "firstStamp": { + "stamp": { + "domain": "tinycloud/node/renewal-stamp/v1", + "unsigned": { + "type": "TinyCloudRenewalStamp", + "version": 1, + "originalCid": "bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4", + "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq", + "leaseGeneration": 1, + "previousStampCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq", + "nodeDid": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "renewerEpoch": 1, + "newEnd": "2024-01-12T22:13:20Z" + } + }, + "canonicalDagCborHex": "a266646f6d61696e781f74696e79636c6f75642f6e6f64652f72656e6577616c2d7374616d702f763168756e7369676e6564a964747970657554696e79436c6f756452656e6577616c5374616d70666e6577456e6474323032342d30312d31325432323a31333a32305a676e6f646544696478386469643a6b65793a7a364d6b7058766555796a7043446536503833724e5971415a6d6f546339716f704a364742755251526f747a337a46376776657273696f6e01686c65617365436964783b6261666b723469676e6833696f77613567797367656774756f7361363270646b77366d70767268737233756d6a65793578327a6f3466766a6771716b6f726967696e616c436964783b6261666b7234696264747778686f6175636377756b6e6d66376364356d336f37347277656b37616c34776773333365636f79677562706d6f7572346c72656e6577657245706f6368016f6c6561736547656e65726174696f6e017070726576696f75735374616d70436964783b6261666b723469676e6833696f77613567797367656774756f7361363270646b77366d70767268737233756d6a65793578327a6f3466766a677171", + "stampCid": "bafkr4iewc5ewhaccw5xh3cvxokvkehjvf76atyhvftq4i4eq7ayneimt3e", + "signerDid": "did:key:z6MkjS5oMpzSoPXDb7kCoRYCDRSNZBuijD8NjVntygpKxdwz", + "signerPublicKeyHex": "49fd44f3fe8930b0e89f61c51333195578deacaf22105a78f647d8c05afc7c5d", + "signatureHex": "763762b3c3d2e57dae2dfeda99ab71d4c286a20da085f9ea3435e972a81b1efe01e8aab34866bfd32bb970fda0fa749aa11577d62364d0becae00713d074620e" + }, + "nodeIdentityDid": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7" } diff --git a/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs b/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs index bd44daba..1f9757cf 100644 --- a/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs +++ b/tinycloud-node-server/tests/renewal_lease_v1_vectors.rs @@ -4,6 +4,7 @@ //! are public test data only; no runtime renewal code is exercised here. use base64::{encode_config, URL_SAFE}; +use ed25519_dalek::{Signer, SigningKey, Verifier}; use serde_json::{json, Value}; use std::collections::BTreeMap; use tinycloud_auth::{ @@ -136,6 +137,91 @@ fn renewal_lease_v1_pins_real_owner_signed_ucan_and_exact_original_cid() { ); let lease_cid = canonical_ucan_cid(&lease_token); + let TinyCloudDelegation::Ucan(original_ucan) = + TinyCloudDelegation::decode(&original_token).unwrap().0 + else { + panic!("original vector is a UCAN"); + }; + let original_payload = original_ucan.payload(); + let original_not_before = original_payload + .not_before + .expect("owner-signed original nbf") + .as_seconds(); + let original_expiration = original_payload.expiration.as_seconds(); + let period_seconds = original_expiration - original_not_before; + assert_eq!(period_seconds, 2_592_000.0); + + let TinyCloudDelegation::Ucan(lease_ucan) = + TinyCloudDelegation::decode(&lease_token).unwrap().0 + else { + panic!("lease vector is a UCAN"); + }; + let lease_payload = lease_ucan.payload(); + let lease_not_before = lease_payload + .not_before + .expect("owner-signed lease nbf") + .as_seconds(); + let lease_expiration = lease_payload.expiration.as_seconds(); + let lease_lifetime_seconds = lease_expiration - lease_not_before; + assert_eq!(lease_lifetime_seconds, 31_536_000.0); + + let first_stamp_now = original_expiration - 86_400.0; + let first_stamp_end = (first_stamp_now + period_seconds).min(lease_expiration); + assert_eq!( + first_stamp_end, + original_expiration + period_seconds - 86_400.0 + ); + let near_lease_end = lease_expiration - 86_400.0; + assert_eq!( + (near_lease_end + period_seconds).min(lease_expiration), + lease_expiration, + "the 365-day lease expiry remains an independent upper bound" + ); + let first_stamp_new_end = time::OffsetDateTime::from_unix_timestamp(first_stamp_end as i64) + .unwrap() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(); + + let node_did = node_secret.node_did(); + assert_ne!(node_did, renewer_did); + let first_stamp = json!({ + "domain": "tinycloud/node/renewal-stamp/v1", + "unsigned": { + "type": "TinyCloudRenewalStamp", + "version": 1, + "originalCid": original_cid, + "leaseCid": lease_cid, + "leaseGeneration": 1, + "previousStampCid": lease_cid, + "nodeDid": node_did, + "renewerEpoch": 1, + "newEnd": first_stamp_new_end, + }, + }); + let first_stamp_bytes = serde_ipld_dagcbor::to_vec(&first_stamp).unwrap(); + let stamp_signer = SigningKey::from_bytes(&renewer_seed); + let first_stamp_signature = stamp_signer.sign(&first_stamp_bytes); + stamp_signer + .verifying_key() + .verify(&first_stamp_bytes, &first_stamp_signature) + .unwrap(); + let first_stamp_cid = hash(&first_stamp_bytes).to_cid(0x55).to_string(); + let first_stamp_vector = json!({ + "stamp": first_stamp, + "canonicalDagCborHex": hex::encode(&first_stamp_bytes), + "stampCid": first_stamp_cid, + "signerDid": renewer_did, + "signerPublicKeyHex": hex::encode(stamp_signer.verifying_key().to_bytes()), + "signatureHex": hex::encode(first_stamp_signature.to_bytes()), + }); + assert_eq!( + first_stamp_vector["stamp"]["unsigned"]["leaseCid"], + lease_cid + ); + assert_eq!( + first_stamp_vector["stamp"]["unsigned"]["previousStampCid"], + lease_cid + ); let vector = json!({ "schema": "tinycloud/node/renewal-lease-vector/v1", "testOnlyOwnerSeedHex": hex::encode(OWNER_SEED), @@ -145,6 +231,9 @@ fn renewal_lease_v1_pins_real_owner_signed_ucan_and_exact_original_cid() { "issuedAt": ISSUED_AT as u64, "originalExpiration": ORIGINAL_EXPIRY as u64, "leaseExpiration": LEASE_EXPIRY as u64, + "renewalPeriodSource": "owner-signed original exp - nbf", + "renewalPeriodSeconds": period_seconds as u64, + "leaseLifetimeSeconds": lease_lifetime_seconds as u64, "ownerDid": owner_did, "holderDid": holder_did, "renewerDid": renewer_did, @@ -156,9 +245,26 @@ fn renewal_lease_v1_pins_real_owner_signed_ucan_and_exact_original_cid() { "renewResource": lease_resource, "leaseToken": lease_token, "leaseCid": lease_cid, + "firstStamp": first_stamp_vector, + "nodeIdentityDid": node_did, }); let fixture: Value = serde_json::from_str(include_str!("fixtures/renewal-lease-v1.json")) .expect("renewal lease fixture parses"); - assert_eq!(vector, fixture, "pin the complete signed UCAN vector"); + assert_eq!( + fixture["firstStamp"]["stamp"]["unsigned"]["leaseCid"], fixture["leaseCid"], + "the first stamp is rooted at the real signed lease CID" + ); + assert_eq!( + fixture["firstStamp"]["stamp"]["unsigned"]["previousStampCid"], fixture["leaseCid"], + "the first previous stamp is the real signed lease CID" + ); + assert_eq!( + fixture["renewalPeriodSeconds"], period_seconds as u64, + "renewal period comes from the owner's signed original exp - nbf" + ); + assert_eq!( + vector, fixture, + "pin the complete signed UCAN and stamp vectors" + ); } From 693d68e7928978e1100b927834592549d7ac7bbf Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 04:21:18 +0000 Subject: [PATCH 3/3] docs: pin renewal stop and enable contract --- docs/specs/stop-reenable-contract-v1.md | 270 ++++++++++++++++++ .../tests/fixtures/renewal-state-v1.json | 95 ++++++ 2 files changed, 365 insertions(+) create mode 100644 docs/specs/stop-reenable-contract-v1.md create mode 100644 tinycloud-node-server/tests/fixtures/renewal-state-v1.json diff --git a/docs/specs/stop-reenable-contract-v1.md b/docs/specs/stop-reenable-contract-v1.md new file mode 100644 index 00000000..d9d3ffca --- /dev/null +++ b/docs/specs/stop-reenable-contract-v1.md @@ -0,0 +1,270 @@ +# Stop and re-enable renewal: proposed control contract v1 + +Status: DOC-ONLY PROPOSAL, revised for Bobbie323fd442 (six required changes), for delta review then Naomi freeze. Not approved wire vocabulary. Requested by Naomi3411713e. No runtime or frozen-fixture changes are authorized by this document. + +The current node runtime slice ships **renew + existing revoke/status only**. Stop/re-enable are deferred to the next slice. Existing #545/#546 Renew leases grant only `tinycloud.delegation/renew`; this proposal does not modify those bytes, CIDs, capabilities or the stamp format. `design.md` R5 is target behavior for that later slice, not a claim that stop is currently exposed. Never substitute revoke when a caller asks to stop: revoking the lease invalidates renewal-added time and falls back to the original signed expiry; revoking the original ends its access/descendants. Stop preserves admitted stamps. This proposal adds no “stop and end now” mode. + +## Recommended route, actions and target + +Recommend ONE new thin endpoint in the **existing delegation-control route family**, `POST /delegation/renewal/state`, using the strict owner-control validation template in `tinycloud-core/src/encryption_network/delete_auth.rs:301–356`: explicit audience, proofless principal, one capability, lifetime, nonce and unknown-caveat rejection. Existing `/delegation/status` supplies only route-family/exact-CID precedent; its signature/time-only verifier is NOT a sufficient auth template (it lacks audience/replay checks). This endpoint does not exist today. Reuse control authorization/replay/transaction code; do not route these owner operations through generic data `/invoke`, and do not overload `/revoke` with a stop mode. + +| Desired transition | Exact proposed ability | Exact signed resource | +|---|---|---| +| active → stopped | `tinycloud.delegation/stop-renewal` | `urn:cid:` | +| stopped → active | `tinycloud.delegation/enable-renewal` | `urn:cid:` | + +`enable` means D13 re-enable of a lease previously stopped through this contract. Registration always starts an owner-authorized renewable lease at `state=active, generation=1`; there is no registered-stopped initial mode in v1. Turning renewal on later for an originally nonrenewing grant requires explicit owner-signed lease registration while that original is live, then active/gen1. It never creates/extends a lease or changes its holder, period or abilities. The target is the **lease**, not the original, so multiple historical leases cannot be confused. New lease authorization/registration remains separate. The server resolves the lease's original from stored signed artifacts; no client-supplied original/holder/scope is trusted. + +Alternative considered: two `/invoke` action handlers on the original CID. Rejected for this slice: expands the generic invocation path and leaves lease selection ambiguous. Existing control-family route + exact lease target is the narrowest extension. Final route/action strings and the numeric caveat below are one-way contract choices for Naomi to freeze after review. + +## Lease cardinality and initial state — recommended freeze + +Require **at most ONE unrevoked, unexpired lease per (canonical original CID, renewer identity epoch)**. A stopped lease counts against this limit. Enforce at registration under the SAME original-CID advisory lock: a repeated registration of the identical lease is idempotent and never resets state/generation; a different live lease conflicts. Do not create lease B to bypass stopped lease A. This closes the multi-lease stop bypass rather than silently stopping just one of two active leases. Different historical expired/revoked leases remain auditable and cannot be re-enabled. A new separately authorized lease must still pass original-live/D15 checks after revoked/expired stamps are excluded. + +If the preceding runtime admits multiple live leases, this cardinality must be enforced/reconciled before the stop endpoint is advertised: return owner-only409 `ambiguous_renewal_lease` for an ambiguous original, never choose one silently or auto-revoke another. Existing affected rows need explicit owner resolution; this doc authorizes no migration or new user flow. Alternative for Naomi: define stop as original-wide across all leases. Recommendation is the ≤1 rule; no all-lease implicit mutation. + +Initial state is **active/gen1** only after explicit owner-authorized registration. Nonrenewing grants have no lease row. D13 re-enable mutates an existing live stopped row, never re-registers to reset its generation. + +## Owner authentication and signed request + +`Authorization` is the existing verifiable signed invocation format accepted by the node (including compact Ed25519 UCAN where applicable), with normal signature, audience, nbf/expiry and replay validation. No new signature scheme. The issuer must resolve to the **original delegation's delegator**, using canonical principal matching (`ControlAction::party_may_act` issuing-side rule). “Owner” here means that authenticated issuer, not arbitrary space owner, current holder or renewer. Under the original-CID lock ALSO require the verified issuer equals the lease’s OWN signed issuer, defending against a misregistered lease. In v1 require direct issuer authorization and `prf: []`; no delegated stop/enable management, space/network fallback, cookie-only authorization or unsigned profile metadata. + +For stop/enable: exactly one resource, one ability, and a caveat LIST of exactly ONE object with exactly ONE key (`expectedGeneration`). Check the raw signed shape before normalization: reject `[{},{}]`, `[{"expectedGeneration":1},{}]`, `[{}]`, objects with extra keys or collapsed/array-index keys (`"0"`, `"1"`). Read uses a distinct explicitly empty caveat profile below. Mutation example: + +```json +{ + "urn:cid:": { + "tinycloud.delegation/stop-renewal": [ { "expectedGeneration": 1 } ] + } +} +``` + +For enable, use the enable ability and current expected generation. `expectedGeneration` is a nonnegative integer representable exactly in JSON (≤2^53−1); reject floats, negative/overflow values and unknown caveat keys. The control parser MUST validate this signed caveat; it must not drop it as a generic unknown restriction. Lease generation cannot wrap. + +Body (strict keys and types): + +```json +{"version":1,"leaseCid":"","expectedGeneration":1} +``` + +Signed exact resource must equal `urn:cid:` + body lease CID, and signed expected generation must equal body generation. The CAS operand is ALWAYS taken from that SIGNED caveat, never the body; body generation is only an equality check. The action is derived ONLY from the single signed ability, never a body `state`, query, HTTP method override or generic resource interpretation. The body is not a separate signature envelope: all mutable selection is bound by those signed checks. Body version must be exactly1; future versions cannot be interpreted as v1. Reject URI paths, fragments, queries, wildcards, Space/Network resources, duplicate/multiple abilities and a target whose CID is not the stored canonical lease CID. This route requires canonical CID strings, not alias guessing. + +Require invocation audience = current stable node DID; node time only, fresh nonce and current invocation lifetime ceiling (the existing compact profile is ≤60s). No grace or timestamp-only replay protection. Replay identity is **(canonical authenticated invoker, nonce)** through `invoker_nonce_reservation`, NEVER invocation content_hash (re-signing/changing exp must not reuse a nonce). Insert that reservation atomically with the successful state mutation; the state-read route also reserves its nonce in its protected transaction. Repeated successful nonce is refused; recovery reads status rather than performing a second state change. Unknown/transient transaction outcome must not be labelled success. + +A Renew lease is NEVER a management proof. The renewer DID remains barred from generic `/delegate` and `/invoke`, and cannot call this owner control route either. The holder cannot stop/re-enable merely because it holds the original grant. An owner changing a setting requires a newly signed explicit request; nothing automatically re-enables after a stop. + +## Conditional mutation and D13/D18 semantics + +Use the SAME `pg_advisory_xact_lock` keyed by canonical original CID as renew/registration; SQLite uses its reserved write transaction. Begin one write transaction; acquire the canonical-original lock before authoritative reads. Never rely on in-process locks or mix a separate `FOR UPDATE` synchronization regime. + +Under that lock re-read canonical original/lease, verified original delegator, lease chain + revocations, original/ancestor current effective ends, current renewer identity epoch, lease end, current state and generation. Check original delegator AND lease signed issuer before producing any lease existence/state-specific response; the owner must still be authorized at commit. Not-found and not-owner have the SAME403 code/message, with no state/generation/leaseEnd details. No404/409 detail is returned to a non-owner. Reject revoked original/ancestor/lease, identity mismatch, an expired lease, or a lapsed original. State changes cannot restore expired or revoked authority (D15/D18). + +- **Stop:** conditional `UPDATE ... SET state='stopped', generation=generation+1 WHERE original=$O AND lease=$L AND state='active' AND generation=$expected`. Exactly one row or409. It creates NO stamp, changes NO signed artifact/end/period, and does NOT mark anything revoked. Admitted stamps continue to their previously admitted end under lease, ancestor, revocation and identity constraints. +- **Enable/re-enable:** only the original delegator can conditionally change `stopped → active`, same generation increment, while the original is effectively LIVE and the lease/chain unrevoked, lease end future, epoch current. It writes NO stamp, moves NO end, and continues the existing lease-CID-rooted stamp chain. A revoked lease never returns. A new signed lease is a separate operation; this control cannot silently create a successor or extend90d to1y. +- **Same-state requests:** return409 `renewal_state_conflict`, not a mutation or new stamp. Clients may query status to see the actual state; no hidden retry with a new generation. Nonce and CAS conflicts cannot increment generation twice. + +Lease generation is mutation/CAS history only, NOT a use-time equality predicate. Stop/re-enable increments must never invalidate old stamps. Renewer **identity epoch** is separate and remains a use-time validity check. Explicit revocation or true node identity change still invalidates affected stamps. Revocation joins are authoritative at use; even if a competing revoke commits after a successful enable, the state cannot override that revoke. + +Commit state update and invocation-replay insertion in the SAME transaction. Rollback both on failure. Audit rows use existing Status read authorization (issuer/delegatee as applicable, or space owner), not extra data access. No new reader may see another holder's lease history. A successful receipt reports only confirmed state: + +```json +{"version":1,"originalCid":"","leaseCid":"","state":"stopped","generation":2,"leaseEnd":""} +``` + +Enable returns active/generation3 for the example sequence. The owner-authorized state read defined below supplies the CAS generation (the current `/delegation/status` response DOES NOT contain it). It must distinguish stopped/active and signed lease end; no success response promises an additional renewal period. If the older node lacks this endpoint, SDK returns unsupported, with no fallback to revoke or silent local-only preference. + +## Owner-authorized lease-state read (required for CAS recovery) + +Use the SAME proposed `POST /delegation/renewal/state` endpoint, with exactly one signed `tinycloud.delegation/status` capability on `urn:cid:`, `prf: []`, and exactly one EMPTY caveat object `[{}]`. Do not accept a generation caveat/extra keys on this read. Body is exactly `{"version":1,"leaseCid":""}`. This read profile is distinct from mutation’s mandatory one-key caveat; the decoder must not infer operation from normalized empty objects. + +Same strict signature/audience/lifetime/nonce and ORIGINAL-delegator == LEASE-issuer checks apply. A generic `/delegation/status` grant or lease recipient’s existing Status rights DO NOT bypass this endpoint’s owner-only rule. No renewer/holder/delegated proof may fetch state here. In a transaction, resolve authority before returning existence/state and reserve `(invoker, nonce)`; same403 for unknown lease and wrong owner. Do not return cached generation. Authorized result: + +```json +{"version":1,"originalCid":"","leaseCid":"","state":"stopped","generation":2,"leaseEnd":""} +``` + +`state`, `generation`, `leaseEnd` are required and current. A successful stop/enable receipt contains the same fields, including leaseEnd, after its confirmed commit. Owner reads may inspect stopped/revoked/expired lease history without mutating it. Existing status/revocation APIs supply revocation/expiry reasons where needed; no additional flags or fabricated state change are introduced by this read. Owner can safely learn generation before signing a new CAS. Unknown transaction outcome → state read with a NEW nonce; never reuse successful mutation nonce or blindly retry a mutation with a guessed generation. + +## Error contract (proposed v1) + +Responses contain `{version:1,code:,message:}`; owner text is reviewed separately before implementation. Do not echo request tokens, raw node errors, URLs or signed artifacts. All refusal cases leave state/stamps unchanged; unauthenticated callers do not receive grant details. + +| HTTP | Code | Condition | +|---|---|---| +|400|`invalid_request`|Bad CID/schema/version, multiple actions/resources, unknown caveat, invalid integer or malformed nonce.| +|401|`auth_required`|Missing/bad signature, expired/not-yet-valid invocation or wrong node audience.| +|403|`unauthorized_control`|Not-found OR not-owner (same static message/no details); signer must match BOTH original delegator and signed lease issuer. Also renewer/holder/proof/wrong binding/wildcard/Space/Network refusal.| +|409|`renewal_state_conflict`|OWNER-ONLY: wrong state, stale generation, overflow or conditional update not exactly one row.| +|409|`ambiguous_renewal_lease`|OWNER-ONLY: multiple unexpired/unrevoked leases for the original/epoch; no partial stop or automatic resolution.| +|409|`invocation_replay`|Successful nonce already committed; never treat it as a new state change.| +|409|`grant_expired`|Original or any required ancestor no longer effectively live.| +|409|`lease_expired`|Lease reached its signed end; re-enable cannot extend it.| +|409|`grant_revoked` / `lease_revoked`|Original/ancestor or lease chain revoked; status-only outcome, no resurrection.| +|409|`renewer_identity_changed`|Lease belongs to another renewer identity epoch.| +|503|`renewal_state_unavailable`|Database/revocation/registry lookup/commit outcome unavailable; fail closed, do not turn it into404/403 or automatic re-request.| + +## Synthetic signed vectors (doc-only; not yet node conformance evidence) + +Use #546 fixture `bd2f95ca`'s public synthetic owner seed `11`×32, owner DID `did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S`, original CID `bafkr4ibdtwxhoauccwuknmf7cd5m3o74rwek7al4wgs33ecoygubpmour4`, lease CID `bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq`, node DID `did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7`. These are PUBLIC TEST DATA, never live keys. + +Known-answer authorizations below were computed locally with existing SDK `signCompactUcanAuthorization` and independently checked by the Ed25519 verifier. Signed bytes are ASCII `.`, existing UCAN0.10.0/EdDSA profile; no new signing format or stamp signature domain is introduced. They are proposals to freeze, not executed against a stop endpoint (none exists). All three use nbf1701000000/exp1701000060, fct[{}], no proofs; verifier clock1701000001. Fixture lease signed end1731536000, original signed end1702592000. Preload original/lease/chain as valid, epoch1, and existing first stamp from #546. Tests after original signed expiry must use effective expiry, not discard it locally. + +### V1: stop active generation1 → stopped generation2 + +Request body: + +```json +{"version":1,"leaseCid":"bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq","expectedGeneration":1} +``` + +Signed claims: + +```json +{ + "att": { + "urn:cid:bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq": { + "tinycloud.delegation/stop-renewal": [ + { + "expectedGeneration": 1 + } + ] + } + }, + "aud": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "exp": 1701000060, + "fct": [ + {} + ], + "iss": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S#z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "nbf": 1701000000, + "nnc": "urn:uuid:00000000-0000-4000-8000-000000000001", + "prf": [] +} +``` + +Authorization (one literal line): + +```text +eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9zdG9wLXJlbmV3YWwiOlt7ImV4cGVjdGVkR2VuZXJhdGlvbiI6MX1dfX0sImF1ZCI6ImRpZDprZXk6ejZNa3BYdmVVeWpwQ0RlNlA4M3JOWXFBWm1vVGM5cW9wSjZHQnVSUVJvdHozekY3IiwiZXhwIjoxNzAxMDAwMDYwLCJmY3QiOlt7fV0sImlzcyI6ImRpZDprZXk6ejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTI3o2TWt0VUx1ZFR0QXNBaFJlZ1lQaVo2NjMxUlYzdml2MTJxZDRHUUY4ejF4QjIyUyIsIm5iZiI6MTcwMTAwMDAwMCwibm5jIjoidXJuOnV1aWQ6MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicHJmIjpbXX0.a7ep_-r1c0emPJJO0p1zQBu9ZGDAbTL1VU_P3GVINGQlwXwIQDkcFm1MWYHGUKGk6qPwGxVEu75FQlGdF5VYDQ +``` + +Detached signature hex: `6bb7a9ffeaf57347a63c924ed29d73401bbd6460c06d32f5554fcfdc6548346425c17c0840391c166d4c5981c650a1a4eaa3f01b1544bbbe4542519d1795580d`. Signing-input SHA-256: `314cf79709ef6bc8f75da4186e0bc69660e4492c848db949294f0dc99ce5e3ce`. + +### V2: enable stopped generation2 → active generation3 + +Request body: + +```json +{"version":1,"leaseCid":"bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq","expectedGeneration":2} +``` + +Signed claims: + +```json +{ + "att": { + "urn:cid:bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq": { + "tinycloud.delegation/enable-renewal": [ + { + "expectedGeneration": 2 + } + ] + } + }, + "aud": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "exp": 1701000060, + "fct": [ + {} + ], + "iss": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S#z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "nbf": 1701000000, + "nnc": "urn:uuid:00000000-0000-4000-8000-000000000002", + "prf": [] +} +``` + +Authorization (one literal line): + +```text +eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9lbmFibGUtcmVuZXdhbCI6W3siZXhwZWN0ZWRHZW5lcmF0aW9uIjoyfV19fSwiYXVkIjoiZGlkOmtleTp6Nk1rcFh2ZVV5anBDRGU2UDgzck5ZcUFabW9UYzlxb3BKNkdCdVJRUm90ejN6RjciLCJleHAiOjE3MDEwMDAwNjAsImZjdCI6W3t9XSwiaXNzIjoiZGlkOmtleTp6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMjejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTIiwibmJmIjoxNzAxMDAwMDAwLCJubmMiOiJ1cm46dXVpZDowMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJwcmYiOltdfQ.RJwsIMaEuh9yMRyKvMhHj06BFANolxxan0a1qLEHAcLEXXTnxtXicyEiNgTp-AAS6m8Q4sX_kTN4rEDl2k1-Dg +``` + +Detached signature hex: `449c2c20c684ba1f72311c8abcc8478f4e81140368971c5a9f46b5a8b10701c2c45d74e7c6d5e27321223604e9f80012ea6f10e2c5ff913378ac40e5da4d7e0e`. Signing-input SHA-256: `acb42ead8923fd435747920f806c7a5cb0b9b81b17c0a4c055a968cbb9654be7`. + +### V3-read: learn the current CAS generation + +The existing node `/delegation/status` shape is insufficient. The third signed sample below binds an owner-only state read to the SAME actual lease CID. It uses an independent nonce; afterV1 it must return state=stopped, generation2 and signed leaseEnd, without changing stamps/state. + +Body: + +```json +{"version":1,"leaseCid":"bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq"} +``` + +Claims: + +```json +{ + "att": { + "urn:cid:bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq": { + "tinycloud.delegation/status": [ + {} + ] + } + }, + "aud": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "exp": 1701000060, + "fct": [ + {} + ], + "iss": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S#z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "nbf": 1701000000, + "nnc": "urn:uuid:00000000-0000-4000-8000-000000000003", + "prf": [] +} +``` + +Authorization: + +```text +eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9zdGF0dXMiOlt7fV19fSwiYXVkIjoiZGlkOmtleTp6Nk1rcFh2ZVV5anBDRGU2UDgzck5ZcUFabW9UYzlxb3BKNkdCdVJRUm90ejN6RjciLCJleHAiOjE3MDEwMDAwNjAsImZjdCI6W3t9XSwiaXNzIjoiZGlkOmtleTp6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMjejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTIiwibmJmIjoxNzAxMDAwMDAwLCJubmMiOiJ1cm46dXVpZDowMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJwcmYiOltdfQ.hzLmQd4IObMUOwNXAfKTWnxhfeZ1KM2-ekzW7tyU6ruoPdyftqhjjz3QDblzgjzv8-LD-OkXJmXJI0ceH-b8AQ +``` + +Signature hex: `8732e641de0839b3143b035701f2935a7c617de67528cdbe7a4cd6eedc94eabba83ddc9fb6a8638f3dd00db973823ceff3e2c3f8e9172665c923471e1fe6fc01`; signing-input SHA-256: `d55df0c5258a6d3a96c112d635faf03db8675864bf991b5d16efa4ab5dc8caa3`. + +### Required conformance and race vectors + +Each mutation below is independently re-signed where stated, so authorization checks—not just bad-signature detection—must fail. Keep positive controls. New fixture/test code is for the FOLLOWING slice only, after freeze. + +| Vector | Input/state | Expected result | +|---|---|---| +|V1|Above owner stop, active/gen1, live/unrevoked/current epoch|200 stopped/gen2; no stamp or end changed.| +|V2|Above owner enable afterV1, stopped/gen2, same live lease|200 active/gen3; no stamp/end changed; same stamp chain.| +|V3|Use #546's gen1 stamp after stop/gen2 and after enable/gen3|Still valid until admitted end, unless original/lease/ancestor revoked or epoch changed.| +|V4|Body generation2 with V1 signed generation1 unchanged|403; no mutation/replay insertion.| +|V5|Fresh owner signature/gen1 after successfulV1 state/gen2|409 generation/state conflict; no second increment.| +|V6|V1 exact token reused after success|409 replay; no second increment.| +|V7|Holder seed22×32 signs otherwise valid stop/enable|403; holder is not original delegator.| +|V8|Derived renewer key signs same; or uses Renew lease as proof|403; renewal right is not management right.| +|V9|Owner signs stop resource naming ORIGINAL CID instead of LEASE CID, body unchanged|403 target mismatch.| +|V10|Re-sign wildcard, Space/Network target, extra resource/ability/caveat, or add proof|400/403 according to error table; never broaden via space-owner branch.| +|V11|Clock equals original's EFFECTIVE end, even lease still live|409 grant_expired; enable never revives.| +|V12|Stopped lease but revoked original, ancestor or lease chain|409 revoked; no re-enable/stamp. A fresh lease requires separate authorization.| +|V13|Clock equals lease end; or true node identity changes|409 lease_expired/renewer_identity_changed; no extension.| +|V14|Signature bit flips, audience changed, expired invocation, nbf in future|401; no state changes.| +|V15|Corrupt/unavailable registry or unknown commit outcome|503; no success claim, no automatic revoke fallback.| +|V16|Two processes: same generation, two valid stop nonces|Exactly one commit/gen increment; loser409.| +|V17|Two processes: stop ∥ renew (both use same canonical-original lock)|Renew-before-stop admitted stamp survives; stop-before-renew rejects issuance. No new stamp after winning stop.| +|V18|Enable ∥ revoke original/lease/ancestor|Even if enable receipt wins first, authoritative use-time join rejects all revoked access. Never treat enable as undoing revoke.| +|V19|Stop → enable ∥ a renew that READ STATE BEFORE acquiring the lock (renew signs NO generation)|Renew must re-read lease state/generation after the shared lock. A stopped winner rejects issue; active current state permits only re-evaluated issuance. Existing admitted old stamp remains valid.| +|V20|Restart client after stop/enable, cached expiry claims access|Re-fetch node status/effective expiry; cache is display hint only.| + +Additional required vectors: owner-only read returns exact current `{state,generation,leaseEnd}`; unknown lease and known foreign lease return identical403 shape/no state; a deliberately misregistered lease whose issuer differs from the original rejects both read/mutation; duplicate live lease registration conflicts including while first is stopped; re-registering same lease cannot reset state/generation; unknown caveat-array shape/multiple entries/floats reject; same invoker+nonce with newly signed exp/body still409; read nonce cannot be reused as mutation; initial registered state active/gen1 and no registered-stopped mode. + +Pin real server success/refusal and multi-process outcomes in implementation tests, not only signed-request fixtures. Verify frozen #545/#546 fixtures remain byte-identical. D13 opt-in-after-stop is explicit owner authorization on a still-live unrevoked lease; no hidden settings-page flow or new approval prompt is imposed by this protocol. + +## Review/freeze boundaries + +Bobbie: verify direct owner authority vs proof delegation, signed generation/body binding, lease-target choice, nonce/atomic mutation, use-time lease revocation join, stop≠revoke, re-enable races, existing signature-suite support and error classification. Naomi: freeze action strings, route/resource, caveat/receipt AND owner-state-read schemas, ≤1-live-lease cardinality, initial active/gen1 and signed vectors after that review. No generic `ControlAction::Revoke/Status` permission is widened; those routes keep their behavior. No published docs/UI wording or production operation is authorized by this internal proposal. + +Primary local references: node d63d4c44 `tinycloud-core/src/encryption_network/delete_auth.rs:301–356` strict owner-control validator template (borrow checks, not key-admin-specific facts); `tinycloud-node-server/src/routes/mod.rs:630` route-family/exact-CID precedent ONLY, not sufficient authentication; `tinycloud-core/src/models/revocation.rs:375-392` ControlAction/party rules and `:480-521` direct-CID vs broader authorization; canonical `design.md` R1–R7; merged expiry spec D13/D15/D18; frozen real lease/stamp #546 bd2f95ca. Actual R5 runtime code is explicitly deferred. diff --git a/tinycloud-node-server/tests/fixtures/renewal-state-v1.json b/tinycloud-node-server/tests/fixtures/renewal-state-v1.json new file mode 100644 index 00000000..68f82122 --- /dev/null +++ b/tinycloud-node-server/tests/fixtures/renewal-state-v1.json @@ -0,0 +1,95 @@ +[ + { + "state": "stopped", + "action": "tinycloud.delegation/stop-renewal", + "body": { + "version": 1, + "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq", + "expectedGeneration": 1 + }, + "claims": { + "att": { + "urn:cid:bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq": { + "tinycloud.delegation/stop-renewal": [ + { + "expectedGeneration": 1 + } + ] + } + }, + "aud": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "exp": 1701000060, + "fct": [ + {} + ], + "iss": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S#z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "nbf": 1701000000, + "nnc": "urn:uuid:00000000-0000-4000-8000-000000000001", + "prf": [] + }, + "authorization": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9zdG9wLXJlbmV3YWwiOlt7ImV4cGVjdGVkR2VuZXJhdGlvbiI6MX1dfX0sImF1ZCI6ImRpZDprZXk6ejZNa3BYdmVVeWpwQ0RlNlA4M3JOWXFBWm1vVGM5cW9wSjZHQnVSUVJvdHozekY3IiwiZXhwIjoxNzAxMDAwMDYwLCJmY3QiOlt7fV0sImlzcyI6ImRpZDprZXk6ejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTI3o2TWt0VUx1ZFR0QXNBaFJlZ1lQaVo2NjMxUlYzdml2MTJxZDRHUUY4ejF4QjIyUyIsIm5iZiI6MTcwMTAwMDAwMCwibm5jIjoidXJuOnV1aWQ6MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicHJmIjpbXX0.a7ep_-r1c0emPJJO0p1zQBu9ZGDAbTL1VU_P3GVINGQlwXwIQDkcFm1MWYHGUKGk6qPwGxVEu75FQlGdF5VYDQ", + "signedAscii": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9zdG9wLXJlbmV3YWwiOlt7ImV4cGVjdGVkR2VuZXJhdGlvbiI6MX1dfX0sImF1ZCI6ImRpZDprZXk6ejZNa3BYdmVVeWpwQ0RlNlA4M3JOWXFBWm1vVGM5cW9wSjZHQnVSUVJvdHozekY3IiwiZXhwIjoxNzAxMDAwMDYwLCJmY3QiOlt7fV0sImlzcyI6ImRpZDprZXk6ejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTI3o2TWt0VUx1ZFR0QXNBaFJlZ1lQaVo2NjMxUlYzdml2MTJxZDRHUUY4ejF4QjIyUyIsIm5iZiI6MTcwMTAwMDAwMCwibm5jIjoidXJuOnV1aWQ6MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicHJmIjpbXX0", + "signatureHex": "6bb7a9ffeaf57347a63c924ed29d73401bbd6460c06d32f5554fcfdc6548346425c17c0840391c166d4c5981c650a1a4eaa3f01b1544bbbe4542519d1795580d" + }, + { + "state": "active", + "action": "tinycloud.delegation/enable-renewal", + "body": { + "version": 1, + "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq", + "expectedGeneration": 2 + }, + "claims": { + "att": { + "urn:cid:bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq": { + "tinycloud.delegation/enable-renewal": [ + { + "expectedGeneration": 2 + } + ] + } + }, + "aud": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "exp": 1701000060, + "fct": [ + {} + ], + "iss": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S#z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "nbf": 1701000000, + "nnc": "urn:uuid:00000000-0000-4000-8000-000000000002", + "prf": [] + }, + "authorization": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9lbmFibGUtcmVuZXdhbCI6W3siZXhwZWN0ZWRHZW5lcmF0aW9uIjoyfV19fSwiYXVkIjoiZGlkOmtleTp6Nk1rcFh2ZVV5anBDRGU2UDgzck5ZcUFabW9UYzlxb3BKNkdCdVJRUm90ejN6RjciLCJleHAiOjE3MDEwMDAwNjAsImZjdCI6W3t9XSwiaXNzIjoiZGlkOmtleTp6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMjejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTIiwibmJmIjoxNzAxMDAwMDAwLCJubmMiOiJ1cm46dXVpZDowMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJwcmYiOltdfQ.RJwsIMaEuh9yMRyKvMhHj06BFANolxxan0a1qLEHAcLEXXTnxtXicyEiNgTp-AAS6m8Q4sX_kTN4rEDl2k1-Dg", + "signedAscii": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9lbmFibGUtcmVuZXdhbCI6W3siZXhwZWN0ZWRHZW5lcmF0aW9uIjoyfV19fSwiYXVkIjoiZGlkOmtleTp6Nk1rcFh2ZVV5anBDRGU2UDgzck5ZcUFabW9UYzlxb3BKNkdCdVJRUm90ejN6RjciLCJleHAiOjE3MDEwMDAwNjAsImZjdCI6W3t9XSwiaXNzIjoiZGlkOmtleTp6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMjejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTIiwibmJmIjoxNzAxMDAwMDAwLCJubmMiOiJ1cm46dXVpZDowMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJwcmYiOltdfQ", + "signatureHex": "449c2c20c684ba1f72311c8abcc8478f4e81140368971c5a9f46b5a8b10701c2c45d74e7c6d5e27321223604e9f80012ea6f10e2c5ff913378ac40e5da4d7e0e" + }, + { + "state": "read", + "action": "tinycloud.delegation/status", + "body": { + "version": 1, + "leaseCid": "bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq" + }, + "claims": { + "att": { + "urn:cid:bafkr4ignh3iowa5gysgegtuosa62pdkw6mpvrhsr3umjey5x2zo4fvjgqq": { + "tinycloud.delegation/status": [ + {} + ] + } + }, + "aud": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7", + "exp": 1701000060, + "fct": [ + {} + ], + "iss": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S#z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S", + "nbf": 1701000000, + "nnc": "urn:uuid:00000000-0000-4000-8000-000000000003", + "prf": [] + }, + "authorization": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9zdGF0dXMiOlt7fV19fSwiYXVkIjoiZGlkOmtleTp6Nk1rcFh2ZVV5anBDRGU2UDgzck5ZcUFabW9UYzlxb3BKNkdCdVJRUm90ejN6RjciLCJleHAiOjE3MDEwMDAwNjAsImZjdCI6W3t9XSwiaXNzIjoiZGlkOmtleTp6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMjejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTIiwibmJmIjoxNzAxMDAwMDAwLCJubmMiOiJ1cm46dXVpZDowMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJwcmYiOltdfQ.hzLmQd4IObMUOwNXAfKTWnxhfeZ1KM2-ekzW7tyU6ruoPdyftqhjjz3QDblzgjzv8-LD-OkXJmXJI0ceH-b8AQ", + "signedAscii": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMEVxeU1uUXJ0S3M2RTJpOVJoWGs1dEFpU3JjYUFXdXZoU0NqTXNsM2h6YyJ9LCJ0eXAiOiJKV1QiLCJ1Y3YiOiIwLjEwLjAifQ.eyJhdHQiOnsidXJuOmNpZDpiYWZrcjRpZ25oM2lvd2E1Z3lzZ2VndHVvc2E2MnBka3c2bXB2cmhzcjN1bWpleTV4MnpvNGZ2amdxcSI6eyJ0aW55Y2xvdWQuZGVsZWdhdGlvbi9zdGF0dXMiOlt7fV19fSwiYXVkIjoiZGlkOmtleTp6Nk1rcFh2ZVV5anBDRGU2UDgzck5ZcUFabW9UYzlxb3BKNkdCdVJRUm90ejN6RjciLCJleHAiOjE3MDEwMDAwNjAsImZjdCI6W3t9XSwiaXNzIjoiZGlkOmtleTp6Nk1rdFVMdWRUdEFzQWhSZWdZUGlaNjYzMVJWM3ZpdjEycWQ0R1FGOHoxeEIyMlMjejZNa3RVTHVkVHRBc0FoUmVnWVBpWjY2MzFSVjN2aXYxMnFkNEdRRjh6MXhCMjJTIiwibmJmIjoxNzAxMDAwMDAwLCJubmMiOiJ1cm46dXVpZDowMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJwcmYiOltdfQ", + "signatureHex": "8732e641de0839b3143b035701f2935a7c617de67528cdbe7a4cd6eedc94eabba83ddc9fb6a8638f3dd00db973823ceff3e2c3f8e9172665c923471e1fe6fc01" + } +] \ No newline at end of file