Skip to main content

memstead_base/
domain_authority_wire.rs

1//! Domain-authority publishing — the pure wire protocol shared by the signer
2//! (CLI) and the verifier (registry).
3//!
4//! The signed-payload format and the header/algorithm names are load-bearing:
5//! the CLI signs over exactly these bytes and the registry verifies over them.
6//! They live here, in the foundational crate both sides already depend on, so a
7//! change cannot drift one side out of agreement with the other. This module is
8//! pure — no crypto, no I/O — so it carries no extra dependency weight.
9
10/// The fixed path the proof manifest is served at on the publisher's own
11/// domain. Distinct from a deployment's discovery manifest
12/// (`memstead-authority.json`): the two make different claims.
13pub const MANIFEST_PATH: &str = "/.well-known/memstead-publishing.json";
14
15/// The only signature algorithm supported at launch. The prefix is part of
16/// every key/signature string (`ed25519:<base64>`).
17pub const ALG: &str = "ed25519";
18
19/// Domain-separation prefix on the signed payload. Binds a signature to *this*
20/// protocol and version so a signature made for some other purpose with the
21/// same key can never be replayed as a Memstead publish authorisation. Bump the
22/// version suffix if the payload shape ever changes.
23pub const SIGNING_DOMAIN: &str = "memstead-domain-publish-v1";
24
25/// Request headers carrying the per-publish signature. The publish handler
26/// reads these for a `<domain>:<handle>` scope; the CLI sets them.
27pub const HEADER_KEY: &str = "x-memstead-domain-key";
28pub const HEADER_SIGNATURE: &str = "x-memstead-domain-signature";
29pub const HEADER_TIMESTAMP: &str = "x-memstead-domain-timestamp";
30
31/// The exact bytes a publish signature covers. Binds the signature to *this*
32/// upload and target so a captured signature cannot be replayed onto a
33/// different archive, scope, name, version, or far-off time.
34///
35/// The serialisation is a newline-joined, domain-separated concatenation:
36///
37/// ```text
38/// memstead-domain-publish-v1\n
39/// <content_sha256>\n      (lowercase hex of the canonical archive bytes)
40/// <scope>\n               (canonical, e.g. acme.com:payments)
41/// <name>\n
42/// <version>\n
43/// <timestamp>             (unix seconds, decimal)
44/// ```
45///
46/// Every field is mandatory and both the signer and the verifier MUST produce
47/// these bytes identically. None of the fields can contain a newline (hex,
48/// scope, name, version, and a decimal integer are all newline-free), so the
49/// join is unambiguous.
50pub fn signing_payload(
51    content_sha256: &str,
52    scope: &str,
53    name: &str,
54    version: &str,
55    timestamp: i64,
56) -> Vec<u8> {
57    format!("{SIGNING_DOMAIN}\n{content_sha256}\n{scope}\n{name}\n{version}\n{timestamp}")
58        .into_bytes()
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn payload_is_deterministic_and_binds_every_field() {
67        let base = signing_payload("deadbeef", "acme.com:pay", "mem", "1.0.0", 1000);
68        assert_eq!(
69            base,
70            signing_payload("deadbeef", "acme.com:pay", "mem", "1.0.0", 1000)
71        );
72        assert_ne!(
73            base,
74            signing_payload("feedface", "acme.com:pay", "mem", "1.0.0", 1000)
75        );
76        assert_ne!(
77            base,
78            signing_payload("deadbeef", "other.com:pay", "mem", "1.0.0", 1000)
79        );
80        assert_ne!(
81            base,
82            signing_payload("deadbeef", "acme.com:pay", "other", "1.0.0", 1000)
83        );
84        assert_ne!(
85            base,
86            signing_payload("deadbeef", "acme.com:pay", "mem", "2.0.0", 1000)
87        );
88        assert_ne!(
89            base,
90            signing_payload("deadbeef", "acme.com:pay", "mem", "1.0.0", 1001)
91        );
92    }
93
94    /// Pin the exact wire bytes. If this vector ever changes, every already-
95    /// hosted manifest and both code paths must change together — this test is
96    /// the tripwire.
97    #[test]
98    fn payload_exact_bytes_are_pinned() {
99        let p = signing_payload("abc123", "acme.com:pay", "demo", "1.2.3", 1700000000);
100        assert_eq!(
101            p,
102            b"memstead-domain-publish-v1\nabc123\nacme.com:pay\ndemo\n1.2.3\n1700000000".to_vec()
103        );
104    }
105}