use std::{
fs::{self, File},
io::Write,
path::PathBuf,
};
use asn1_rs::{oid, Oid};
use c509_certificate::{
big_uint::UnwrappedBigUint,
extensions::Extensions,
issuer_sig_algo::IssuerSignatureAlgorithm,
name::{rdn::RelativeDistinguishedName, Name, NameValue},
signing::{PrivateKey, PublicKey},
subject_pub_key_algo::SubjectPubKeyAlgorithm,
tbs_cert::TbsCert,
time::Time,
};
use chrono::{DateTime, Utc};
use clap::Parser;
use hex::ToHex;
use minicbor::Decode;
use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Parser)]
#[command(version, about, long_about = None)]
enum Cli {
Generate {
#[clap(short = 'f', long)]
json_file: PathBuf,
#[clap(short, long)]
output: Option<PathBuf>,
#[clap(long)]
private_key: Option<PathBuf>,
#[clap(long)]
key_type: Option<String>,
},
Verify {
#[clap(short, long)]
file: PathBuf,
#[clap(long)]
public_key: PathBuf,
},
Decode {
#[clap(short, long)]
file: PathBuf,
#[clap(short, long)]
output: Option<PathBuf>,
},
}
impl Cli {
pub(crate) fn exec() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli {
Cli::Generate {
json_file,
output,
private_key,
key_type,
} => {
let sk = match private_key {
Some(key) => Some(PrivateKey::from_file(key)?),
None => None,
};
generate(&json_file, output, sk.as_ref(), &key_type)
},
Cli::Verify { file, public_key } => verify(&file, public_key),
Cli::Decode { file, output } => decode(&file, output),
}
}
}
#[derive(Deserialize, Serialize)]
struct C509Json {
self_signed: bool,
certificate_type: Option<u8>,
serial_number: Option<UnwrappedBigUint>,
issuer: Option<RelativeDistinguishedName>,
validity_not_before: Option<String>,
validity_not_after: Option<String>,
subject: RelativeDistinguishedName,
subject_public_key_algorithm: Option<SubjectPubKeyAlgorithm>,
subject_public_key: String,
extensions: Extensions,
issuer_signature_algorithm: Option<IssuerSignatureAlgorithm>,
#[serde(skip_deserializing)]
issuer_signature_value: Option<Vec<u8>>,
}
const ED25519: (Oid, Option<String>) = (oid!(1.3.101 .112), None);
const SELF_SIGNED_INT: u8 = 0;
fn generate(
file: &PathBuf, output: Option<PathBuf>, private_key: Option<&PrivateKey>,
key_type: &Option<String>,
) -> anyhow::Result<()> {
let data = fs::read_to_string(file)?;
let c509_json: C509Json = serde_json::from_str(&data)?;
validate_certificate_type(c509_json.self_signed, c509_json.certificate_type)?;
let serial_number = parse_serial_number(c509_json.serial_number);
let issuer = determine_issuer(
c509_json.self_signed,
c509_json.issuer,
c509_json.subject.clone(),
)?;
let not_before = parse_or_default_date(c509_json.validity_not_before, Utc::now().timestamp())?;
let not_after = parse_or_default_date(
c509_json.validity_not_after,
parse_or_default_date(Some("9999-12-31T23:59:59+00:00".to_string()), 0)?,
)?;
let public_key = parse_public_key(&c509_json.subject_public_key)?;
let key_type = get_key_type(key_type)?;
let tbs = TbsCert::new(
c509_json.certificate_type.unwrap_or(SELF_SIGNED_INT),
serial_number,
Name::new(NameValue::RelativeDistinguishedName(issuer)),
Time::new(not_before),
Time::new(not_after),
Name::new(NameValue::RelativeDistinguishedName(c509_json.subject)),
c509_json
.subject_public_key_algorithm
.unwrap_or(SubjectPubKeyAlgorithm::new(key_type.0.clone(), key_type.1)),
public_key.to_bytes(),
c509_json.extensions.clone(),
c509_json
.issuer_signature_algorithm
.unwrap_or(IssuerSignatureAlgorithm::new(key_type.0, ED25519.1)),
);
let cert = c509_certificate::generate(&tbs, private_key)?;
if let Some(output) = output {
write_to_output_file(output, &cert)?;
};
println!("Hex: {:?}", hex::encode(&cert));
println!("Bytes: {:?}", &cert);
Ok(())
}
fn write_to_output_file(output: PathBuf, data: &[u8]) -> anyhow::Result<()> {
let mut file = File::create(output).map_err(|e| anyhow::anyhow!(e))?;
file.write_all(data).map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}
fn determine_issuer(
self_signed: bool, issuer: Option<RelativeDistinguishedName>,
subject: RelativeDistinguishedName,
) -> anyhow::Result<RelativeDistinguishedName> {
if self_signed {
Ok(subject)
} else {
issuer.ok_or_else(|| anyhow::anyhow!("Issuer must be present if self-signed is false"))
}
}
fn validate_certificate_type(
self_signed: bool, certificate_type: Option<u8>,
) -> anyhow::Result<()> {
if self_signed && certificate_type.unwrap_or(SELF_SIGNED_INT) != SELF_SIGNED_INT {
return Err(anyhow::anyhow!(
"Certificate type must be 0 if self-signed is true"
));
}
Ok(())
}
fn parse_public_key(public_key: &str) -> anyhow::Result<PublicKey> {
let pk_path = PathBuf::from(public_key);
PublicKey::from_file(pk_path)
}
fn get_key_type(key_type: &Option<String>) -> anyhow::Result<(Oid<'static>, Option<String>)> {
match key_type.as_deref() {
Some("ed25519") | None => Ok(ED25519),
Some(_) => Err(anyhow::anyhow!("Currently only support Ed25519")),
}
}
fn parse_or_default_date(date_option: Option<String>, default: i64) -> Result<i64, anyhow::Error> {
match date_option {
Some(date) => {
DateTime::parse_from_rfc3339(&date)
.map(|dt| dt.timestamp())
.map_err(|e| anyhow::anyhow!(format!("Failed to parse date {date}: {e}",)))
},
None => Ok(default),
}
}
fn parse_serial_number(serial_number: Option<UnwrappedBigUint>) -> UnwrappedBigUint {
let random_number: u64 = rand::thread_rng().gen();
serial_number.unwrap_or(UnwrappedBigUint::new(random_number))
}
fn verify(file: &PathBuf, public_key: PathBuf) -> anyhow::Result<()> {
let cert = fs::read(file)?;
let pk = PublicKey::from_file(public_key)?;
match c509_certificate::verify(&cert, &pk) {
Ok(()) => println!("Signature verified!"),
Err(e) => println!("Signature verification failed: {e}"),
};
Ok(())
}
fn decode(file: &PathBuf, output: Option<PathBuf>) -> anyhow::Result<()> {
let cert = fs::read(file)?;
let mut d = minicbor::Decoder::new(&cert);
let c509 = c509_certificate::c509::C509::decode(&mut d, &mut ())?;
let tbs_cert = c509.get_tbs_cert();
let is_self_signed = tbs_cert.get_c509_certificate_type() == SELF_SIGNED_INT;
let c509_json = C509Json {
self_signed: is_self_signed,
certificate_type: Some(tbs_cert.get_c509_certificate_type()),
serial_number: Some(tbs_cert.get_certificate_serial_number().clone()),
issuer: Some(extract_relative_distinguished_name(tbs_cert.get_issuer())?),
validity_not_before: Some(time_to_string(tbs_cert.get_validity_not_before().to_i64())?),
validity_not_after: Some(time_to_string(tbs_cert.get_validity_not_after().to_i64())?),
subject: extract_relative_distinguished_name(tbs_cert.get_subject())?,
subject_public_key_algorithm: Some(tbs_cert.get_subject_public_key_algorithm().clone()),
subject_public_key: tbs_cert.get_subject_public_key().encode_hex(),
extensions: tbs_cert.get_extensions().clone(),
issuer_signature_algorithm: Some(tbs_cert.get_issuer_signature_algorithm().clone()),
issuer_signature_value: c509.get_issuer_signature_value().clone(),
};
let data = serde_json::to_string(&c509_json)?;
if let Some(output) = output {
write_to_output_file(output, data.as_bytes())?;
};
println!("{data}");
Ok(())
}
fn extract_relative_distinguished_name(name: &Name) -> anyhow::Result<RelativeDistinguishedName> {
match name.get_value() {
NameValue::RelativeDistinguishedName(rdn) => Ok(rdn.clone()),
_ => Err(anyhow::anyhow!("Expected RelativeDistinguishedName")),
}
}
fn time_to_string(time: i64) -> anyhow::Result<String> {
let datetime =
DateTime::from_timestamp(time, 0).ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?;
Ok(datetime.to_rfc3339())
}
fn main() -> anyhow::Result<()> {
Cli::exec()
}