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:
@@ -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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user