Advance forge rollout, Ethereum rails, and NBC sources
This commit is contained in:
parent
be26313225
commit
7d84510eac
88 changed files with 11230 additions and 302 deletions
|
|
@ -1,12 +1,19 @@
|
|||
//! Cryptographic helpers for every.channel.
|
||||
|
||||
use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305, XNonce};
|
||||
use ec_core::ManifestSignature;
|
||||
use ec_core::{ManifestBody, ManifestSignature};
|
||||
use ec_eth::{manifest_body_eip712_signing_hash, ETH_MANIFEST_SIG_ALG};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use k256::ecdsa::{
|
||||
RecoveryId as SecpRecoveryId, Signature as SecpSignature, SigningKey as SecpSigningKey,
|
||||
VerifyingKey as SecpVerifyingKey,
|
||||
};
|
||||
use sha3::{Digest, Keccak256};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
pub const MANIFEST_SIG_ALG: &str = "ed25519";
|
||||
pub const ETH_MANIFEST_SIGNING_KEY_ENV: &str = "EVERY_CHANNEL_ETH_MANIFEST_SIGNING_KEY";
|
||||
|
||||
pub const ENCRYPTION_ALG: &str = "xchacha20poly1305";
|
||||
|
||||
|
|
@ -83,19 +90,29 @@ pub struct ManifestKeypair {
|
|||
pub verifying_key: VerifyingKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EthereumManifestKeypair {
|
||||
pub signing_key: SecpSigningKey,
|
||||
pub verifying_key: SecpVerifyingKey,
|
||||
}
|
||||
|
||||
fn decode_env_hex_or_file(value: &str) -> Result<Vec<u8>, String> {
|
||||
let trimmed = value.trim();
|
||||
if std::path::Path::new(trimmed).exists() {
|
||||
let text = fs::read_to_string(trimmed).map_err(|err| err.to_string())?;
|
||||
hex::decode(text.trim().trim_start_matches("0x")).map_err(|err| err.to_string())
|
||||
} else {
|
||||
hex::decode(trimmed.trim_start_matches("0x")).map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_manifest_keypair_from_env() -> Result<Option<ManifestKeypair>, String> {
|
||||
let value = match env::var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY") {
|
||||
Ok(value) => value,
|
||||
Err(env::VarError::NotPresent) => return Ok(None),
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
let trimmed = value.trim();
|
||||
let key_bytes = if std::path::Path::new(trimmed).exists() {
|
||||
let text = fs::read_to_string(trimmed).map_err(|err| err.to_string())?;
|
||||
hex::decode(text.trim()).map_err(|err| err.to_string())?
|
||||
} else {
|
||||
hex::decode(trimmed).map_err(|err| err.to_string())?
|
||||
};
|
||||
let key_bytes = decode_env_hex_or_file(&value)?;
|
||||
let bytes = if key_bytes.len() == 32 {
|
||||
key_bytes
|
||||
} else if key_bytes.len() == 64 {
|
||||
|
|
@ -113,10 +130,36 @@ pub fn load_manifest_keypair_from_env() -> Result<Option<ManifestKeypair>, Strin
|
|||
}))
|
||||
}
|
||||
|
||||
pub fn load_ethereum_manifest_keypair_from_env() -> Result<Option<EthereumManifestKeypair>, String>
|
||||
{
|
||||
let value = match env::var(ETH_MANIFEST_SIGNING_KEY_ENV) {
|
||||
Ok(value) => value,
|
||||
Err(env::VarError::NotPresent) => return Ok(None),
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
let key_bytes = decode_env_hex_or_file(&value)?;
|
||||
if key_bytes.len() != 32 {
|
||||
return Err("ethereum manifest signing key must be exactly 32 hex bytes".to_string());
|
||||
}
|
||||
let signing_key =
|
||||
SecpSigningKey::from_bytes((&key_bytes[..]).into()).map_err(|err| err.to_string())?;
|
||||
let verifying_key = *signing_key.verifying_key();
|
||||
Ok(Some(EthereumManifestKeypair {
|
||||
signing_key,
|
||||
verifying_key,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn signer_id_from_key(key: &VerifyingKey) -> String {
|
||||
format!("ed25519:{}", hex::encode(key.to_bytes()))
|
||||
}
|
||||
|
||||
pub fn ethereum_signer_id_from_key(key: &SecpVerifyingKey) -> String {
|
||||
let encoded = key.to_encoded_point(false);
|
||||
let digest = Keccak256::digest(&encoded.as_bytes()[1..]);
|
||||
format!("eth:0x{}", hex::encode(&digest[12..]))
|
||||
}
|
||||
|
||||
pub fn sign_manifest_id(manifest_id: &str, keypair: &ManifestKeypair) -> ManifestSignature {
|
||||
let signature: Signature = keypair.signing_key.sign(manifest_id.as_bytes());
|
||||
ManifestSignature {
|
||||
|
|
@ -126,6 +169,25 @@ pub fn sign_manifest_id(manifest_id: &str, keypair: &ManifestKeypair) -> Manifes
|
|||
}
|
||||
}
|
||||
|
||||
pub fn sign_manifest_body_eip712(
|
||||
body: &ManifestBody,
|
||||
keypair: &EthereumManifestKeypair,
|
||||
) -> Result<ManifestSignature, String> {
|
||||
let digest = manifest_body_eip712_signing_hash(body).map_err(|err| err.to_string())?;
|
||||
let (signature, recovery_id) = keypair
|
||||
.signing_key
|
||||
.sign_prehash_recoverable(digest.as_slice())
|
||||
.map_err(|err| err.to_string())?;
|
||||
let mut bytes = Vec::with_capacity(65);
|
||||
bytes.extend_from_slice(&signature.to_bytes());
|
||||
bytes.push(recovery_id.to_byte());
|
||||
Ok(ManifestSignature {
|
||||
signer_id: ethereum_signer_id_from_key(&keypair.verifying_key),
|
||||
alg: ETH_MANIFEST_SIG_ALG.to_string(),
|
||||
signature: hex::encode(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verify_manifest_signature(manifest_id: &str, sig: &ManifestSignature) -> bool {
|
||||
if sig.alg != MANIFEST_SIG_ALG {
|
||||
return false;
|
||||
|
|
@ -156,6 +218,57 @@ pub fn verify_manifest_signature(manifest_id: &str, sig: &ManifestSignature) ->
|
|||
.is_ok()
|
||||
}
|
||||
|
||||
fn normalize_eth_signer_id(value: &str) -> Option<String> {
|
||||
let trimmed = value
|
||||
.strip_prefix("eth:")
|
||||
.or_else(|| value.strip_prefix("ETH:"))
|
||||
.unwrap_or(value)
|
||||
.trim();
|
||||
let address = trimmed.trim_start_matches("0x");
|
||||
if address.len() != 40 || !address.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
Some(format!("eth:0x{}", address.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
pub fn verify_manifest_body_eip712_signature(body: &ManifestBody, sig: &ManifestSignature) -> bool {
|
||||
if sig.alg != ETH_MANIFEST_SIG_ALG {
|
||||
return false;
|
||||
}
|
||||
let Some(expected_signer_id) = normalize_eth_signer_id(&sig.signer_id) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(sig_bytes) = hex::decode(sig.signature.trim().trim_start_matches("0x")) else {
|
||||
return false;
|
||||
};
|
||||
if sig_bytes.len() != 65 {
|
||||
return false;
|
||||
}
|
||||
let Ok(signature) = SecpSignature::from_slice(&sig_bytes[..64]) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(recovery_id) = SecpRecoveryId::try_from(sig_bytes[64]) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(digest) = manifest_body_eip712_signing_hash(body) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(verifying_key) =
|
||||
SecpVerifyingKey::recover_from_prehash(digest.as_slice(), &signature, recovery_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
ethereum_signer_id_from_key(&verifying_key) == expected_signer_id
|
||||
}
|
||||
|
||||
pub fn verify_manifest_signature_with_body(
|
||||
manifest_id: &str,
|
||||
body: &ManifestBody,
|
||||
sig: &ManifestSignature,
|
||||
) -> bool {
|
||||
verify_manifest_signature(manifest_id, sig) || verify_manifest_body_eip712_signature(body, sig)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -212,6 +325,35 @@ mod tests {
|
|||
assert!(!verify_manifest_signature("evil", &sig));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ethereum_manifest_sign_verify_roundtrip() {
|
||||
let body = ManifestBody {
|
||||
stream_id: ec_core::StreamId("stream".to_string()),
|
||||
epoch_id: "epoch-1".to_string(),
|
||||
chunk_duration_ms: 2000,
|
||||
total_chunks: 1,
|
||||
chunk_start_index: 0,
|
||||
encoder_profile_id: "p".to_string(),
|
||||
merkle_root: "11".repeat(32),
|
||||
created_unix_ms: 1,
|
||||
metadata: Vec::new(),
|
||||
chunk_hashes: vec!["22".repeat(32)],
|
||||
variants: None,
|
||||
};
|
||||
let secret = [7u8; 32];
|
||||
let signing_key = SecpSigningKey::from_bytes((&secret).into()).unwrap();
|
||||
let verifying_key = *signing_key.verifying_key();
|
||||
let keypair = EthereumManifestKeypair {
|
||||
signing_key,
|
||||
verifying_key,
|
||||
};
|
||||
let sig = sign_manifest_body_eip712(&body, &keypair).unwrap();
|
||||
assert!(verify_manifest_body_eip712_signature(&body, &sig));
|
||||
let mut tampered = body.clone();
|
||||
tampered.created_unix_ms = 2;
|
||||
assert!(!verify_manifest_body_eip712_signature(&tampered, &sig));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_keypair_from_env_hex() {
|
||||
let prev = env::var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY").ok();
|
||||
|
|
@ -224,4 +366,17 @@ mod tests {
|
|||
None => env::remove_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_ethereum_keypair_from_env_hex() {
|
||||
let prev = env::var(ETH_MANIFEST_SIGNING_KEY_ENV).ok();
|
||||
env::set_var(ETH_MANIFEST_SIGNING_KEY_ENV, "01".repeat(32));
|
||||
let loaded = load_ethereum_manifest_keypair_from_env().unwrap().unwrap();
|
||||
let id = ethereum_signer_id_from_key(&loaded.verifying_key);
|
||||
assert!(id.starts_with("eth:0x"));
|
||||
match prev {
|
||||
Some(value) => env::set_var(ETH_MANIFEST_SIGNING_KEY_ENV, value),
|
||||
None => env::remove_var(ETH_MANIFEST_SIGNING_KEY_ENV),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue