use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::{
DirectoryUpdateManifest, Error, ManifestSignature, Result, MANIFEST_SIGNATURE_ALGORITHM,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateKeypair {
pub private_key: String,
pub public_key: String,
pub key_id: String,
}
pub fn generate_update_keypair() -> Result<UpdateKeypair> {
let mut secret = [0_u8; 32];
getrandom::fill(&mut secret).map_err(|error| Error::InvalidKeyMaterial {
kind: "random seed".to_string(),
message: error.to_string(),
})?;
let signing_key = SigningKey::from_bytes(&secret);
let public_key = signing_key.verifying_key().to_bytes();
Ok(UpdateKeypair {
private_key: STANDARD.encode(secret),
public_key: STANDARD.encode(public_key),
key_id: key_id(&public_key),
})
}
pub fn sign_directory_manifest(
manifest: &mut DirectoryUpdateManifest,
private_key: &str,
signature_key_id: Option<String>,
) -> Result<ManifestSignature> {
let signature = sign_payload(&manifest.signing_payload()?, private_key, signature_key_id)?;
manifest.signature = Some(signature.clone());
Ok(signature)
}
pub fn verify_directory_manifest_signature(
manifest: &DirectoryUpdateManifest,
public_key: &str,
) -> Result<()> {
verify_directory_manifest_signature_with_keys(manifest, &[public_key.to_string()])
}
pub fn verify_directory_manifest_signature_with_keys(
manifest: &DirectoryUpdateManifest,
public_keys: &[String],
) -> Result<()> {
if public_keys.is_empty() {
return Err(Error::SignaturePublicKeyMissing);
}
let signature = manifest.signature.as_ref().ok_or(Error::SignatureMissing)?;
let payload = manifest.signing_payload()?;
let mut ordered = public_keys.iter().collect::<Vec<_>>();
if let Some(expected_key_id) = signature.key_id.as_deref() {
ordered.sort_by_key(|public_key| {
public_key_id(public_key)
.map(|key_id| key_id != expected_key_id)
.unwrap_or(true)
});
}
let mut last_error = None;
for public_key in ordered {
match verify_payload(&payload, signature, public_key) {
Ok(()) => return Ok(()),
Err(error) => last_error = Some(error),
}
}
Err(last_error.unwrap_or(Error::SignatureVerificationFailed))
}
pub fn public_key_id(public_key: &str) -> Result<String> {
Ok(key_id(&verifying_key_from_base64(public_key)?.to_bytes()))
}
fn sign_payload(
payload: &[u8],
private_key: &str,
signature_key_id: Option<String>,
) -> Result<ManifestSignature> {
let signing_key = signing_key_from_base64(private_key)?;
let signature: Signature = signing_key.sign(payload);
let signing_key_id =
signature_key_id.unwrap_or_else(|| key_id(&signing_key.verifying_key().to_bytes()));
Ok(ManifestSignature {
algorithm: MANIFEST_SIGNATURE_ALGORITHM.to_string(),
key_id: Some(signing_key_id),
value: STANDARD.encode(signature.to_bytes()),
})
}
fn verify_payload(payload: &[u8], signature: &ManifestSignature, public_key: &str) -> Result<()> {
if signature.algorithm != MANIFEST_SIGNATURE_ALGORITHM {
return Err(Error::UnsupportedAlgorithm(signature.algorithm.clone()));
}
let verifying_key = verifying_key_from_base64(public_key)?;
let signature_bytes = decode_exact::<64>(&signature.value, "manifest signature")?;
let signature = Signature::from_bytes(&signature_bytes);
verifying_key
.verify_strict(payload, &signature)
.map_err(|_| Error::SignatureVerificationFailed)
}
pub fn key_id(public_key: &[u8; 32]) -> String {
let digest = crate::sha256_bytes(public_key);
digest[..16].to_string()
}
fn signing_key_from_base64(private_key: &str) -> Result<SigningKey> {
let bytes = decode_exact::<32>(private_key, "private key")?;
Ok(SigningKey::from_bytes(&bytes))
}
fn verifying_key_from_base64(public_key: &str) -> Result<VerifyingKey> {
let bytes = decode_exact::<32>(public_key, "public key")?;
VerifyingKey::from_bytes(&bytes).map_err(|error| Error::InvalidKeyMaterial {
kind: "public key".to_string(),
message: error.to_string(),
})
}
fn decode_exact<const N: usize>(value: &str, kind: &str) -> Result<[u8; N]> {
let bytes = STANDARD
.decode(value.trim())
.map_err(|error| Error::InvalidKeyMaterial {
kind: kind.to_string(),
message: error.to_string(),
})?;
bytes
.try_into()
.map_err(|bytes: Vec<u8>| Error::InvalidKeyMaterial {
kind: kind.to_string(),
message: format!("expected {N} bytes, got {}", bytes.len()),
})
}