use std::path::Path;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::Result;
#[derive(Serialize, Deserialize)]
pub struct JwkKey {
pub kty: String,
pub crv: String,
pub x: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub d: Option<String>,
}
impl JwkKey {
pub fn from_signing_key(key: &SigningKey) -> Self {
Self {
kty: "OKP".to_owned(),
crv: "Ed25519".to_owned(),
x: URL_SAFE_NO_PAD.encode(key.verifying_key().to_bytes()),
d: Some(URL_SAFE_NO_PAD.encode(key.to_bytes())),
}
}
pub fn to_public(&self) -> Self {
Self {
kty: self.kty.clone(),
crv: self.crv.clone(),
x: self.x.clone(),
d: None,
}
}
fn check_type(&self) -> Result<()> {
if self.kty != "OKP" || self.crv != "Ed25519" {
return Err("unsupported key: expected kty=OKP, crv=Ed25519".into());
}
Ok(())
}
pub fn signing_key(&self) -> Result<SigningKey> {
self.check_type()?;
let d = self
.d
.as_ref()
.ok_or("key file has no private component `d`")?;
let bytes = URL_SAFE_NO_PAD.decode(d.trim())?;
let seed: [u8; 32] = bytes.try_into().map_err(|_| "`d` is not 32 bytes")?;
let signing_key = SigningKey::from_bytes(&seed);
let advertised: [u8; 32] = URL_SAFE_NO_PAD
.decode(self.x.trim())?
.try_into()
.map_err(|_| "`x` is not 32 bytes")?;
if advertised != signing_key.verifying_key().to_bytes() {
return Err("private key `d` does not match public key `x`".into());
}
Ok(signing_key)
}
pub fn verifying_key(&self) -> Result<VerifyingKey> {
self.check_type()?;
let bytes = URL_SAFE_NO_PAD.decode(self.x.trim())?;
let public: [u8; 32] = bytes.try_into().map_err(|_| "`x` is not 32 bytes")?;
VerifyingKey::from_bytes(&public).map_err(|_| "`x` is not a valid Ed25519 key".into())
}
}
pub fn load(path: &Path) -> Result<JwkKey> {
let bytes = std::fs::read(path).map_err(|e| format!("reading {}: {e}", path.display()))?;
serde_json::from_slice(&bytes).map_err(|e| format!("parsing {}: {e}", path.display()).into())
}
pub fn to_json(key: &JwkKey) -> Result<String> {
Ok(serde_json::to_string_pretty(key)?)
}