Skip to main content

systemprompt_cli/commands/core/services/
signing.rs

1//! Bundle signing seeds: where they come from and what they identify.
2//!
3//! A bundle signing key is an ed25519 seed owned by the editing repository,
4//! never the instance manifest seed. It is supplied as a file holding the
5//! base64 seed, or as `env:VAR` naming an environment variable, so CI can
6//! keep it in its own secret store and never write it to the workspace.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::path::Path;
12
13use anyhow::{Context, Result, bail};
14use base64::Engine;
15use rand::{Rng, rng};
16use systemprompt_security::manifest_signing::{key_id_for_pubkey, pubkey_b64_from_seed};
17
18pub const SEED_BYTES: usize = 32;
19pub const ENV_PREFIX: &str = "env:";
20
21#[derive(Debug, Clone)]
22pub struct BundleSigningKey {
23    pub seed: [u8; SEED_BYTES],
24    pub public_key: String,
25    pub key_id: String,
26}
27
28impl BundleSigningKey {
29    #[must_use]
30    pub fn from_seed(seed: [u8; SEED_BYTES]) -> Self {
31        let public_key = pubkey_b64_from_seed(&seed);
32        let key_id = key_id_for_pubkey(&public_key);
33        Self {
34            seed,
35            public_key,
36            key_id,
37        }
38    }
39
40    #[must_use]
41    pub fn generate() -> Self {
42        let mut seed = [0u8; SEED_BYTES];
43        rng().fill_bytes(&mut seed);
44        Self::from_seed(seed)
45    }
46
47    #[must_use]
48    pub fn seed_b64(&self) -> String {
49        base64::engine::general_purpose::STANDARD.encode(self.seed)
50    }
51}
52
53pub fn load_signing_key(reference: &str) -> Result<BundleSigningKey> {
54    let encoded = if let Some(var) = reference.strip_prefix(ENV_PREFIX) {
55        std::env::var(var).with_context(|| format!("Signing key variable {var} is not set"))?
56    } else {
57        std::fs::read_to_string(Path::new(reference))
58            .with_context(|| format!("Failed to read signing key {reference}"))?
59    };
60    decode_seed(encoded.trim())
61}
62
63pub fn decode_seed(encoded: &str) -> Result<BundleSigningKey> {
64    let raw = base64::engine::general_purpose::STANDARD
65        .decode(encoded)
66        .context("Signing key is not valid base64")?;
67    let seed: [u8; SEED_BYTES] = raw.as_slice().try_into().map_or_else(
68        |_e| {
69            bail!(
70                "Signing key must decode to {SEED_BYTES} bytes, got {}",
71                raw.len()
72            )
73        },
74        Ok,
75    )?;
76    Ok(BundleSigningKey::from_seed(seed))
77}