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>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
//! 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())
|
||||
}
|
||||
Reference in New Issue
Block a user