From b2a3c162c2b7922a4262b3e29107f9652f13398d Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 27 Nov 2025 07:43:27 +0000 Subject: [PATCH] json-signer: Support detach signing and detach/non-detach verification Signed-off-by: Ming-Wei Shih --- Cargo.lock | 1 + src/crypto/src/rustls_impl/ecdsa.rs | 1 + tools/json-signer/Cargo.toml | 3 +- tools/json-signer/src/lib.rs | 100 +++++++++++++++++++++++++--- tools/json-signer/src/main.rs | 94 +++++++++++++++++++++----- 5 files changed, 173 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c69da71e..932f668e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,6 +1046,7 @@ dependencies = [ "anyhow", "clap", "crypto", + "der", "serde", "serde_json", ] diff --git a/src/crypto/src/rustls_impl/ecdsa.rs b/src/crypto/src/rustls_impl/ecdsa.rs index b00c1087..4271e257 100644 --- a/src/crypto/src/rustls_impl/ecdsa.rs +++ b/src/crypto/src/rustls_impl/ecdsa.rs @@ -130,6 +130,7 @@ pub fn pem_to_der_from_slice(pem_data: &[u8]) -> Result> { Some((Item::Pkcs8Key(key), _)) => Ok(key.secret_pkcs8_der().to_vec()), Some((Item::Pkcs1Key(key), _)) => Ok(key.secret_pkcs1_der().to_vec()), Some((Item::Sec1Key(key), _)) => Ok(key.secret_sec1_der().to_vec()), + Some((Item::SubjectPublicKeyInfo(spki), _)) => Ok(spki.to_vec()), _ => Err(Error::DecodePemCert), } } diff --git a/tools/json-signer/Cargo.toml b/tools/json-signer/Cargo.toml index 11ee5b77..289262a4 100644 --- a/tools/json-signer/Cargo.toml +++ b/tools/json-signer/Cargo.toml @@ -8,4 +8,5 @@ anyhow = "1.0" clap = { version = "4.0", features = ["derive"] } crypto = { path = "../../src/crypto" } serde = { version = "1.0", features = ["derive"]} -serde_json = { version = "1.0", features = ["raw_value", "preserve_order"] } \ No newline at end of file +serde_json = { version = "1.0", features = ["raw_value", "preserve_order"] } +der = "0.7" \ No newline at end of file diff --git a/tools/json-signer/src/lib.rs b/tools/json-signer/src/lib.rs index cd2d742c..5c625025 100644 --- a/tools/json-signer/src/lib.rs +++ b/tools/json-signer/src/lib.rs @@ -7,19 +7,26 @@ extern crate alloc; use alloc::{ - format, - string::{String, ToString}, - vec::Vec, + boxed::Box, collections::BTreeMap, format, string::String, string::ToString, vec::Vec, }; use core::result::Result; +use crypto::x509::SubjectPublicKeyInfo; +use der::Decode; pub use serde_json::Error as JsonError; -use serde_json::{Map, Value}; +use serde_json::{value::RawValue, Map, Value}; + +const SIGNATURE_KEY: &str = "signature"; #[derive(Debug)] pub enum Error { InvalidJson(JsonError), InvalidKey, + InvalidString, + InvalidSignedJson, Sign, + Verify, + NotCanonical, + NoPublicKey, } impl From for Error { @@ -29,29 +36,104 @@ impl From for Error { } pub fn json_sign(json_key: &str, data: &[u8], private_key: &[u8]) -> Result, Error> { + let value: Value = serde_json::from_slice(data)?; + let canonical_data = serde_json::to_vec(&value)?; + + if data != canonical_data { + return Err(Error::NotCanonical); + } + let private_key_der = - crypto::ecdsa::pem_to_der_from_slice(&private_key).map_err(|_| Error::InvalidKey)?; - let signature = crypto::ecdsa::ecdsa_sign(&private_key_der, &data).map_err(|_| Error::Sign)?; + crypto::ecdsa::pem_to_der_from_slice(private_key).map_err(|_| Error::InvalidKey)?; + let signature = crypto::ecdsa::ecdsa_sign(&private_key_der, data).map_err(|_| Error::Sign)?; + json_set_signature(json_key, data, &signature) } +pub fn json_sign_detached(data: &[u8], private_key: &[u8]) -> Result, Error> { + let private_key_der = + crypto::ecdsa::pem_to_der_from_slice(private_key).map_err(|_| Error::InvalidKey)?; + let signature = crypto::ecdsa::ecdsa_sign(&private_key_der, data).map_err(|_| Error::Sign)?; + + Ok(signature.to_vec()) +} + +pub fn json_verify(data: &[u8], public_key: &[u8], signature: &[u8]) -> Result<(), Error> { + let public_key_der = + crypto::ecdsa::pem_to_der_from_slice(public_key).map_err(|_| Error::InvalidKey)?; + let public_key_raw = extract_public_key_bytes(&public_key_der)?; + + crypto::ecdsa::ecdsa_verify(&public_key_raw, data, signature).map_err(|_| Error::Verify) +} + +pub fn json_verify_from_signed( + json_key: &str, + signed_json: &[u8], + public_key: &[u8], +) -> Result<(), Error> { + let public_key_der = + crypto::ecdsa::pem_to_der_from_slice(public_key).map_err(|_| Error::InvalidKey)?; + let public_key_raw = extract_public_key_bytes(&public_key_der)?; + let parsed: BTreeMap = serde_json::from_slice(signed_json)?; + + // Must have exactly 2 keys + if parsed.len() != 2 { + return Err(Error::InvalidSignedJson); + } + + if !parsed.contains_key(SIGNATURE_KEY) || !parsed.contains_key(json_key) { + return Err(Error::InvalidSignedJson); + } + + let sig_hex = parsed + .get(SIGNATURE_KEY) + .and_then(|v| serde_json::from_str::(v.get()).ok()) + .ok_or(Error::InvalidSignedJson)?; + let signature = hex_string_to_bytes(&sig_hex)?; + let data_bytes = parsed + .get(json_key) + .ok_or(Error::InvalidSignedJson)? + .get() + .as_bytes(); + + crypto::ecdsa::ecdsa_verify(&public_key_raw, data_bytes, &signature).map_err(|_| Error::Verify) +} + +fn hex_string_to_bytes(s: &str) -> Result, Error> { + if s.len() % 2 != 0 { + return Err(Error::InvalidString); + } + + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| Error::InvalidString)) + .collect::, Error>>() +} + pub fn json_set_signature( key: &str, json_slice: &[u8], signature: &[u8], ) -> Result, Error> { let val: Value = serde_json::from_slice(json_slice)?; - let sig_hex = bytes_to_hex_string(signature); let mut map = Map::new(); map.insert(key.to_string(), val); - map.insert("signature".to_string(), Value::String(sig_hex)); + map.insert(SIGNATURE_KEY.to_string(), Value::String(sig_hex)); let output = serde_json::to_vec(&map)?; Ok(output) } fn bytes_to_hex_string(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{:02X}", b)).collect() + bytes.iter().map(|b| format!("{b:02X}")).collect() +} + +fn extract_public_key_bytes(spki_der: &[u8]) -> Result, Error> { + let spki = SubjectPublicKeyInfo::from_der(spki_der).map_err(|_| Error::InvalidKey)?; + spki.subject_public_key + .as_bytes() + .map(|bytes| bytes.to_vec()) + .ok_or(Error::NoPublicKey) } diff --git a/tools/json-signer/src/main.rs b/tools/json-signer/src/main.rs index 1112897f..2c8a389e 100644 --- a/tools/json-signer/src/main.rs +++ b/tools/json-signer/src/main.rs @@ -4,7 +4,9 @@ use anyhow::{Context, Result}; use clap::Parser; -use json_signer::{json_set_signature, json_sign}; +use json_signer::{ + json_set_signature, json_sign, json_sign_detached, json_verify, json_verify_from_signed, +}; use std::{ fs, path::{Path, PathBuf}, @@ -18,7 +20,6 @@ use std::{ about = "Sign a JSON file or package a provided signature.", propagate_version = true )] - struct Cli { /// Finalize the JSON object by embedding the provided signature (requires --signature) #[arg(long, requires = "signature")] @@ -28,7 +29,15 @@ struct Cli { #[arg(long, requires = "private_key")] sign: bool, - /// Provide a signature file to finalize the JSON object. + /// Verify the signature of a JSON object (requires --public-key) + #[arg(long, requires = "public_key")] + verify: bool, + + /// For --sign: output only the signature. For --verify: use detached signature file (requires --signature) + #[arg(long)] + detach: bool, + + /// Provide a signature file to finalize or verify (with --detach) the JSON object. #[arg(long, value_name = "FILE")] signature: Option, @@ -36,6 +45,10 @@ struct Cli { #[arg(long, value_name = "FILE")] private_key: Option, + /// Provide the public key to verify the JSON object. + #[arg(long, value_name = "FILE")] + public_key: Option, + /// Name of the JSON object to sign (e.g., "policyData") #[arg(long, short)] name: String, @@ -46,48 +59,97 @@ struct Cli { /// Where to write the generated file #[arg(long, short, value_name = "FILE")] - output: PathBuf, + output: Option, } fn main() { let cli = Cli::parse(); let input = read_file(&cli.input).unwrap_or_else(|e| { - eprintln!("Failed to read input file: {}", e); + eprintln!("Failed to read input file: {e}"); exit(1); }); - if cli.sign { + if cli.verify { + let public_key = read_file(&cli.public_key.unwrap()).unwrap_or_else(|e| { + eprintln!("Failed to read public key file: {e}"); + exit(1); + }); + + let result = if cli.detach { + // Verify with detached signature + if cli.signature.is_none() { + eprintln!("--signature is required when using --verify --detach"); + exit(1); + } + let signature = read_file(&cli.signature.unwrap()).unwrap_or_else(|e| { + eprintln!("Failed to read signature file: {e}"); + exit(1); + }); + json_verify(&input, &public_key, &signature) + } else { + // Verify from signed JSON (input contains both data and signature) + json_verify_from_signed(&cli.name, &input, &public_key) + }; + + match result { + Ok(_) => { + println!("Signature verification succeeded"); + exit(0); + } + Err(e) => { + eprintln!("Signature verification failed: {e:?}"); + exit(1); + } + } + } else if cli.sign { // clap guarantees private_key present let private_key = read_file(&cli.private_key.unwrap()).unwrap_or_else(|e| { - eprintln!("Failed to read private key file: {}", e); + eprintln!("Failed to read private key file: {e}"); exit(1); }); - let output_bytes = json_sign(&cli.name, &input, &private_key).unwrap_or_else(|e| { - eprintln!("Failed to sign input json: {:?}", e); + let output_bytes = if cli.detach { + json_sign_detached(&input, &private_key).unwrap_or_else(|e| { + eprintln!("Failed to sign input json: {e:?}"); + exit(1); + }) + } else { + json_sign(&cli.name, &input, &private_key).unwrap_or_else(|e| { + eprintln!("Failed to sign input json: {e:?}"); + exit(1); + }) + }; + + let output = cli.output.unwrap_or_else(|| { + eprintln!("Output file is required for sign operation"); exit(1); }); - if let Err(e) = fs::write(&cli.output, output_bytes) { - eprintln!("Failed to write output file: {}", e); + if let Err(e) = fs::write(&output, output_bytes) { + eprintln!("Failed to write output file: {e}"); exit(1); } } else if cli.finalize { // clap guarantees signature present let signature = read_file(&cli.signature.unwrap()).unwrap_or_else(|e| { - eprintln!("Failed to read signature file: {}", e); + eprintln!("Failed to read signature file: {e}"); exit(1); }); let output_bytes = json_set_signature(&cli.name, &input, &signature).unwrap_or_else(|e| { - eprintln!("Failed to sign input json: {:?}", e); + eprintln!("Failed to finalize input json: {e:?}"); + exit(1); + }); + + let output = cli.output.unwrap_or_else(|| { + eprintln!("Output file is required for finalize operation"); exit(1); }); - if let Err(e) = fs::write(&cli.output, output_bytes) { - eprintln!("Failed to write output file: {}", e); + if let Err(e) = fs::write(&output, output_bytes) { + eprintln!("Failed to write output file: {e}"); exit(1); } } else { - eprintln!("Either --finalize or --sign must be specified"); + eprintln!("One of --verify, --sign, or --finalize must be specified"); exit(1); } }