use std::fmt;
use ed25519_dalek::Signature;
use ed25519_dalek::Signer;
use ed25519_dalek::SigningKey;
use ed25519_dalek::VerifyingKey;
use serde::Deserialize;
use serde::Serialize;
use crate::CodePushError;
use crate::manifest::BundleManifest;
const SIGNING_CONTEXT: &[u8] = b"leptoskit-codepush-release-v1:";
fn signed_message(manifest: &BundleManifest) -> Vec<u8> {
let hash = manifest.bundle_hash();
let mut message = Vec::with_capacity(SIGNING_CONTEXT.len() + hash.len());
message.extend_from_slice(SIGNING_CONTEXT);
message.extend_from_slice(hash.as_bytes());
message
}
pub(crate) fn decode_hex<const N: usize>(
what: &str,
encoded: &str,
) -> Result<[u8; N], CodePushError> {
let mut bytes = [0u8; N];
hex::decode_to_slice(encoded.trim(), &mut bytes).map_err(|_| {
CodePushError::InvalidKey(format!("{what} must be {} hex characters", N * 2))
})?;
Ok(bytes)
}
pub struct ReleaseSigningKey(SigningKey);
impl ReleaseSigningKey {
pub fn from_seed(seed: &[u8; 32]) -> Self {
Self(SigningKey::from_bytes(seed))
}
pub fn from_hex(encoded: &str) -> Result<Self, CodePushError> {
Ok(Self::from_seed(&decode_hex::<32>(
"a signing key",
encoded,
)?))
}
pub fn to_hex(&self) -> String {
hex::encode(self.0.as_bytes())
}
pub fn public_key(&self) -> ReleasePublicKey {
ReleasePublicKey(self.0.verifying_key())
}
pub fn sign(&self, manifest: &BundleManifest) -> ReleaseSignature {
let signature = self.0.sign(&signed_message(manifest));
ReleaseSignature {
public_key: self.public_key().to_hex(),
signature: hex::encode(signature.to_bytes()),
}
}
pub(crate) fn sign_message(&self, message: &[u8]) -> [u8; 64] {
self.0.sign(message).to_bytes()
}
}
impl fmt::Debug for ReleaseSigningKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ReleaseSigningKey")
.field("public_key", &self.public_key().to_hex())
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ReleasePublicKey(VerifyingKey);
impl ReleasePublicKey {
pub fn from_hex(encoded: &str) -> Result<Self, CodePushError> {
let bytes = decode_hex::<32>("a public key", encoded)?;
let key = VerifyingKey::from_bytes(&bytes)
.map_err(|error| CodePushError::InvalidKey(format!("invalid public key: {error}")))?;
if key.is_weak() {
return Err(CodePushError::InvalidKey(
"the public key is a weak, small-order point".to_owned(),
));
}
Ok(Self(key))
}
pub fn to_hex(&self) -> String {
hex::encode(self.0.as_bytes())
}
pub(crate) fn verifies(&self, message: &[u8], signature: &[u8; 64]) -> bool {
self.0
.verify_strict(message, &Signature::from_bytes(signature))
.is_ok()
}
}
impl fmt::Debug for ReleasePublicKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("ReleasePublicKey")
.field(&self.to_hex())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReleaseSignature {
pub public_key: String,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustedKeys(Vec<ReleasePublicKey>);
impl TrustedKeys {
pub fn new(keys: impl IntoIterator<Item = ReleasePublicKey>) -> Result<Self, CodePushError> {
let keys: Vec<_> = keys.into_iter().collect();
if keys.is_empty() {
return Err(CodePushError::InvalidKey(
"at least one trusted public key is required".to_owned(),
));
}
Ok(Self(keys))
}
pub fn from_hex<S: AsRef<str>>(
keys: impl IntoIterator<Item = S>,
) -> Result<Self, CodePushError> {
let keys = keys
.into_iter()
.map(|key| ReleasePublicKey::from_hex(key.as_ref()))
.collect::<Result<Vec<_>, _>>()?;
Self::new(keys)
}
pub fn contains(&self, key: &ReleasePublicKey) -> bool {
self.0.contains(key)
}
pub fn iter(&self) -> impl Iterator<Item = &ReleasePublicKey> {
self.0.iter()
}
pub fn verify(
&self,
manifest: &BundleManifest,
signature: Option<&ReleaseSignature>,
) -> Result<(), CodePushError> {
let release = || {
format!(
"{}/{}/{}+{}",
manifest.app, manifest.channel, manifest.version, manifest.build
)
};
let signature = signature.ok_or_else(|| CodePushError::Unsigned(release()))?;
let key = ReleasePublicKey::from_hex(&signature.public_key)
.ok()
.filter(|key| self.contains(key))
.ok_or_else(|| CodePushError::UntrustedKey(signature.public_key.clone()))?;
let bytes = decode_hex::<64>("a signature", &signature.signature)
.map_err(|_| CodePushError::BadSignature(release()))?;
key.0
.verify_strict(&signed_message(manifest), &Signature::from_bytes(&bytes))
.map_err(|_| CodePushError::BadSignature(release()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::MANIFEST_FORMAT_VERSION;
fn manifest() -> BundleManifest {
BundleManifest {
format_version: MANIFEST_FORMAT_VERSION,
app: "app".to_owned(),
channel: "production".to_owned(),
version: semver::Version::new(1, 0, 0),
build: 1,
entry: "app.wasm".to_owned(),
files: Vec::new(),
created_unix: 0,
mandatory: false,
}
}
#[test]
fn keys_round_trip_through_hex() {
let key = ReleaseSigningKey::from_seed(&[7; 32]);
let parsed = ReleaseSigningKey::from_hex(&format!("{}\n", key.to_hex())).expect("parse");
assert_eq!(parsed.public_key(), key.public_key());
assert_eq!(
ReleasePublicKey::from_hex(&key.public_key().to_hex()).expect("parse"),
key.public_key()
);
}
#[test]
fn debug_output_never_contains_the_secret() {
let key = ReleaseSigningKey::from_seed(&[7; 32]);
assert!(!format!("{key:?}").contains(&key.to_hex()));
}
#[test]
fn rejects_malformed_and_weak_public_keys() {
for encoded in ["", "zz", &"00".repeat(31), &"00".repeat(33)] {
assert!(
matches!(
ReleasePublicKey::from_hex(encoded),
Err(CodePushError::InvalidKey(_))
),
"{encoded:?} must be rejected"
);
}
let mut identity = [0u8; 32];
identity[0] = 1;
assert!(matches!(
ReleasePublicKey::from_hex(&hex::encode(identity)),
Err(CodePushError::InvalidKey(_))
));
}
#[test]
fn an_empty_trust_set_is_rejected() {
assert!(matches!(
TrustedKeys::new([]),
Err(CodePushError::InvalidKey(_))
));
}
#[test]
fn verifies_against_any_trusted_key() {
let old = ReleaseSigningKey::from_seed(&[1; 32]);
let new = ReleaseSigningKey::from_seed(&[2; 32]);
let trusted = TrustedKeys::new([old.public_key(), new.public_key()]).expect("keys");
let manifest = manifest();
trusted
.verify(&manifest, Some(&old.sign(&manifest)))
.expect("old key verifies");
trusted
.verify(&manifest, Some(&new.sign(&manifest)))
.expect("new key verifies");
}
#[test]
fn a_signature_does_not_cover_a_changed_manifest() {
let key = ReleaseSigningKey::from_seed(&[1; 32]);
let trusted = TrustedKeys::new([key.public_key()]).expect("keys");
let signature = key.sign(&manifest());
let mut changed = manifest();
changed.mandatory = true;
assert!(matches!(
trusted.verify(&changed, Some(&signature)),
Err(CodePushError::BadSignature(_))
));
}
}