Skip to main content

runx_runtime/registry/
trust_anchor.rs

1use base64::Engine;
2use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
3use ring::signature::{ED25519, UnparsedPublicKey};
4use std::collections::BTreeMap;
5
6use super::source_authority::{
7    RegistryManifestSourceAuthority, registry_manifest_source_authority_from_env,
8    registry_manifest_source_key,
9};
10use super::types::{RegistrySignedManifest, TrustTier};
11
12pub const REGISTRY_SIGNED_MANIFEST_SCHEMA: &str = "runx.registry.signed_manifest.v1";
13pub const RUNX_REGISTRY_MANIFEST_TRUST_KEY_ENV: &str = "RUNX_REGISTRY_MANIFEST_TRUST_KEY_BASE64";
14pub const RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID_ENV: &str = "RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID";
15pub const RUNX_REGISTRY_MANIFEST_TRUST_OWNER_ENV: &str = "RUNX_REGISTRY_MANIFEST_TRUST_OWNER";
16
17const RUNX_REGISTRY_MANIFEST_KEY_ID: &str = "runx-registry-ed25519-v1";
18const RUNX_REGISTRY_MANIFEST_PUBLIC_KEY_BASE64: &str =
19    "u770bVZvqF7ULrnxWeTeLTSOsEwlbIkoOQJEAVy98No=";
20const REGISTRY_MANIFEST_SIGNATURE_BASE64_PREFIX: &str = "base64:";
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct TrustedRegistryManifestKey {
24    pub key_id: String,
25    pub public_key: Vec<u8>,
26    pub scope: RegistryManifestTrustScope,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum RegistryManifestTrustScope {
31    OfficialRunx,
32    ThirdParty {
33        allowed_owner: String,
34        allowed_source: String,
35    },
36}
37
38impl TrustedRegistryManifestKey {
39    pub fn from_base64(
40        key_id: String,
41        public_key: &str,
42        allowed_owner: String,
43        allowed_source: String,
44    ) -> Result<Self, RegistryManifestKeyError> {
45        let allowed_owner = validate_owner_namespace(allowed_owner)?;
46        let allowed_source = validate_registry_source(allowed_source)?;
47        Self::from_base64_with_scope(
48            key_id,
49            public_key,
50            RegistryManifestTrustScope::ThirdParty {
51                allowed_owner,
52                allowed_source,
53            },
54        )
55    }
56
57    pub fn official_from_base64(
58        key_id: String,
59        public_key: &str,
60    ) -> Result<Self, RegistryManifestKeyError> {
61        Self::from_base64_with_scope(key_id, public_key, RegistryManifestTrustScope::OfficialRunx)
62    }
63
64    fn from_base64_with_scope(
65        key_id: String,
66        public_key: &str,
67        scope: RegistryManifestTrustScope,
68    ) -> Result<Self, RegistryManifestKeyError> {
69        let public_key = decode_base64(public_key).map_err(|_| RegistryManifestKeyError)?;
70        if public_key.len() != 32 {
71            return Err(RegistryManifestKeyError);
72        }
73        Ok(Self {
74            key_id,
75            public_key,
76            scope,
77        })
78    }
79
80    #[must_use]
81    pub fn public_key_base64(&self) -> String {
82        STANDARD.encode(&self.public_key)
83    }
84}
85
86#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
87#[error("registry manifest key is invalid")]
88pub struct RegistryManifestKeyError;
89
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub enum RegistryManifestVerificationFailure {
92    UnsupportedSchema,
93    UnsupportedAlgorithm,
94    MalformedPayload,
95    UnknownKey,
96    MalformedKey,
97    MalformedSignature,
98    SignatureMismatch,
99}
100
101pub fn verify_registry_signed_manifest<'a>(
102    manifest: &RegistrySignedManifest,
103    trusted_keys: &'a [TrustedRegistryManifestKey],
104) -> Result<&'a TrustedRegistryManifestKey, RegistryManifestVerificationFailure> {
105    if manifest.schema != REGISTRY_SIGNED_MANIFEST_SCHEMA {
106        return Err(RegistryManifestVerificationFailure::UnsupportedSchema);
107    }
108    if manifest.signature.alg != "ed25519" {
109        return Err(RegistryManifestVerificationFailure::UnsupportedAlgorithm);
110    }
111    validate_registry_manifest_payload_terms(manifest)?;
112    let key = trusted_keys
113        .iter()
114        .find(|key| key.key_id == manifest.signer.key_id)
115        .ok_or(RegistryManifestVerificationFailure::UnknownKey)?;
116    if key.public_key.len() != 32 {
117        return Err(RegistryManifestVerificationFailure::MalformedKey);
118    }
119    let signature = decode_signature(&manifest.signature.value)?;
120    if signature.len() != 64 {
121        return Err(RegistryManifestVerificationFailure::MalformedSignature);
122    }
123    let payload = registry_manifest_payload(
124        &manifest.skill_id,
125        &manifest.version,
126        &manifest.digest,
127        manifest.profile_digest.as_deref(),
128        manifest.package_digest.as_deref(),
129        &manifest.signer.id,
130        &manifest.signer.key_id,
131    );
132    UnparsedPublicKey::new(&ED25519, &key.public_key)
133        .verify(payload.as_bytes(), &signature)
134        .map_err(|_| RegistryManifestVerificationFailure::SignatureMismatch)?;
135    Ok(key)
136}
137
138pub fn default_trusted_registry_manifest_keys()
139-> Result<Vec<TrustedRegistryManifestKey>, RegistryManifestKeyError> {
140    Ok(vec![TrustedRegistryManifestKey::official_from_base64(
141        RUNX_REGISTRY_MANIFEST_KEY_ID.to_owned(),
142        RUNX_REGISTRY_MANIFEST_PUBLIC_KEY_BASE64,
143    )?])
144}
145
146#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
147pub enum RegistryManifestTrustEnvError {
148    #[error("registry manifest trust key is invalid")]
149    InvalidKey,
150    #[error("registry manifest trust key id is required")]
151    MissingKeyId,
152    #[error("registry manifest trust owner is required")]
153    MissingOwner,
154    #[error("registry manifest trust source is required")]
155    MissingSource,
156}
157
158pub fn trusted_registry_manifest_keys_from_env(
159    env: &BTreeMap<String, String>,
160) -> Result<Vec<TrustedRegistryManifestKey>, RegistryManifestTrustEnvError> {
161    trusted_registry_manifest_keys_from_env_with_source(
162        env,
163        registry_manifest_source_authority_from_env(env),
164    )
165}
166
167pub fn trusted_registry_manifest_keys_from_env_with_source(
168    env: &BTreeMap<String, String>,
169    source_authority: Option<RegistryManifestSourceAuthority>,
170) -> Result<Vec<TrustedRegistryManifestKey>, RegistryManifestTrustEnvError> {
171    let mut trusted_keys = default_trusted_registry_manifest_keys()
172        .map_err(|_| RegistryManifestTrustEnvError::InvalidKey)?;
173    let Some(public_key) = env.get(RUNX_REGISTRY_MANIFEST_TRUST_KEY_ENV) else {
174        return Ok(trusted_keys);
175    };
176    let key_id = env
177        .get(RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID_ENV)
178        .cloned()
179        .ok_or(RegistryManifestTrustEnvError::MissingKeyId)?;
180    if matches!(
181        source_authority,
182        Some(RegistryManifestSourceAuthority::OfficialRunx)
183    ) {
184        return Err(RegistryManifestTrustEnvError::InvalidKey);
185    }
186    let allowed_owner = env
187        .get(RUNX_REGISTRY_MANIFEST_TRUST_OWNER_ENV)
188        .cloned()
189        .ok_or(RegistryManifestTrustEnvError::MissingOwner)?;
190    let allowed_source = source_authority
191        .as_ref()
192        .map(registry_manifest_source_key)
193        .ok_or(RegistryManifestTrustEnvError::MissingSource)?;
194    let key =
195        TrustedRegistryManifestKey::from_base64(key_id, public_key, allowed_owner, allowed_source)
196            .map_err(|_| RegistryManifestTrustEnvError::InvalidKey)?;
197    trusted_keys.push(key);
198    Ok(trusted_keys)
199}
200
201pub fn registry_manifest_key_allows(
202    key: &TrustedRegistryManifestKey,
203    skill_id: &str,
204    trust_tier: &TrustTier,
205    source_authority: Option<&RegistryManifestSourceAuthority>,
206) -> Result<(), String> {
207    match &key.scope {
208        RegistryManifestTrustScope::OfficialRunx => {
209            if !matches!(
210                source_authority,
211                Some(RegistryManifestSourceAuthority::OfficialRunx)
212            ) {
213                return Err(
214                    "official key may only grant trust for the official runx registry source"
215                        .to_owned(),
216                );
217            }
218            if matches!(trust_tier, TrustTier::FirstParty) && !skill_id.starts_with("runx/") {
219                return Err(
220                    "official key may only grant first_party trust to runx/* skills".to_owned(),
221                );
222            }
223            Ok(())
224        }
225        RegistryManifestTrustScope::ThirdParty {
226            allowed_owner,
227            allowed_source,
228        } => {
229            if matches!(trust_tier, TrustTier::FirstParty) {
230                return Err("third-party keys may not grant first_party trust".to_owned());
231            }
232            let actual_source = source_authority
233                .map(registry_manifest_source_key)
234                .ok_or_else(|| "third-party key requires a registry source".to_owned())?;
235            if actual_source != *allowed_source {
236                return Err(format!(
237                    "third-party key may only sign from registry source {allowed_source}"
238                ));
239            }
240            let Some((owner, _name)) = skill_id.split_once('/') else {
241                return Err("skill id must include an owner namespace".to_owned());
242            };
243            if owner == "runx" {
244                return Err("third-party keys may not sign runx/* skills".to_owned());
245            }
246            if owner != allowed_owner {
247                return Err(format!(
248                    "third-party key may only sign {allowed_owner}/* skills"
249                ));
250            }
251            Ok(())
252        }
253    }
254}
255
256fn validate_owner_namespace(value: String) -> Result<String, RegistryManifestKeyError> {
257    let owner = value.trim();
258    if owner.is_empty()
259        || owner == "runx"
260        || owner.contains('/')
261        || owner
262            .bytes()
263            .any(|byte| matches!(byte, b'\n' | b'\r' | b'=' | 0))
264    {
265        return Err(RegistryManifestKeyError);
266    }
267    Ok(owner.to_owned())
268}
269
270fn validate_registry_source(value: String) -> Result<String, RegistryManifestKeyError> {
271    let source = value.trim();
272    if source.is_empty()
273        || source
274            .bytes()
275            .any(|byte| matches!(byte, b'\n' | b'\r' | b'=' | 0))
276    {
277        return Err(RegistryManifestKeyError);
278    }
279    Ok(source.to_owned())
280}
281
282fn registry_manifest_payload(
283    skill_id: &str,
284    version: &str,
285    digest: &str,
286    profile_digest: Option<&str>,
287    package_digest: Option<&str>,
288    signer_id: &str,
289    key_id: &str,
290) -> String {
291    format!(
292        "{REGISTRY_SIGNED_MANIFEST_SCHEMA}\nskill_id={skill_id}\nversion={version}\ndigest={digest}\nprofile_digest={}\npackage_digest={}\nsigner_id={signer_id}\nkey_id={key_id}\n",
293        profile_digest.unwrap_or(""),
294        package_digest.unwrap_or("")
295    )
296}
297
298fn validate_registry_manifest_payload_terms(
299    manifest: &RegistrySignedManifest,
300) -> Result<(), RegistryManifestVerificationFailure> {
301    validate_registry_manifest_payload_term(&manifest.skill_id)?;
302    validate_registry_manifest_payload_term(&manifest.version)?;
303    validate_registry_manifest_payload_term(&manifest.digest)?;
304    if let Some(profile_digest) = &manifest.profile_digest {
305        validate_registry_manifest_payload_term(profile_digest)?;
306    }
307    if let Some(package_digest) = &manifest.package_digest {
308        validate_registry_manifest_payload_term(package_digest)?;
309    }
310    validate_registry_manifest_payload_term(&manifest.signer.id)?;
311    validate_registry_manifest_payload_term(&manifest.signer.key_id)
312}
313
314fn validate_registry_manifest_payload_term(
315    value: &str,
316) -> Result<(), RegistryManifestVerificationFailure> {
317    if value.is_empty()
318        || value
319            .bytes()
320            .any(|byte| matches!(byte, b'\n' | b'\r' | b'=' | 0))
321    {
322        return Err(RegistryManifestVerificationFailure::MalformedPayload);
323    }
324    Ok(())
325}
326
327fn decode_signature(value: &str) -> Result<Vec<u8>, RegistryManifestVerificationFailure> {
328    let Some(encoded) = value.strip_prefix(REGISTRY_MANIFEST_SIGNATURE_BASE64_PREFIX) else {
329        return Err(RegistryManifestVerificationFailure::MalformedSignature);
330    };
331    decode_base64(encoded).map_err(|_| RegistryManifestVerificationFailure::MalformedSignature)
332}
333
334fn decode_base64(value: &str) -> Result<Vec<u8>, base64::DecodeError> {
335    URL_SAFE_NO_PAD
336        .decode(value)
337        .or_else(|_| STANDARD.decode(value))
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    fn trust_env() -> BTreeMap<String, String> {
345        BTreeMap::from([
346            (
347                RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID_ENV.to_owned(),
348                "test-key".to_owned(),
349            ),
350            (
351                RUNX_REGISTRY_MANIFEST_TRUST_KEY_ENV.to_owned(),
352                "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=".to_owned(),
353            ),
354        ])
355    }
356
357    #[test]
358    fn env_trust_key_cannot_promote_itself_to_official_source() {
359        let result = trusted_registry_manifest_keys_from_env_with_source(
360            &trust_env(),
361            Some(RegistryManifestSourceAuthority::OfficialRunx),
362        );
363        assert_eq!(result, Err(RegistryManifestTrustEnvError::InvalidKey));
364    }
365
366    #[test]
367    fn third_party_source_trust_key_still_requires_owner() {
368        let result = trusted_registry_manifest_keys_from_env_with_source(
369            &trust_env(),
370            Some(RegistryManifestSourceAuthority::RegistrySource(
371                "local:/tmp/runx-registry".to_owned(),
372            )),
373        );
374
375        assert_eq!(result, Err(RegistryManifestTrustEnvError::MissingOwner));
376    }
377}