Skip to main content

clawft_kernel/workload_pkg/
trust.rs

1//! Trust anchors: the pinned WeftOS signer set plus operator-pinned keys
2//! (ADR-099 section 8.1), and optionally pinned Cognitum release keys
3//! (ADR-100 section 6.2).
4
5use serde::{Deserialize, Serialize};
6
7use super::codec::{base64_decode, hex_decode_exact};
8use super::manifest::valid_token;
9
10/// Schema id of an operator trust file.
11pub const TRUST_FILE_SCHEMA: &str = "weftos.workload-trust.v1";
12
13/// Compiled-in WeftOS release signer set, as `(key_id, public_key_hex)`.
14///
15/// Empty until the WeftOS package-signing key is provisioned and its public
16/// half is pinned here (a code change reviewed like any other). Until then
17/// every accepted signature comes from an operator-pinned key.
18pub const WEFTOS_PINNED_SIGNERS: &[(&str, &str)] = &[];
19
20/// Which anchor set a key belongs to.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum KeyOrigin {
24    /// Compiled-in WeftOS signer set.
25    Weftos,
26    /// Operator-pinned key from governance config.
27    Operator,
28    /// Cognitum release-registry key (only used by the optional verifier).
29    CognitumRelease,
30}
31
32/// A pinned Ed25519 public key.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct PinnedKey {
35    /// Key id the signature entry must carry.
36    pub key_id: String,
37    /// Raw public key.
38    pub public_key: [u8; 32],
39    /// Anchor set.
40    pub origin: KeyOrigin,
41}
42
43/// Trust anchors used by the verifier.
44#[derive(Debug, Clone, Default, PartialEq, Eq)]
45pub struct TrustAnchors {
46    /// Keys whose signatures satisfy the manifest signature requirement.
47    pub signers: Vec<PinnedKey>,
48    /// Cognitum release-record keys, consulted only when the policy enables
49    /// the Cognitum verifier.
50    pub cognitum: Vec<PinnedKey>,
51}
52
53/// One key entry in a trust file.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct TrustFileKey {
57    /// Key id.
58    pub key_id: String,
59    /// Public key: 64 lower-case hex chars, or an Ed25519 SPKI PEM block.
60    pub public_key: String,
61}
62
63/// Operator trust file (JSON), as held in governance config.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct TrustFile {
67    /// Must be [`TRUST_FILE_SCHEMA`].
68    pub schema: String,
69    /// Operator-pinned package signers.
70    #[serde(default)]
71    pub operator_keys: Vec<TrustFileKey>,
72    /// Pinned Cognitum release-registry keys.
73    #[serde(default)]
74    pub cognitum_release_keys: Vec<TrustFileKey>,
75}
76
77impl TrustAnchors {
78    /// The compiled-in WeftOS signer set only.
79    pub fn weftos_default() -> Result<Self, String> {
80        let mut anchors = Self::default();
81        for (id, hex) in WEFTOS_PINNED_SIGNERS {
82            anchors.push_signer(id, hex, KeyOrigin::Weftos)?;
83        }
84        Ok(anchors)
85    }
86
87    /// WeftOS signer set plus the keys in an operator trust file.
88    pub fn from_trust_file(file: &TrustFile) -> Result<Self, String> {
89        if file.schema != TRUST_FILE_SCHEMA {
90            return Err(format!("trust file schema must be {TRUST_FILE_SCHEMA}"));
91        }
92        let mut anchors = Self::weftos_default()?;
93        for k in &file.operator_keys {
94            anchors.push_signer(&k.key_id, &k.public_key, KeyOrigin::Operator)?;
95        }
96        for k in &file.cognitum_release_keys {
97            let key = parse_key(&k.key_id, &k.public_key, KeyOrigin::CognitumRelease)?;
98            anchors.cognitum.push(key);
99        }
100        Ok(anchors)
101    }
102
103    /// Parse a trust file from JSON bytes.
104    pub fn from_trust_json(bytes: &[u8]) -> Result<Self, String> {
105        if bytes.len() > 256 * 1024 {
106            return Err("trust file too large".into());
107        }
108        let file: TrustFile =
109            serde_json::from_slice(bytes).map_err(|e| format!("trust file: {e}"))?;
110        Self::from_trust_file(&file)
111    }
112
113    /// Add a signer key.
114    pub fn push_signer(
115        &mut self,
116        key_id: &str,
117        public_key: &str,
118        origin: KeyOrigin,
119    ) -> Result<(), String> {
120        let key = parse_key(key_id, public_key, origin)?;
121        if let Some(existing) = self.signers.iter().find(|k| k.public_key == key.public_key) {
122            if existing.key_id != key.key_id {
123                return Err(format!(
124                    "public key pinned twice under {} and {}",
125                    existing.key_id, key.key_id
126                ));
127            }
128            return Ok(());
129        }
130        if self.signers.iter().any(|k| k.key_id == key.key_id) {
131            return Err(format!(
132                "key id {} pinned to two different keys",
133                key.key_id
134            ));
135        }
136        self.signers.push(key);
137        Ok(())
138    }
139
140    /// Pinned signer matching this raw public key.
141    pub fn signer(&self, public_key: &[u8; 32]) -> Option<&PinnedKey> {
142        self.signers.iter().find(|k| &k.public_key == public_key)
143    }
144}
145
146fn parse_key(key_id: &str, public_key: &str, origin: KeyOrigin) -> Result<PinnedKey, String> {
147    if !valid_token(key_id, 128) {
148        return Err(format!("bad key id {key_id:?}"));
149    }
150    let raw =
151        parse_public_key(public_key).ok_or_else(|| format!("{key_id}: bad Ed25519 public key"))?;
152    // Reject encodings that are not a valid curve point up front.
153    ed25519_dalek::VerifyingKey::from_bytes(&raw).map_err(|e| format!("{key_id}: {e}"))?;
154    Ok(PinnedKey {
155        key_id: key_id.to_string(),
156        public_key: raw,
157        origin,
158    })
159}
160
161/// Ed25519 SubjectPublicKeyInfo DER prefix (RFC 8410).
162const SPKI_PREFIX: [u8; 12] = [
163    0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
164];
165
166/// Parse 64-char lower-case hex, or an Ed25519 `PUBLIC KEY` PEM block (the
167/// form Cognitum trust registries publish).
168pub fn parse_public_key(s: &str) -> Option<[u8; 32]> {
169    let s = s.trim();
170    if let Some(raw) = hex_decode_exact::<32>(s) {
171        return Some(raw);
172    }
173    let body = s
174        .strip_prefix("-----BEGIN PUBLIC KEY-----")?
175        .strip_suffix("-----END PUBLIC KEY-----")?;
176    let b64: String = body.split_whitespace().collect();
177    let der = base64_decode(&b64)?;
178    if der.len() != 44 || der[..12] != SPKI_PREFIX {
179        return None;
180    }
181    der[12..].try_into().ok()
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    const PK_HEX: &str = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
189
190    #[test]
191    fn parses_hex_and_pem() {
192        let raw = parse_public_key(PK_HEX).unwrap();
193        let mut der = SPKI_PREFIX.to_vec();
194        der.extend_from_slice(&raw);
195        let b64 = standard_b64(&der);
196        let pem = format!("-----BEGIN PUBLIC KEY-----\n{b64}\n-----END PUBLIC KEY-----\n");
197        assert_eq!(parse_public_key(&pem), Some(raw));
198        assert_eq!(parse_public_key("zz"), None);
199    }
200
201    #[test]
202    fn trust_file_rejects_wrong_schema_and_conflicting_pins() {
203        let bad = br#"{"schema":"nope","operator_keys":[]}"#;
204        assert!(TrustAnchors::from_trust_json(bad).is_err());
205        let conflict = format!(
206            r#"{{"schema":"{TRUST_FILE_SCHEMA}","operator_keys":[{{"key_id":"a","public_key":"{PK_HEX}"}},{{"key_id":"b","public_key":"{PK_HEX}"}}]}}"#
207        );
208        assert!(TrustAnchors::from_trust_json(conflict.as_bytes()).is_err());
209        let ok = format!(
210            r#"{{"schema":"{TRUST_FILE_SCHEMA}","operator_keys":[{{"key_id":"a","public_key":"{PK_HEX}"}}]}}"#
211        );
212        let anchors = TrustAnchors::from_trust_json(ok.as_bytes()).unwrap();
213        assert_eq!(anchors.signers.len(), 1);
214        assert_eq!(anchors.signers[0].origin, KeyOrigin::Operator);
215    }
216
217    fn standard_b64(bytes: &[u8]) -> String {
218        const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
219        let mut s = String::new();
220        for chunk in bytes.chunks(3) {
221            let n =
222                chunk.iter().fold(0u32, |acc, b| (acc << 8) | *b as u32) << (8 * (3 - chunk.len()));
223            for i in 0..=chunk.len() {
224                s.push(A[((n >> (18 - 6 * i)) & 63) as usize] as char);
225            }
226        }
227        while !s.len().is_multiple_of(4) {
228            s.push('=');
229        }
230        s
231    }
232}