Skip to main content

mkit_server/takedown/
publication.rs

1//! Required receipt-and-notice role key publication for takedown deployments.
2use crate::{Code, ServerError};
3use mkit_core::hash::{hash, to_hex};
4use serde::Deserialize;
5
6#[derive(Deserialize)]
7#[serde(rename_all = "camelCase", deny_unknown_fields)]
8struct ReceiptEntry {
9    key_id: String,
10    alg: String,
11    public_key: String,
12    not_before_ms: Option<String>,
13    not_after_ms: Option<String>,
14}
15#[derive(Deserialize)]
16#[serde(deny_unknown_fields)]
17struct List {
18    version: u32,
19    keys: Vec<ReceiptEntry>,
20}
21
22/// Validated public configuration; private key bytes never enter diagnostics.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct PublicationConfig {
25    /// Current raw Ed25519 public key.
26    pub public_key: [u8; 32],
27    /// BLAKE3 digest of the public key, without a prefix.
28    pub key_id: String,
29    /// Exact public key-list JSON returned at the well-known endpoint.
30    pub key_list: String,
31    keys: Vec<[u8; 32]>,
32}
33impl PublicationConfig {
34    /// Validate the private seed and published list required by ยง14.7.
35    /// # Errors
36    /// Invalid keys, duplicate identities, bounds or a missing signing key.
37    pub fn parse(seed_hex: &str, list: &str) -> Result<Self, ServerError> {
38        let invalid = || {
39            ServerError::new(
40                Code::InvalidArgument,
41                "invalid receipt-and-notice key configuration",
42            )
43        };
44        let seed =
45            zeroize::Zeroizing::new(crate::admin::auth::hex::<32>(seed_hex).ok_or_else(invalid)?);
46        let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
47        let public_key = signing_key.verifying_key().to_bytes();
48        let key_id = to_hex(&hash(&public_key));
49        if list.len() > 65_536 {
50            return Err(invalid());
51        }
52        let parsed: List = serde_json::from_str(list).map_err(|_| invalid())?;
53        if parsed.version != 1 || parsed.keys.is_empty() || parsed.keys.len() > 128 {
54            return Err(invalid());
55        }
56        let mut key_ids = std::collections::BTreeSet::new();
57        let mut present = false;
58        let mut keys = Vec::new();
59        for entry in parsed.keys {
60            let public = crate::admin::auth::hex::<32>(&entry.public_key).ok_or_else(invalid)?;
61            ed25519_dalek::VerifyingKey::from_bytes(&public).map_err(|_| invalid())?;
62            if entry.alg != "ed25519"
63                || entry.key_id != to_hex(&hash(&public))
64                || !key_ids.insert(entry.key_id.clone())
65            {
66                return Err(invalid());
67            }
68            let bound = |value: Option<String>| -> Result<Option<i64>, ServerError> {
69                value
70                    .map(|s| {
71                        s.parse::<i64>()
72                            .ok()
73                            .filter(|v| v.to_string() == s)
74                            .ok_or_else(invalid)
75                    })
76                    .transpose()
77            };
78            let before = bound(entry.not_before_ms)?;
79            let after = bound(entry.not_after_ms)?;
80            if before.zip(after).is_some_and(|(b, a)| b >= a) {
81                return Err(invalid());
82            }
83            present |= entry.key_id == key_id;
84            keys.push(public);
85        }
86        if !present {
87            return Err(invalid());
88        }
89        Ok(Self {
90            public_key,
91            key_id,
92            key_list: list.into(),
93            keys,
94        })
95    }
96    /// Every published role key, including retired keys forbidden for writers.
97    #[must_use]
98    pub fn public_keys(&self) -> &[[u8; 32]] {
99        &self.keys
100    }
101}