runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use base64::Engine;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use ring::signature::{ED25519, UnparsedPublicKey};
use std::collections::BTreeMap;

use super::source_authority::{
    RegistryManifestSourceAuthority, registry_manifest_source_authority_from_env,
    registry_manifest_source_key,
};
use super::types::{RegistrySignedManifest, TrustTier};

pub const REGISTRY_SIGNED_MANIFEST_SCHEMA: &str = "runx.registry.signed_manifest.v1";
pub const RUNX_REGISTRY_MANIFEST_TRUST_KEY_ENV: &str = "RUNX_REGISTRY_MANIFEST_TRUST_KEY_BASE64";
pub const RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID_ENV: &str = "RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID";
pub const RUNX_REGISTRY_MANIFEST_TRUST_OWNER_ENV: &str = "RUNX_REGISTRY_MANIFEST_TRUST_OWNER";

const RUNX_REGISTRY_MANIFEST_KEY_ID: &str = "runx-registry-ed25519-v1";
const RUNX_REGISTRY_MANIFEST_PUBLIC_KEY_BASE64: &str =
    "u770bVZvqF7ULrnxWeTeLTSOsEwlbIkoOQJEAVy98No=";
const REGISTRY_MANIFEST_SIGNATURE_BASE64_PREFIX: &str = "base64:";

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustedRegistryManifestKey {
    pub key_id: String,
    pub public_key: Vec<u8>,
    pub scope: RegistryManifestTrustScope,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RegistryManifestTrustScope {
    OfficialRunx,
    ThirdParty {
        allowed_owner: String,
        allowed_source: String,
    },
}

impl TrustedRegistryManifestKey {
    pub fn from_base64(
        key_id: String,
        public_key: &str,
        allowed_owner: String,
        allowed_source: String,
    ) -> Result<Self, RegistryManifestKeyError> {
        let allowed_owner = validate_owner_namespace(allowed_owner)?;
        let allowed_source = validate_registry_source(allowed_source)?;
        Self::from_base64_with_scope(
            key_id,
            public_key,
            RegistryManifestTrustScope::ThirdParty {
                allowed_owner,
                allowed_source,
            },
        )
    }

    pub fn official_from_base64(
        key_id: String,
        public_key: &str,
    ) -> Result<Self, RegistryManifestKeyError> {
        Self::from_base64_with_scope(key_id, public_key, RegistryManifestTrustScope::OfficialRunx)
    }

    fn from_base64_with_scope(
        key_id: String,
        public_key: &str,
        scope: RegistryManifestTrustScope,
    ) -> Result<Self, RegistryManifestKeyError> {
        let public_key = decode_base64(public_key).map_err(|_| RegistryManifestKeyError)?;
        if public_key.len() != 32 {
            return Err(RegistryManifestKeyError);
        }
        Ok(Self {
            key_id,
            public_key,
            scope,
        })
    }

    #[must_use]
    pub fn public_key_base64(&self) -> String {
        STANDARD.encode(&self.public_key)
    }
}

#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
#[error("registry manifest key is invalid")]
pub struct RegistryManifestKeyError;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RegistryManifestVerificationFailure {
    UnsupportedSchema,
    UnsupportedAlgorithm,
    MalformedPayload,
    UnknownKey,
    MalformedKey,
    MalformedSignature,
    SignatureMismatch,
}

pub fn verify_registry_signed_manifest<'a>(
    manifest: &RegistrySignedManifest,
    trusted_keys: &'a [TrustedRegistryManifestKey],
) -> Result<&'a TrustedRegistryManifestKey, RegistryManifestVerificationFailure> {
    if manifest.schema != REGISTRY_SIGNED_MANIFEST_SCHEMA {
        return Err(RegistryManifestVerificationFailure::UnsupportedSchema);
    }
    if manifest.signature.alg != "ed25519" {
        return Err(RegistryManifestVerificationFailure::UnsupportedAlgorithm);
    }
    validate_registry_manifest_payload_terms(manifest)?;
    let key = trusted_keys
        .iter()
        .find(|key| key.key_id == manifest.signer.key_id)
        .ok_or(RegistryManifestVerificationFailure::UnknownKey)?;
    if key.public_key.len() != 32 {
        return Err(RegistryManifestVerificationFailure::MalformedKey);
    }
    let signature = decode_signature(&manifest.signature.value)?;
    if signature.len() != 64 {
        return Err(RegistryManifestVerificationFailure::MalformedSignature);
    }
    let payload = registry_manifest_payload(
        &manifest.skill_id,
        &manifest.version,
        &manifest.digest,
        manifest.profile_digest.as_deref(),
        manifest.package_digest.as_deref(),
        &manifest.signer.id,
        &manifest.signer.key_id,
    );
    UnparsedPublicKey::new(&ED25519, &key.public_key)
        .verify(payload.as_bytes(), &signature)
        .map_err(|_| RegistryManifestVerificationFailure::SignatureMismatch)?;
    Ok(key)
}

pub fn default_trusted_registry_manifest_keys()
-> Result<Vec<TrustedRegistryManifestKey>, RegistryManifestKeyError> {
    Ok(vec![TrustedRegistryManifestKey::official_from_base64(
        RUNX_REGISTRY_MANIFEST_KEY_ID.to_owned(),
        RUNX_REGISTRY_MANIFEST_PUBLIC_KEY_BASE64,
    )?])
}

#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum RegistryManifestTrustEnvError {
    #[error("registry manifest trust key is invalid")]
    InvalidKey,
    #[error("registry manifest trust key id is required")]
    MissingKeyId,
    #[error("registry manifest trust owner is required")]
    MissingOwner,
    #[error("registry manifest trust source is required")]
    MissingSource,
}

pub fn trusted_registry_manifest_keys_from_env(
    env: &BTreeMap<String, String>,
) -> Result<Vec<TrustedRegistryManifestKey>, RegistryManifestTrustEnvError> {
    trusted_registry_manifest_keys_from_env_with_source(
        env,
        registry_manifest_source_authority_from_env(env),
    )
}

pub fn trusted_registry_manifest_keys_from_env_with_source(
    env: &BTreeMap<String, String>,
    source_authority: Option<RegistryManifestSourceAuthority>,
) -> Result<Vec<TrustedRegistryManifestKey>, RegistryManifestTrustEnvError> {
    let mut trusted_keys = default_trusted_registry_manifest_keys()
        .map_err(|_| RegistryManifestTrustEnvError::InvalidKey)?;
    let Some(public_key) = env.get(RUNX_REGISTRY_MANIFEST_TRUST_KEY_ENV) else {
        return Ok(trusted_keys);
    };
    let key_id = env
        .get(RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID_ENV)
        .cloned()
        .ok_or(RegistryManifestTrustEnvError::MissingKeyId)?;
    if matches!(
        source_authority,
        Some(RegistryManifestSourceAuthority::OfficialRunx)
    ) {
        return Err(RegistryManifestTrustEnvError::InvalidKey);
    }
    let allowed_owner = env
        .get(RUNX_REGISTRY_MANIFEST_TRUST_OWNER_ENV)
        .cloned()
        .ok_or(RegistryManifestTrustEnvError::MissingOwner)?;
    let allowed_source = source_authority
        .as_ref()
        .map(registry_manifest_source_key)
        .ok_or(RegistryManifestTrustEnvError::MissingSource)?;
    let key =
        TrustedRegistryManifestKey::from_base64(key_id, public_key, allowed_owner, allowed_source)
            .map_err(|_| RegistryManifestTrustEnvError::InvalidKey)?;
    trusted_keys.push(key);
    Ok(trusted_keys)
}

pub fn registry_manifest_key_allows(
    key: &TrustedRegistryManifestKey,
    skill_id: &str,
    trust_tier: &TrustTier,
    source_authority: Option<&RegistryManifestSourceAuthority>,
) -> Result<(), String> {
    match &key.scope {
        RegistryManifestTrustScope::OfficialRunx => {
            if !matches!(
                source_authority,
                Some(RegistryManifestSourceAuthority::OfficialRunx)
            ) {
                return Err(
                    "official key may only grant trust for the official runx registry source"
                        .to_owned(),
                );
            }
            if matches!(trust_tier, TrustTier::FirstParty) && !skill_id.starts_with("runx/") {
                return Err(
                    "official key may only grant first_party trust to runx/* skills".to_owned(),
                );
            }
            Ok(())
        }
        RegistryManifestTrustScope::ThirdParty {
            allowed_owner,
            allowed_source,
        } => {
            if matches!(trust_tier, TrustTier::FirstParty) {
                return Err("third-party keys may not grant first_party trust".to_owned());
            }
            let actual_source = source_authority
                .map(registry_manifest_source_key)
                .ok_or_else(|| "third-party key requires a registry source".to_owned())?;
            if actual_source != *allowed_source {
                return Err(format!(
                    "third-party key may only sign from registry source {allowed_source}"
                ));
            }
            let Some((owner, _name)) = skill_id.split_once('/') else {
                return Err("skill id must include an owner namespace".to_owned());
            };
            if owner == "runx" {
                return Err("third-party keys may not sign runx/* skills".to_owned());
            }
            if owner != allowed_owner {
                return Err(format!(
                    "third-party key may only sign {allowed_owner}/* skills"
                ));
            }
            Ok(())
        }
    }
}

fn validate_owner_namespace(value: String) -> Result<String, RegistryManifestKeyError> {
    let owner = value.trim();
    if owner.is_empty()
        || owner == "runx"
        || owner.contains('/')
        || owner
            .bytes()
            .any(|byte| matches!(byte, b'\n' | b'\r' | b'=' | 0))
    {
        return Err(RegistryManifestKeyError);
    }
    Ok(owner.to_owned())
}

fn validate_registry_source(value: String) -> Result<String, RegistryManifestKeyError> {
    let source = value.trim();
    if source.is_empty()
        || source
            .bytes()
            .any(|byte| matches!(byte, b'\n' | b'\r' | b'=' | 0))
    {
        return Err(RegistryManifestKeyError);
    }
    Ok(source.to_owned())
}

fn registry_manifest_payload(
    skill_id: &str,
    version: &str,
    digest: &str,
    profile_digest: Option<&str>,
    package_digest: Option<&str>,
    signer_id: &str,
    key_id: &str,
) -> String {
    format!(
        "{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",
        profile_digest.unwrap_or(""),
        package_digest.unwrap_or("")
    )
}

fn validate_registry_manifest_payload_terms(
    manifest: &RegistrySignedManifest,
) -> Result<(), RegistryManifestVerificationFailure> {
    validate_registry_manifest_payload_term(&manifest.skill_id)?;
    validate_registry_manifest_payload_term(&manifest.version)?;
    validate_registry_manifest_payload_term(&manifest.digest)?;
    if let Some(profile_digest) = &manifest.profile_digest {
        validate_registry_manifest_payload_term(profile_digest)?;
    }
    if let Some(package_digest) = &manifest.package_digest {
        validate_registry_manifest_payload_term(package_digest)?;
    }
    validate_registry_manifest_payload_term(&manifest.signer.id)?;
    validate_registry_manifest_payload_term(&manifest.signer.key_id)
}

fn validate_registry_manifest_payload_term(
    value: &str,
) -> Result<(), RegistryManifestVerificationFailure> {
    if value.is_empty()
        || value
            .bytes()
            .any(|byte| matches!(byte, b'\n' | b'\r' | b'=' | 0))
    {
        return Err(RegistryManifestVerificationFailure::MalformedPayload);
    }
    Ok(())
}

fn decode_signature(value: &str) -> Result<Vec<u8>, RegistryManifestVerificationFailure> {
    let Some(encoded) = value.strip_prefix(REGISTRY_MANIFEST_SIGNATURE_BASE64_PREFIX) else {
        return Err(RegistryManifestVerificationFailure::MalformedSignature);
    };
    decode_base64(encoded).map_err(|_| RegistryManifestVerificationFailure::MalformedSignature)
}

fn decode_base64(value: &str) -> Result<Vec<u8>, base64::DecodeError> {
    URL_SAFE_NO_PAD
        .decode(value)
        .or_else(|_| STANDARD.decode(value))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn trust_env() -> BTreeMap<String, String> {
        BTreeMap::from([
            (
                RUNX_REGISTRY_MANIFEST_TRUST_KEY_ID_ENV.to_owned(),
                "test-key".to_owned(),
            ),
            (
                RUNX_REGISTRY_MANIFEST_TRUST_KEY_ENV.to_owned(),
                "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=".to_owned(),
            ),
        ])
    }

    #[test]
    fn env_trust_key_cannot_promote_itself_to_official_source() {
        let result = trusted_registry_manifest_keys_from_env_with_source(
            &trust_env(),
            Some(RegistryManifestSourceAuthority::OfficialRunx),
        );
        assert_eq!(result, Err(RegistryManifestTrustEnvError::InvalidKey));
    }

    #[test]
    fn third_party_source_trust_key_still_requires_owner() {
        let result = trusted_registry_manifest_keys_from_env_with_source(
            &trust_env(),
            Some(RegistryManifestSourceAuthority::RegistrySource(
                "local:/tmp/runx-registry".to_owned(),
            )),
        );

        assert_eq!(result, Err(RegistryManifestTrustEnvError::MissingOwner));
    }
}