Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/crypto/src/rustls_impl/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ pub fn pem_to_der_from_slice(pem_data: &[u8]) -> Result<Vec<u8>> {
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),
}
}
Expand Down
3 changes: 2 additions & 1 deletion tools/json-signer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
serde_json = { version = "1.0", features = ["raw_value", "preserve_order"] }
der = "0.7"
100 changes: 91 additions & 9 deletions tools/json-signer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonError> for Error {
Expand All @@ -29,29 +36,104 @@ impl From<JsonError> for Error {
}

pub fn json_sign(json_key: &str, data: &[u8], private_key: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, 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<String, &RawValue> = 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::<String>(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<Vec<u8>, 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::<Result<Vec<u8>, Error>>()
}

pub fn json_set_signature(
key: &str,
json_slice: &[u8],
signature: &[u8],
) -> Result<Vec<u8>, 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<Vec<u8>, 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)
}
94 changes: 78 additions & 16 deletions tools/json-signer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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")]
Expand All @@ -28,14 +29,26 @@ 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<PathBuf>,

/// Provide the private key to sign the JSON object.
#[arg(long, value_name = "FILE")]
private_key: Option<PathBuf>,

/// Provide the public key to verify the JSON object.
#[arg(long, value_name = "FILE")]
public_key: Option<PathBuf>,

/// Name of the JSON object to sign (e.g., "policyData")
#[arg(long, short)]
name: String,
Expand All @@ -46,48 +59,97 @@ struct Cli {

/// Where to write the generated file
#[arg(long, short, value_name = "FILE")]
output: PathBuf,
output: Option<PathBuf>,
}

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);
}
}
Expand Down
Loading