Files
ODIAN_Launcher/crates/odian-packs/src/signing.rs
T
Kelorastus b7f3061be3 ODIAN Launcher: hub, installer, local stack and remembered sign-in (M2)
- Tauri 2 + Svelte 5 hub in ODIAN dark: patch notes and roadmap, install and
  update progress, local stack status, settings with DHP and MCP addresses.
- crates/odian-packs: Rust installer for pack format v1 (signed manifests,
  resumable verified downloads from a DHP or an ODIAN drive), byte-compatible
  with ODIAN_MCP_Server's Python odian_packs.
- Local stack on 127.0.0.1: Postgres -> Engine API -> tile server -> Odian.exe,
  one Job Object, clean shutdown; database snapshot restore on a fresh PC only.
- Keycloak sign-in through the DHP's MCP, remembered until Sign out with a
  launcher session that renews the Engine token silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:23:17 +02:00

53 lines
2.4 KiB
Rust

//! Ed25519 release signatures (verification only; the Python module `odian_packs.signing`).
//!
//! Key text: `ed25519:<base64 of the raw 32 bytes>`. Signature: Ed25519 over
//! [`crate::manifest::signing_payload`], written as `ed25519:<base64 of 64 bytes>`.
use base64::alphabet;
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
use base64::Engine;
use ed25519_dalek::{Signature, VerifyingKey};
use serde_json::Value;
use crate::error::{Error, IntegrityKind, Result};
use crate::manifest::signing_payload;
/// Python's `base64.b64decode(text, validate=True)`: standard alphabet, correct padding,
/// no whitespace, and (like CPython) non-zero trailing bits tolerated.
const B64: GeneralPurpose = GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new()
.with_decode_allow_trailing_bits(true)
.with_decode_padding_mode(DecodePaddingMode::RequireCanonical),
);
fn signature_error(detail: &str) -> Error {
Error::integrity(IntegrityKind::Signature, "manifest", detail)
}
/// Parse `ed25519:<base64>` (surrounding whitespace ignored).
pub fn load_public_key(text: &str) -> Result<VerifyingKey> {
let body = text
.trim()
.strip_prefix("ed25519:")
.ok_or_else(|| signature_error("public key must look like 'ed25519:<base64>'"))?;
let raw = B64.decode(body).map_err(|_| signature_error("public key is not valid base64"))?;
let bytes: [u8; 32] = raw.try_into().map_err(|_| signature_error("an Ed25519 public key is 32 bytes"))?;
VerifyingKey::from_bytes(&bytes).map_err(|_| signature_error("public key is not a valid Ed25519 point"))
}
/// Error ([`IntegrityKind::Signature`]) unless `manifest` carries a valid signature by that key.
pub fn verify(manifest: &Value, public_key: &str) -> Result<()> {
let body = manifest
.get("signature")
.and_then(Value::as_str)
.and_then(|s| s.strip_prefix("ed25519:"))
.ok_or_else(|| signature_error("manifest is not signed"))?;
let raw = B64.decode(body).map_err(|_| signature_error("signature is not valid base64"))?;
let key = load_public_key(public_key)?;
let payload = signing_payload(manifest)?;
let mismatch = || signature_error("signature does not match: manifest altered, or signed by another key");
let signature = Signature::from_slice(&raw).map_err(|_| mismatch())?;
key.verify_strict(&payload, &signature).map_err(|_| mismatch())
}