From fed67019aa08359479d9186825bd1d4541a657d3 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Wed, 3 Jul 2024 23:58:34 +0800 Subject: [PATCH] feat: add ssh-piv-sign, but it not works right --- Cargo.lock | 64 ++++++++++++++-- Cargo.toml | 1 + src/cmd_sshpivsign.rs | 171 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 + 4 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 src/cmd_sshpivsign.rs diff --git a/Cargo.lock b/Cargo.lock index a09e11f..522c094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,8 +83,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", + "asn1-rs-derive 0.4.0", + "asn1-rs-impl 0.1.0", "displaydoc", "nom", "num-traits", @@ -93,6 +93,21 @@ dependencies = [ "time 0.3.36", ] +[[package]] +name = "asn1-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" +dependencies = [ + "asn1-rs-derive 0.5.0", + "asn1-rs-impl 0.2.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", +] + [[package]] name = "asn1-rs-derive" version = "0.4.0" @@ -105,6 +120,18 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" +dependencies = [ + "proc-macro2", + "quote 1.0.36", + "syn 2.0.66", + "synstructure 0.13.1", +] + [[package]] name = "asn1-rs-impl" version = "0.1.0" @@ -116,6 +143,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote 1.0.36", + "syn 2.0.66", +] + [[package]] name = "atty" version = "0.2.14" @@ -375,6 +413,7 @@ dependencies = [ "bech32", "chrono", "clap", + "der-parser 9.0.0", "digest 0.10.7", "ecdsa", "env_logger", @@ -676,7 +715,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", "displaydoc", "nom", "num-bigint", @@ -684,6 +723,19 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.1", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der_derive" version = "0.7.2" @@ -1961,7 +2013,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", ] [[package]] @@ -4142,9 +4194,9 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", "data-encoding", - "der-parser", + "der-parser 8.2.0", "lazy_static", "nom", "oid-registry", diff --git a/Cargo.toml b/Cargo.toml index a32d15f..7f9e15d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ reqwest = { version = "0.11", features = ["blocking"] } pinentry = "0.5.0" rpassword = "7.3.1" secrecy = "0.8.0" +der-parser = "9.0.0" #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_sshpivsign.rs b/src/cmd_sshpivsign.rs new file mode 100644 index 0000000..9d59e9d --- /dev/null +++ b/src/cmd_sshpivsign.rs @@ -0,0 +1,171 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; +use der_parser::ber::BerObjectContent; +use pem::Pem; +use rust_util::util_clap::{Command, CommandError}; +use yubikey::{Key, YubiKey}; +use yubikey::piv::{AlgorithmId, sign_data}; + +use crate::{pinutil, pivutil, util}; +use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr}; + +trait VecWriter { + fn write_bytes(&mut self, bytes: &[u8]) -> (); + fn write_u32(&mut self, num: u32) -> (); + fn write_string(&mut self, bytes: &[u8]) -> (); +} + +impl VecWriter for Vec { + fn write_bytes(&mut self, bytes: &[u8]) -> () { + self.extend_from_slice(bytes); + } + + fn write_u32(&mut self, num: u32) -> () { + self.write_bytes(&num.to_be_bytes()); + } + + fn write_string(&mut self, bytes: &[u8]) -> () { + self.write_u32(bytes.len() as u32); + self.write_bytes(bytes); + } +} + +pub struct CommandImpl; + +// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig +impl Command for CommandImpl { + fn name(&self) -> &str { "ssh-piv-sign" } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()).about("SSH parse sign subcommand") + .arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN")) + .arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e")) + .arg(Arg::with_name("namespace").short("n").long("namespace").takes_value(true).help("Namespace")) + .arg(Arg::with_name("in").long("in").takes_value(true).help("In file, - for stdin")) + .arg(Arg::with_name("raw-in").long("raw-in").takes_value(true).help("Raw in data")) + } + + fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + warning!("It NOT works in the right way, and I do not know how to fix it now"); + + let namespace_opt = sub_arg_matches.value_of("namespace"); + let namespace = match namespace_opt { + None => return simple_error!("Namespace required"), + Some(namespace) => namespace, + }; + + let (is_raw_in, data) = match sub_arg_matches.value_of("in") { + None => match sub_arg_matches.value_of("raw-in") { + None => return simple_error!("--in or --raw-in must assign one"), + Some(raw_in) => (true, util::try_decode(raw_in)?), + } + Some(file_in) => { + let message = util::read_file_or_stdin(file_in)?; + debugging!("File in: {:?}", message); + debugging!("File in string: {}", String::from_utf8_lossy(&message)); + (false, message) + } + }; + + let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"); + let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let slot_id = pivutil::get_slot_id(slot)?; + + let pin_opt = sub_arg_matches.value_of("pin"); + let pin_opt = pinutil::get_pin(pin_opt); + let pin_opt = pin_opt.as_deref(); + + let mut algorithm_id_opt = None; + let mut ec_key_point = vec![]; + match Key::list(&mut yk) { + Err(e) => warning!("List keys failed: {}", e), + Ok(keys) => for k in &keys { + let slot_str = format!("{:x}", Into::::into(k.slot())); + if slot_equals(&slot_id, &slot_str) { + let cert = &k.certificate().cert.tbs_certificate; + let certificate = k.certificate(); + if let Ok(algorithm_id) = get_algorithm_id_by_certificate(certificate) { + match algorithm_id { + AlgorithmId::EccP256 | AlgorithmId::EccP384 => { + let public_key_bit_string = &cert.subject_public_key_info.subject_public_key; + ec_key_point.extend_from_slice(public_key_bit_string.raw_bytes()); + algorithm_id_opt = Some(algorithm_id); + } + _ => return simple_error!("Not P256/384 key: {}", algorithm_id.to_str()), + } + } + } + } + } + let algorithm_id = match algorithm_id_opt { + None => return simple_error!("Slot key not found!"), + Some(algorithm_id) => algorithm_id, + }; + let ec_bit_len = iff!(matches!(algorithm_id, AlgorithmId::EccP256), 256, 384); + + let mut buffer = vec![]; + buffer.write_bytes("SSHSIG".as_bytes()); + buffer.write_u32(1); + + let mut public_key = vec![]; + public_key.write_string(format!("ecdsa-sha2-nistp{}", ec_bit_len).as_bytes()); + public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes()); + public_key.write_string(&ec_key_point); + buffer.write_string(&public_key); + buffer.write_string(namespace.as_bytes()); + buffer.write_string("".as_bytes()); + // The supported hash algorithms are "sha256" and "sha512". + buffer.write_string("sha512".as_bytes()); + + let mut signature = vec![]; + signature.write_string(format!("ecdsa-sha2-nistp{}", ec_bit_len).as_bytes()); + + let data = if is_raw_in { + data + } else { + crate::digest::sha512_bytes(&data) + }; + let mut sign_message = vec![]; + sign_message.write_bytes("SSHSIG".as_bytes()); + sign_message.write_string(namespace.as_bytes()); + sign_message.write_string("".as_bytes()); + sign_message.write_string("sha512".as_bytes()); + sign_message.write_string(&data); + let tobe_signed_data = if ec_bit_len == 256 { + crate::digest::sha256_bytes(&signature) + } else { + crate::digest::sha384_bytes(&signature) + }; + + if let Some(pin) = pin_opt { + opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); + } + let mut signature_value = vec![]; + let signed_data = opt_result!(sign_data(&mut yk, &tobe_signed_data, algorithm_id, slot_id), "Sign PIV failed: {}"); + let (_, parsed_signature) = opt_result!(der_parser::parse_der(signed_data.as_slice()), "Parse signature failed: {}"); + match parsed_signature.content { + BerObjectContent::Sequence(seq) => { + match &seq[0].content { + BerObjectContent::Integer(x) => { + signature_value.write_string(x); + } + _ => return simple_error!("Parse signature failed: [0]not integer"), + } + match &seq[1].content { + BerObjectContent::Integer(y) => { + signature_value.write_string(y); + } + _ => return simple_error!("Parse signature failed: [1]not integer"), + } + } + _ => return simple_error!("Parse signature failed: not sequence"), + } + signature.write_string(&signature_value); + buffer.write_string(&signature); + + let ssh_sig = Pem::new("SSH SIGNATURE", buffer); + let ssh_sig_pem = ssh_sig.to_string(); + println!("{}", ssh_sig_pem); + + Ok(None) + } +} diff --git a/src/main.rs b/src/main.rs index f5661e6..ead34b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,7 @@ mod cmd_chall; mod cmd_challconfig; mod cmd_sshagent; mod cmd_sshparsesign; +mod cmd_sshpivsign; mod cmd_pgpageaddress; mod cmd_signjwt; mod cmd_signfile; @@ -101,6 +102,7 @@ fn inner_main() -> CommandError { Box::new(cmd_u2fsign::CommandImpl), Box::new(cmd_sshagent::CommandImpl), Box::new(cmd_sshparsesign::CommandImpl), + Box::new(cmd_sshpivsign::CommandImpl), Box::new(cmd_pgpageaddress::CommandImpl), Box::new(cmd_signjwt::CommandImpl), Box::new(cmd_signfile::CommandImpl),