Files
keysat/licensing-service/src/crypto/keys.rs
T
Grant 6ac118ae70 v0.1.0:24 — Keysat licensing service end-to-end
Daemon, StartOS wrapper, admin SPA, public buy/thank-you pages,
discount codes, free-license redemption, Apply-discount UX,
self-licensing, and v0.1.0 release notes.
2026-05-07 10:33:39 -05:00

78 lines
2.5 KiB
Rust

//! Server key lifecycle: generate on first boot, load on subsequent boots.
//!
//! Keys are stored in SQLite (rather than on the filesystem) so the same
//! backup mechanism that protects licenses also protects the signing key.
//! On StartOS, the database file lives under the service's encrypted data
//! volume, so at-rest encryption is handled by the OS.
use anyhow::{Context, Result};
use chrono::Utc;
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
use ed25519_dalek::{SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use sqlx::SqlitePool;
/// Both halves of the server keypair.
#[derive(Clone)]
pub struct ServerKeypair {
pub signing: SigningKey,
pub verifying: VerifyingKey,
/// PEM-encoded public key, for display / SDK bundling.
pub public_key_pem: String,
}
/// Load the keypair from the DB, generating and persisting a new one if no
/// row exists. This function is idempotent and safe to call on every boot.
pub async fn load_or_generate(pool: &SqlitePool) -> Result<ServerKeypair> {
// Try to load.
let existing = sqlx::query_as::<_, (String, String)>(
"SELECT public_key_pem, private_key_pem FROM server_keys WHERE id = 1",
)
.fetch_optional(pool)
.await?;
if let Some((pub_pem, priv_pem)) = existing {
let signing = SigningKey::from_pkcs8_pem(&priv_pem)
.context("failed to parse stored private key")?;
let verifying = VerifyingKey::from_public_key_pem(&pub_pem)
.context("failed to parse stored public key")?;
return Ok(ServerKeypair {
signing,
verifying,
public_key_pem: pub_pem,
});
}
// Generate a new keypair.
let signing = SigningKey::generate(&mut OsRng);
let verifying = signing.verifying_key();
use pkcs8::LineEnding;
let priv_pem = signing
.to_pkcs8_pem(LineEnding::LF)
.context("failed to encode private key to PEM")?
.to_string();
let pub_pem = verifying
.to_public_key_pem(LineEnding::LF)
.context("failed to encode public key to PEM")?;
let now = Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO server_keys (id, algorithm, public_key_pem, private_key_pem, created_at)
VALUES (1, 'ed25519', ?, ?, ?)",
)
.bind(&pub_pem)
.bind(&priv_pem)
.bind(&now)
.execute(pool)
.await?;
tracing::info!("generated new Ed25519 server signing key");
Ok(ServerKeypair {
signing,
verifying,
public_key_pem: pub_pem,
})
}