b7f3061be3
- 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>
53 lines
2.4 KiB
Rust
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())
|
|
}
|