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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/specs/renewal-stamp-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Renewal stamp v1 wire contract

This file records the exact v1 signed-stamp representation pinned by fixture commit `377c0cd9269c982bb2e71b60a51bc77367718031`, based on main `d63d4c44ef274d24f67a7f889379226e4769e261`. The selected architecture is the Bobbie-reviewed design `1635acfc1c4d9f965a58101eb5f8a0dcb45f39ef0996225021597157ac757a53`. This specification freezes the wire contract only; it does not implement runtime renewal behavior.

## Renewal lease authority

A renewal lease is an existing owner-authorized UCAN/CACAO delegation. This specification does **not** define a new lease serialization, lease signature format, or a real signed lease vector. Its exact control capability is:

- Ability: `tinycloud.delegation/renew`
- Resource: `urn:cid:<canonical original delegation CID>`

The CIDs named `originalCid` and `leaseCid` in the known-answer fixture are generated from synthetic DAG-CBOR placeholder artifacts. They test stamp encoding and the non-null root link only; they are not signed UCAN/CACAO delegation records and do not freeze the lease's actual bytes.

## Signed stamp bytes

The signed byte sequence is the deterministic DAG-CBOR encoding of exactly this two-entry map:

```text
{
"domain": "tinycloud/node/renewal-stamp/v1",
"unsigned": {
"type": "TinyCloudRenewalStamp",
"version": 1,
"originalCid": <canonical CID string>,
"leaseCid": <canonical CID string>,
"leaseGeneration": <unsigned integer>,
"previousStampCid": <canonical CID string>,
"nodeDid": <DID string>,
"renewerEpoch": <unsigned integer>,
"newEnd": <RFC3339 UTC string>
}
}
```

`previousStampCid` is non-null. The first stamp sets it to that lease's CID. `leaseGeneration` records the issuance generation; it is not a use-time equality check. `renewerEpoch` is the current renewer identity epoch. `newEnd` is serialized as a UTC RFC3339 string. The domain value has no NUL terminator; there is no additional domain prefix outside the encoded map.

Encode with the repository's deterministic `serde_ipld_dagcbor` representation. The detached Ed25519 signature is over the exact encoded bytes above; it is not embedded in those bytes. The stamp CID is CIDv1 with raw codec `0x55` and Blake3-256 over those exact bytes; the detached signature is excluded from the CID input.

Derive the renewer signing seed from the node's `StaticSecret` with the exact context bytes `tinycloud/node/renewer/v1`, using `StaticSecret::derive_key` (Blake3-256 of the static secret followed by the context). This is distinct from the node's identity-key context and storage-key generation.

## Pinned vector

The synthetic fixture is `tinycloud-node-server/tests/fixtures/renewal-stamp-v1.json`; its assertions are in `tinycloud-node-server/tests/renewal_stamp_v1_vectors.rs`. The fixture's fixed master-secret bytes (`42` repeated 32 times) are public test data only.

- Original placeholder CID: `bafkr4ifaiwrq52xkndrm7jbwqvywq3tkilydyxrj24oofckit6ink7f6hy`
- Lease placeholder CID and first `previousStampCid`: `bafkr4iaiwsgu3x5bvuigtms3hrlhrwpshbv5lxar5oj7uhh5dfg2sbcl4q`
- Stamp CID: `bafkr4ibtywvyzwld56wljgytygpis2dddualt6h6b343e52rzmsegyw5nq`
- Signer public key (hex): `49fd44f3fe8930b0e89f61c51333195578deacaf22105a78f647d8c05afc7c5d`
- Detached signature (hex): `34caf6cc60615a4c0d4f4e934c0263dde1c17fdd5becfa0f693337987b0250f0b430d7fe4518db41a882cc26ee979603736b883798fb83bbc95fb61af077020d`

Exact canonical DAG-CBOR bytes (hex):

```text
a266646f6d61696e781f74696e79636c6f75642f6e6f64652f72656e6577616c2d7374616d702f763168756e7369676e6564a964747970657554696e79436c6f756452656e6577616c5374616d70666e6577456e6474323033302d30312d30325430333a30343a30355a676e6f646544696478386469643a6b65793a7a364d6b7058766555796a7043446536503833724e5971415a6d6f546339716f704a364742755251526f747a337a46376776657273696f6e01686c65617365436964783b6261666b7234696169777367753378356276756967746d733368726c6872777073686276356c786172356f6a3775686835646667327362636c34716b6f726967696e616c436964783b6261666b7234696661697772713532786b6e64726d376a6277717679777133746b696c79647978726a32346f6f66636b697436696e6b37663668796c72656e6577657245706f6368076f6c6561736547656e65726174696f6e037070726576696f75735374616d70436964783b6261666b7234696169777367753378356276756967746d733368726c6872777073686276356c786172356f6a3775686835646667327362636c3471
```
34 changes: 34 additions & 0 deletions tinycloud-node-server/tests/fixtures/renewal-stamp-v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"schema": "tinycloud/node/renewal-stamp-vector/v1",
"renewAbility": "tinycloud.delegation/renew",
"renewResourcePrefix": "urn:cid:",
"renewerKeyContext": "tinycloud/node/renewer/v1",
"stampDomain": "tinycloud/node/renewal-stamp/v1",
"testOnlyMasterSecretHex": "4242424242424242424242424242424242424242424242424242424242424242",
"originalArtifact": {
"type": "RenewalVectorOriginalDelegation",
"version": 1
},
"leaseArtifact": {
"type": "RenewalVectorLease",
"version": 1
},
"stamp": {
"domain": "tinycloud/node/renewal-stamp/v1",
"unsigned": {
"type": "TinyCloudRenewalStamp",
"version": 1,
"originalCid": "bafkr4ifaiwrq52xkndrm7jbwqvywq3tkilydyxrj24oofckit6ink7f6hy",
"leaseCid": "bafkr4iaiwsgu3x5bvuigtms3hrlhrwpshbv5lxar5oj7uhh5dfg2sbcl4q",
"leaseGeneration": 3,
"previousStampCid": "bafkr4iaiwsgu3x5bvuigtms3hrlhrwpshbv5lxar5oj7uhh5dfg2sbcl4q",
"nodeDid": "did:key:z6MkpXveUyjpCDe6P83rNYqAZmoTc9qopJ6GBuRQRotz3zF7",
"renewerEpoch": 7,
"newEnd": "2030-01-02T03:04:05Z"
}
},
"canonicalDagCborHex": "a266646f6d61696e781f74696e79636c6f75642f6e6f64652f72656e6577616c2d7374616d702f763168756e7369676e6564a964747970657554696e79436c6f756452656e6577616c5374616d70666e6577456e6474323033302d30312d30325430333a30343a30355a676e6f646544696478386469643a6b65793a7a364d6b7058766555796a7043446536503833724e5971415a6d6f546339716f704a364742755251526f747a337a46376776657273696f6e01686c65617365436964783b6261666b7234696169777367753378356276756967746d733368726c6872777073686276356c786172356f6a3775686835646667327362636c34716b6f726967696e616c436964783b6261666b7234696661697772713532786b6e64726d376a6277717679777133746b696c79647978726a32346f6f66636b697436696e6b37663668796c72656e6577657245706f6368076f6c6561736547656e65726174696f6e037070726576696f75735374616d70436964783b6261666b7234696169777367753378356276756967746d733368726c6872777073686276356c786172356f6a3775686835646667327362636c3471",
"stampCid": "bafkr4ibtywvyzwld56wljgytygpis2dddualt6h6b343e52rzmsegyw5nq",
"signerPublicKeyHex": "49fd44f3fe8930b0e89f61c51333195578deacaf22105a78f647d8c05afc7c5d",
"signatureHex": "34caf6cc60615a4c0d4f4e934c0263dde1c17fdd5becfa0f693337987b0250f0b430d7fe4518db41a882cc26ee979603736b883798fb83bbc95fb61af077020d"
}
89 changes: 89 additions & 0 deletions tinycloud-node-server/tests/renewal_stamp_v1_vectors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! Contract-only known-answer vector for the v1 renewal stamp.
//!
//! The fixture pins canonical DAG-CBOR bytes, the raw-Blake3 CID, and the
//! detached Ed25519 signature before runtime implementation. Its fixed master
//! seed is synthetic and public; it is not a production key.

use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use serde_json::{json, Value};
use tinycloud_core::{hash::hash, keys::StaticSecret};

fn raw_blake3_cid(bytes: &[u8]) -> String {
hash(bytes).to_cid(0x55).to_string()
}

fn dag_cbor(value: &Value) -> Vec<u8> {
serde_ipld_dagcbor::to_vec(value).expect("test vector DAG-CBOR serializes")
}

fn decode_hex(value: &Value) -> Vec<u8> {
hex::decode(value.as_str().expect("hex string")).expect("valid fixture hex")
}

#[test]
fn renewal_stamp_v1_matches_pinned_bytes_cid_and_signature() {
let fixture: Value = serde_json::from_str(include_str!("fixtures/renewal-stamp-v1.json"))
.expect("renewal vector JSON parses");
let key_context = fixture["renewerKeyContext"].as_str().unwrap().as_bytes();
let domain = fixture["stampDomain"].as_str().unwrap();
let master_secret = decode_hex(&fixture["testOnlyMasterSecretHex"]);
let node_secret = StaticSecret::new(master_secret).expect("32-byte test seed");
let signing_seed: [u8; 32] = node_secret
.derive_key(key_context)
.try_into()
.expect("fixed-length derivation");
let signer = SigningKey::from_bytes(&signing_seed);

let original_cid = raw_blake3_cid(&dag_cbor(&fixture["originalArtifact"]));
let lease_cid = raw_blake3_cid(&dag_cbor(&fixture["leaseArtifact"]));
let resource = format!(
"{}{}",
fixture["renewResourcePrefix"].as_str().unwrap(),
original_cid
);

assert_eq!(fixture["renewAbility"], "tinycloud.delegation/renew");
assert_eq!(
fixture["renewResourcePrefix"], "urn:cid:",
"the control capability targets the canonical original CID"
);
assert_eq!(resource, format!("urn:cid:{original_cid}"));

let stamp = &fixture["stamp"];
assert_eq!(stamp["domain"], domain);
assert_eq!(stamp["unsigned"]["type"], "TinyCloudRenewalStamp");
assert_eq!(stamp["unsigned"]["version"], 1);
assert_eq!(stamp["unsigned"]["originalCid"], original_cid);
assert_eq!(stamp["unsigned"]["leaseCid"], lease_cid);
assert_eq!(stamp["unsigned"]["previousStampCid"], lease_cid);
assert_eq!(stamp["unsigned"]["nodeDid"], node_secret.node_did());
assert_eq!(stamp["unsigned"]["leaseGeneration"], 3);
assert_eq!(stamp["unsigned"]["renewerEpoch"], 7);
assert_eq!(stamp["unsigned"]["newEnd"], "2030-01-02T03:04:05Z");

let bytes = dag_cbor(stamp);
assert_eq!(hex::encode(&bytes), fixture["canonicalDagCborHex"]);
assert_eq!(raw_blake3_cid(&bytes), fixture["stampCid"]);
assert_eq!(
hex::encode(signer.verifying_key().to_bytes()),
fixture["signerPublicKeyHex"]
);

let signature_bytes = signer.sign(&bytes).to_bytes();
assert_eq!(hex::encode(signature_bytes), fixture["signatureHex"]);
let signature = ed25519_dalek::Signature::from_slice(&decode_hex(&fixture["signatureHex"]))
.expect("64-byte Ed25519 signature");
let verifying_key = VerifyingKey::from_bytes(&signer.verifying_key().to_bytes())
.expect("derived public key is valid");
verifying_key
.verify(&bytes, &signature)
.expect("pinned detached signature verifies over exact DAG-CBOR bytes");

let decoded: Value =
serde_ipld_dagcbor::from_slice(&bytes).expect("canonical DAG-CBOR decodes");
assert_eq!(
dag_cbor(&decoded),
bytes,
"DAG-CBOR re-encoding is byte-identical"
);
}
Loading