use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
pub(crate) const SIG_VERSION: &str = "2";
pub(crate) const HEADER_VERSION: &str = "X-WK-Sig-Ver";
pub(crate) const HEADER_TIMESTAMP: &str = "X-WK-Sig-Ts";
pub(crate) const HEADER_NONCE: &str = "X-WK-Sig-Nonce";
pub(crate) const HEADER_BUILD_VERSION: &str = "X-WK-Build-Version";
pub(crate) const HEADER_PUBKEY: &str = "X-WK-Pub";
pub(crate) const HEADER_CERT: &str = "X-WK-Cert";
pub(crate) const HEADER_SIGNATURE: &str = "X-WK-Sig";
#[derive(Debug, Clone)]
pub struct ReleaseCredential {
pub private_key_hex: String,
pub public_key_hex: String,
pub cert_hex: String,
pub version: String,
}
#[derive(Debug, Clone)]
pub struct RequestSignature {
pub timestamp: String,
pub nonce: String,
pub signature_hex: String,
}
pub(crate) fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
pub(crate) fn canonical_request(
timestamp: &str,
nonce: &str,
method: &str,
path: &str,
body_hash_hex: &str,
) -> String {
format!("WKSIG{SIG_VERSION}\n{timestamp}\n{nonce}\n{method}\n{path}\n{body_hash_hex}")
}
pub fn cert_payload(version: &str, public_key_hex: &str) -> String {
format!("WKCERT{SIG_VERSION}\n{version}\n{public_key_hex}")
}
pub(crate) fn unix_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub(crate) fn random_nonce() -> String {
use rand::RngCore;
let mut buf = [0u8; 16];
rand::thread_rng().fill_bytes(&mut buf);
hex::encode(buf)
}
fn hex_to_array<const N: usize>(hex_str: &str, what: &str) -> Result<[u8; N]> {
let bytes = hex::decode(hex_str.trim())
.map_err(|e| Error::BadRequest(format!("{what}: invalid hex: {e}")))?;
bytes
.try_into()
.map_err(|_| Error::BadRequest(format!("{what}: expected {N} bytes")))
}
impl ReleaseCredential {
fn signing_key(&self) -> Result<SigningKey> {
let seed = hex_to_array::<32>(&self.private_key_hex, "release private key")?;
Ok(SigningKey::from_bytes(&seed))
}
pub fn sign_request(&self, method: &str, path: &str, body: &[u8]) -> Result<RequestSignature> {
let signing_key = self.signing_key()?;
let timestamp = unix_secs().to_string();
let nonce = random_nonce();
let body_hash = sha256_hex(body);
let message = canonical_request(×tamp, &nonce, method, path, &body_hash);
let sig: Signature = signing_key.sign(message.as_bytes());
Ok(RequestSignature {
timestamp,
nonce,
signature_hex: hex::encode(sig.to_bytes()),
})
}
}
pub fn generate_keypair() -> (String, String) {
use rand::RngCore;
let mut seed = [0u8; 32];
rand::thread_rng().fill_bytes(&mut seed);
let signing_key = SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
(hex::encode(seed), hex::encode(verifying_key.to_bytes()))
}
pub fn generate_master() -> (String, String) {
generate_keypair()
}
pub fn issue_release_credential(
master_private_key_hex: &str,
version: &str,
) -> Result<ReleaseCredential> {
let master_seed = hex_to_array::<32>(master_private_key_hex, "master private key")?;
let master = SigningKey::from_bytes(&master_seed);
let (private_key_hex, public_key_hex) = generate_keypair();
let payload = cert_payload(version, &public_key_hex);
let cert: Signature = master.sign(payload.as_bytes());
Ok(ReleaseCredential {
private_key_hex,
public_key_hex,
cert_hex: hex::encode(cert.to_bytes()),
version: version.to_string(),
})
}
pub fn verify_cert(
master_public_key_hex: &str,
version: &str,
public_key_hex: &str,
cert_hex: &str,
) -> Result<bool> {
let master_pub = hex_to_array::<32>(master_public_key_hex, "master public key")?;
let master = VerifyingKey::from_bytes(&master_pub)
.map_err(|e| Error::BadRequest(format!("master public key: {e}")))?;
let cert_bytes = hex_to_array::<64>(cert_hex, "certificate")?;
let cert = Signature::from_bytes(&cert_bytes);
let payload = cert_payload(version, public_key_hex);
Ok(master.verify(payload.as_bytes(), &cert).is_ok())
}
pub fn verify_request(
public_key_hex: &str,
timestamp: &str,
nonce: &str,
method: &str,
path: &str,
body: &[u8],
signature_hex: &str,
) -> Result<bool> {
let pub_bytes = hex_to_array::<32>(public_key_hex, "public key")?;
let verifying_key = VerifyingKey::from_bytes(&pub_bytes)
.map_err(|e| Error::BadRequest(format!("public key: {e}")))?;
let sig_bytes = hex_to_array::<64>(signature_hex, "signature")?;
let sig = Signature::from_bytes(&sig_bytes);
let body_hash = sha256_hex(body);
let message = canonical_request(timestamp, nonce, method, path, &body_hash);
Ok(verifying_key.verify(message.as_bytes(), &sig).is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_request_layout_is_stable() {
let s = canonical_request(
"1700000000",
"deadbeef",
"POST",
"/api/voice/installs/heartbeat",
"abc123",
);
assert_eq!(
s,
"WKSIG2\n1700000000\ndeadbeef\nPOST\n/api/voice/installs/heartbeat\nabc123"
);
}
#[test]
fn cert_payload_layout_is_stable() {
let s = cert_payload("0.0.22", "ab12");
assert_eq!(s, "WKCERT2\n0.0.22\nab12");
}
#[test]
fn sha256_hex_matches_known_vector() {
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn generate_keypair_yields_distinct_32_byte_keys() {
let (priv_a, pub_a) = generate_keypair();
let (priv_b, _pub_b) = generate_keypair();
assert_eq!(priv_a.len(), 64, "32-byte private → 64 hex chars");
assert_eq!(pub_a.len(), 64, "32-byte public → 64 hex chars");
assert_ne!(priv_a, priv_b, "fresh keypairs must differ");
}
#[test]
fn full_chain_round_trips() {
let (master_priv, master_pub) = generate_master();
let cred = issue_release_credential(&master_priv, "0.0.22").unwrap();
assert!(
verify_cert(&master_pub, "0.0.22", &cred.public_key_hex, &cred.cert_hex).unwrap(),
"valid cert must verify under the master public key"
);
let body = br#"{"installId":"x"}"#;
let rs = cred
.sign_request("POST", "/api/voice/installs/heartbeat", body)
.unwrap();
assert!(
verify_request(
&cred.public_key_hex,
&rs.timestamp,
&rs.nonce,
"POST",
"/api/voice/installs/heartbeat",
body,
&rs.signature_hex,
)
.unwrap(),
"a freshly signed request must verify under its pub_v"
);
}
#[test]
fn cert_is_rejected_under_the_wrong_master() {
let (master_priv, _master_pub) = generate_master();
let (_other_priv, other_pub) = generate_master();
let cred = issue_release_credential(&master_priv, "0.0.22").unwrap();
assert!(
!verify_cert(&other_pub, "0.0.22", &cred.public_key_hex, &cred.cert_hex).unwrap(),
"cert must not verify under a foreign master key"
);
}
#[test]
fn cert_is_bound_to_its_version() {
let (master_priv, master_pub) = generate_master();
let cred = issue_release_credential(&master_priv, "0.0.22").unwrap();
assert!(
!verify_cert(&master_pub, "0.0.99", &cred.public_key_hex, &cred.cert_hex).unwrap(),
"cert must not verify for a different version"
);
}
#[test]
fn tampered_request_body_fails_verification() {
let (master_priv, _master_pub) = generate_master();
let cred = issue_release_credential(&master_priv, "0.0.22").unwrap();
let rs = cred.sign_request("POST", "/p", b"original").unwrap();
assert!(
!verify_request(
&cred.public_key_hex,
&rs.timestamp,
&rs.nonce,
"POST",
"/p",
b"tampered",
&rs.signature_hex,
)
.unwrap(),
"a body that differs from the signed one must fail"
);
}
#[test]
fn request_signed_by_one_version_fails_under_another_pubkey() {
let (master_priv, _master_pub) = generate_master();
let cred_a = issue_release_credential(&master_priv, "0.0.22").unwrap();
let cred_b = issue_release_credential(&master_priv, "0.0.23").unwrap();
let rs = cred_a.sign_request("POST", "/p", b"body").unwrap();
assert!(
!verify_request(
&cred_b.public_key_hex,
&rs.timestamp,
&rs.nonce,
"POST",
"/p",
b"body",
&rs.signature_hex,
)
.unwrap(),
"a signature must only verify under its own pub_v"
);
}
}