refactor(core,wasm): zeroized ed25519 seed decode in core; org_unwrap_key reuses it

Review fixes for Task 1 (org_unwrap_key). Public signature unchanged.

- Add device::extract_ed25519_seed -> Zeroizing<[u8;32]>: length-checks the
  slice then copy_from_slice straight into the Zeroizing buffer, so the raw
  seed never lands in a plain [u8;32] (Important #1).
- Refactor device::sign() to call the new helper (pure refactor; device/sign
  roundtrip tests unchanged and green) (Important #2).
- org_unwrap_key now calls relicario_core::device::extract_ed25519_seed
  directly; drop the duplicated local decode_device_seed helper and remove the
  redundant ssh-key dep from relicario-wasm (core already pins it) (Important #2).
- Device-key-form finding (OpenSSH PEM, not base64; spec name matches reality)
  moved onto the core helper, condensed copy kept on org_unwrap_key.
- Test helper keeps the private key as Zeroizing<String> and passes &priv
  (derefs to &str) — no plain-String copy (Minor #3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
This commit is contained in:
adlee-was-taken
2026-06-25 21:13:49 -04:00
parent d55861ffd8
commit 92febf38f1
5 changed files with 44 additions and 47 deletions

View File

@@ -54,10 +54,20 @@ pub fn generate_keypair() -> Result<(Zeroizing<String>, String)> {
Ok((Zeroizing::new(private_pem.to_string()), public_line))
}
/// Sign data with an OpenSSH private key, returning base64 signature.
pub fn sign(private_key_openssh: &str, data: &[u8]) -> Result<String> {
use base64::Engine;
/// Extract the raw 32-byte ed25519 seed from an OpenSSH PEM private key.
///
/// **Device-key form (verified against the producers):** `generate_keypair()`
/// emits the private key as an OpenSSH PEM blob — the multiline
/// `-----BEGIN OPENSSH PRIVATE KEY-----...` form produced by
/// `ssh_key::PrivateKey::to_openssh()` — NOT a base64-encoded raw-bytes string.
/// The org plan's `device_private_key_base64` parameter name was therefore
/// misleading; the design spec's `device_private_openssh` matches reality. The
/// extension SW holds this blob in WASM `DEVICE_STATE` and never exposes it to JS.
///
/// The seed bytes are copied straight into a `Zeroizing<[u8; 32]>` — no
/// intermediate plain `[u8; 32]` of secret material is ever materialized — so
/// the seed is wiped on drop.
pub fn extract_ed25519_seed(private_key_openssh: &str) -> Result<Zeroizing<[u8; 32]>> {
let private = PrivateKey::from_openssh(private_key_openssh)
.map_err(|e| RelicarioError::DeviceKey(format!("parse private key: {e}")))?;
@@ -67,11 +77,20 @@ pub fn sign(private_key_openssh: &str, data: &[u8]) -> Result<String> {
.ok_or_else(|| RelicarioError::DeviceKey("not an ed25519 key".into()))?;
let secret_slice: &[u8] = key_data.private.as_ref();
let secret_bytes: [u8; 32] = secret_slice
.try_into()
.map_err(|_| RelicarioError::DeviceKey("invalid key length".into()))?;
if secret_slice.len() != 32 {
return Err(RelicarioError::DeviceKey("invalid key length".into()));
}
let mut seed = Zeroizing::new([0u8; 32]);
seed.copy_from_slice(secret_slice);
Ok(seed)
}
let signing_key = SigningKey::from_bytes(&secret_bytes);
/// Sign data with an OpenSSH private key, returning base64 signature.
pub fn sign(private_key_openssh: &str, data: &[u8]) -> Result<String> {
use base64::Engine;
let seed = extract_ed25519_seed(private_key_openssh)?;
let signing_key = SigningKey::from_bytes(&seed);
let signature = signing_key.sign(data);
Ok(base64::engine::general_purpose::STANDARD.encode(signature.to_bytes()))
}