Skip to main content

aven_protocol/claim/
publication.rs

1//! Fixed genesis-to-PublishBootstrap signed profile.
2//! Signing prepares intent only; it neither persists dispatch ownership nor adopts
3//! membership. Hosts must not dispatch from a never-dispatched capture journal.
4//!
5//! # Fixed publication profile
6//!
7//! Membership byte strings use U32 big-endian lengths, not catalog U64 framing.
8//! T is bootstrap[32], stream[32], SHA256(exact descriptor)[32], manifest ordered
9//! ciphertext aggregate[32], U64(N). The 416-byte resulting state is genesis state
10//! with AVGS version 2, bootstrap-present=1 and T immediately after that flag. All
11//! remaining genesis fields are unchanged. Action is AVPB, U16(1), T (142 bytes).
12//! The 301-byte core retains genesis framing with sequence=1, predecessor=genesis
13//! commitment, device signer=seed, action=2 and the resulting state commitment.
14//! Attachments are exactly AVGA, U16(2), U8(0 count), seven bytes with no grants.
15//! Outer record version 1 frames core, state, attachments and signature (805 bytes).
16//! State/signature CCE labels and whole-record SHA256 are unchanged from genesis.
17//! No other successors, counts, state flags or grants are supported. Epoch is an
18//! unsigned first-admission precondition, never part of immutable signed intent.
19
20use super::*;
21use crate::artifact::Descriptor;
22
23pub const PUBLICATION_BYTES: usize = 805;
24
25/// Client-authorized immutable bootstrap binding. Not proof of server acceptance.
26#[derive(Clone, PartialEq, Eq)]
27pub struct Publication {
28    record: Box<[u8; PUBLICATION_BYTES]>,
29    pub(crate) binding: PublicationBinding,
30}
31
32impl fmt::Debug for Publication {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.debug_struct("Publication")
35            .field("commitment", &self.commitment())
36            .field("binding", &self.binding)
37            .finish()
38    }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct PublicationBinding {
43    pub vault_id: [u8; 32],
44    pub genesis_commitment: [u8; 32],
45    pub bootstrap_id: [u8; 32],
46    pub stream_id: [u8; 32],
47    pub descriptor_commitment: [u8; 32],
48    pub manifest_commitment: [u8; 32],
49    pub prefix_count: u64,
50}
51
52impl PublicationBinding {
53    pub fn from_descriptor(genesis: &Genesis, descriptor: &[u8]) -> Result<Self> {
54        let d = Descriptor::decode(descriptor)?;
55        let b = &d;
56        ensure!(
57            b.vault == genesis.context().vault_id
58                && b.generation == genesis.context().generation_id
59                && b.membership == genesis.commitment(),
60            "error bootstrap-publication-context"
61        );
62        Ok(Self {
63            vault_id: b.vault,
64            genesis_commitment: b.membership,
65            bootstrap_id: b.bootstrap,
66            stream_id: b.stream,
67            descriptor_commitment: hash(descriptor),
68            manifest_commitment: d.manifest.aggregate,
69            prefix_count: d.prefix,
70        })
71    }
72
73    pub(super) fn tuple(&self) -> Vec<u8> {
74        let mut out = Vec::with_capacity(136);
75        for value in [
76            self.bootstrap_id,
77            self.stream_id,
78            self.descriptor_commitment,
79            self.manifest_commitment,
80        ] {
81            out.extend(value);
82        }
83        out.extend(self.prefix_count.to_be_bytes());
84        out
85    }
86}
87
88pub(super) fn components(
89    genesis: &Genesis,
90    binding: &PublicationBinding,
91) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
92    let (_, original, _, _) = codec::components(genesis.record()).expect("verified genesis");
93    let mut state = b"AVGS\0\x02\x01".to_vec();
94    state.extend(genesis.context().vault_id);
95    state.push(1);
96    state.extend(binding.tuple());
97    // The remaining authority and generation fields retain their genesis bytes.
98    state.extend_from_slice(&original[40..]);
99    let mut body = b"AVPB\0\x01".to_vec();
100    body.extend(binding.tuple());
101    let core = membership_core(
102        &genesis.context().vault_id,
103        1,
104        &genesis.commitment(),
105        &genesis.device_id(),
106        2,
107        &body,
108        &state,
109    );
110    (core, state, b"AVGA\0\x02\0".to_vec())
111}
112
113impl Publication {
114    /// Verifies exact successor semantics against a trusted genesis and descriptor.
115    /// A server must additionally authenticate current membership and admission.
116    pub fn from_record(genesis: &Genesis, descriptor: &[u8], record: &[u8]) -> Result<Self> {
117        ensure!(
118            record.len() == PUBLICATION_BYTES,
119            "error bootstrap-publication-invalid"
120        );
121        let binding = PublicationBinding::from_descriptor(genesis, descriptor)?;
122        let (core, state, attachments) = components(genesis, &binding);
123        let mut r = Reader(record);
124        valid(r.take(1)? == [1])?;
125        valid(r.blob(301)? == core)?;
126        valid(r.blob(416)? == state)?;
127        valid(r.blob(7)? == attachments)?;
128        let signature = ed25519_dalek::Signature::from_slice(r.blob(64)?)?;
129        r.end()?;
130        ed25519_dalek::VerifyingKey::from_bytes(&genesis.signing_public())?
131            .verify_strict(&cce(MEMBERSHIP_SIGN, &[&core, &attachments]), &signature)
132            .map_err(|_| anyhow::anyhow!("error bootstrap-publication-signature"))?;
133        Ok(Self {
134            record: Box::new(record.try_into()?),
135            binding,
136        })
137    }
138
139    pub fn record(&self) -> &[u8; PUBLICATION_BYTES] {
140        &self.record
141    }
142    pub fn commitment(&self) -> [u8; 32] {
143        hash(self.record.as_slice())
144    }
145    pub fn binding(&self) -> &PublicationBinding {
146        &self.binding
147    }
148}
149
150impl SeedAuthority {
151    /// Signs publication intent for a descriptor whose package the caller
152    /// has already authenticated under `key` against this genesis.
153    pub fn sign_authenticated_publication(
154        &self,
155        descriptor: &[u8],
156        key: &LocalSharedStatePackageKey,
157    ) -> Result<Publication> {
158        self.validate(self.genesis().context(), key)?;
159        let binding = PublicationBinding::from_descriptor(self.genesis(), descriptor)?;
160        let (core, state, attachments) = components(self.genesis(), &binding);
161        let signature = SigningKey::from_bytes(self.protected_signing_seed().expose())
162            .sign(&cce(MEMBERSHIP_SIGN, &[&core, &attachments]));
163        let mut record = vec![1];
164        for part in [&core[..], &state, &attachments, &signature.to_bytes()] {
165            bytes(&mut record, part);
166        }
167        Publication::from_record(self.genesis(), descriptor, &record)
168    }
169}
170
171/// Retained server acceptance of exact signed intent. Not physical durability,
172/// current membership, or permission to adopt a local database.
173#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct PublicationOutcome {
175    pub(crate) publication: Publication,
176}
177
178impl PublicationOutcome {
179    /// Wraps verified intent as a retained outcome; callers establish acceptance.
180    pub fn from_publication(publication: Publication) -> Self {
181        Self { publication }
182    }
183
184    /// Decodes a publication reported by an authenticated transport. The signature
185    /// proves exact client intent, not server storage or current authorization.
186    /// Expectations must come from protected local intent, never the response.
187    pub fn from_response(genesis: &Genesis, descriptor: &[u8], record: &[u8]) -> Result<Self> {
188        Ok(Self {
189            publication: Publication::from_record(genesis, descriptor, record)?,
190        })
191    }
192
193    pub fn publication(&self) -> &Publication {
194        &self.publication
195    }
196
197    /// Authenticate the signed binding against pinned genesis and local expectation.
198    /// Never replace that expectation with a descriptor supplied by the server.
199    /// The record is already verified against the genesis and descriptor its
200    /// binding commits to, so comparing commitments suffices.
201    pub fn validate_expected(&self, genesis: &Genesis, expected_descriptor: &[u8]) -> Result<()> {
202        let b = &self.publication.binding;
203        ensure!(
204            b.genesis_commitment == genesis.commitment()
205                && b.descriptor_commitment == hash(expected_descriptor),
206            "error bootstrap-publication-conflict"
207        );
208        Ok(())
209    }
210}
211
212#[cfg(any(test, feature = "test-support"))]
213pub(super) mod fixture;
214#[cfg(test)]
215pub(super) mod tests;