diff --git a/Cargo.lock b/Cargo.lock index e16fda5..c3f948b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,7 +332,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.8.3" +version = "1.8.4" dependencies = [ "authenticator", "base64 0.21.5", diff --git a/Cargo.toml b/Cargo.toml index 1cb4e70..2518e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.8.3" +version = "1.8.4" authors = ["Hatter Jiang "] edition = "2018" diff --git a/src/cmd_signfile.rs b/src/cmd_signfile.rs new file mode 100644 index 0000000..1180330 --- /dev/null +++ b/src/cmd_signfile.rs @@ -0,0 +1,234 @@ +use std::time::SystemTime; + +use clap::{App, Arg, ArgMatches, SubCommand}; +use rust_util::{util_msg, XResult}; +use rust_util::util_clap::{Command, CommandError}; +use serde::Serialize; +use yubikey::{Key, YubiKey}; +use yubikey::piv::{sign_data, SlotId}; + +use crate::{argsutil, pivutil}; +use crate::digest::sha256_bytes; +use crate::util::base64_encode; + +pub const SIMPLE_SIG_V1: &str = "v1"; +pub const SIMPLE_SIG_SPECIFICATION: &str = "https://openwebstandard.org/simple-sign-file/v1"; +pub const HASH_ALGORITHM_SHA256: &str = "sha256"; +pub const SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA: &str = "SHA256withECDSA"; + +pub struct SignFileRequest { + pub filename: Option, + pub digest: Vec, + pub timestamp: i128, + pub attributes: Option, + pub comment: Option, +} + +impl SignFileRequest { + pub fn get_tobe_signed(&self) -> Vec { + let mut tobe_signed = vec![]; + // "v1"||TLV(filename)||TLV(timestamp)||TLV(attributes)||TLV(comment)||TLV(digest) + debugging!("Tobe signed version: {}", SIMPLE_SIG_V1); + tobe_signed.extend_from_slice(SIMPLE_SIG_V1.as_bytes()); + let tobe_signed_filename = SignFileTlv::Filename(self.filename.clone()).to_byes(); + debugging!("Tobe signed filename: {} ({:?})", hex::encode(&tobe_signed_filename), &self.filename); + tobe_signed.extend_from_slice(&tobe_signed_filename); + let tobe_signed_timestamp = SignFileTlv::Timestamp(self.timestamp.clone()).to_byes(); + debugging!("Tobe signed timestamp: {} ({})", hex::encode(&tobe_signed_timestamp), &self.timestamp); + tobe_signed.extend_from_slice(&tobe_signed_timestamp); + let tobe_signed_attributes = SignFileTlv::Attributes(self.attributes.clone()).to_byes(); + debugging!("Tobe signed attributes: {} ({:?})", hex::encode(&tobe_signed_attributes), &self.attributes); + tobe_signed.extend_from_slice(&tobe_signed_attributes); + let tobe_signed_comment = SignFileTlv::Comment(self.comment.clone()).to_byes(); + debugging!("Tobe signed comment: {} ({:?})", hex::encode(&tobe_signed_comment), &self.comment); + tobe_signed.extend_from_slice(&tobe_signed_comment); + let tobe_signed_digest = SignFileTlv::Digest(self.digest.clone()).to_byes(); + debugging!("Tobe signed digest: {}", hex::encode(&tobe_signed_digest)); + tobe_signed.extend_from_slice(&tobe_signed_digest); + tobe_signed + } +} + +pub enum SignFileTlv { + Filename(Option), + Timestamp(i128), + Attributes(Option), + Comment(Option), + Digest(Vec), +} + +impl SignFileTlv { + pub fn tag(&self) -> u8 { + match self { + SignFileTlv::Filename(_) => 0, + SignFileTlv::Timestamp(_) => 1, + SignFileTlv::Attributes(_) => 2, + SignFileTlv::Comment(_) => 3, + SignFileTlv::Digest(_) => 255, + } + } + + pub fn to_byes(&self) -> Vec { + let mut bytes = vec![]; + bytes.push(self.tag()); + match self { + SignFileTlv::Timestamp(timestamp) => { + bytes.extend_from_slice(×tamp.to_be_bytes()); + } + SignFileTlv::Filename(value) + | SignFileTlv::Attributes(value) + | SignFileTlv::Comment(value) => { + Self::extends_bytes(&mut bytes, match value { + None => &[], + Some(value) => value.as_bytes(), + }); + } + SignFileTlv::Digest(digest) => { + Self::extends_bytes(&mut bytes, digest); + } + } + bytes + } + + fn extends_bytes(bytes: &mut Vec, b: &[u8]) { + if b.len() > u16::MAX as usize { + panic!("Cannot more than: {}", u16::MAX); + } + bytes.extend_from_slice(&(b.len() as u16).to_be_bytes()); + bytes.extend_from_slice(b); + } +} + +#[derive(Serialize)] +pub struct SimpleSignFileSignature { + pub algorithm: String, + pub signature: String, + pub certificates: Vec, +} + +#[derive(Serialize)] +pub struct SimpleSignFile { + pub specification: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + pub digest: String, + pub timestamp: i128, + #[serde(skip_serializing_if = "Option::is_none")] + pub attributes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + pub signatures: Vec, +} + +pub struct CommandImpl; + +// Format: +// { +// "specification": "https://openwebstandard.org/simple-sign-file/v1", +// "version": "v1", +// "filename": "example.zip", +// "digest": "HEX(SHA256(filename-content))", +// "timestamp": 1700964163340, +// "attributes": "****", +// "comment": "***", +// "signatures": [{ +// "signature": "SHA256withECDSA:HEX(Sign(SHA256("v1"||TLV(filename)||TLV(timestamp)||TLV(attributes)||TLV(comment)||TLV(digest))))", +// "certificates": ["-----BEGIN CERTIFICATE-----\n*****\n-----END CERTIFICATE-----", ...] +// }] +// } +// v1 only support SHA256 +// all hex is in lower case default +// file ext: *.simple-sig +impl Command for CommandImpl { + fn name(&self) -> &str { "sign-file" } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()).about("PIV Sign(with SHA256) 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).required(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e")) + .arg(Arg::with_name("file").short("f").long("file").takes_value(true).required(true).help("Input file")) + .arg(Arg::with_name("filename").short("n").long("filename").takes_value(true).help("Filename")) + .arg(Arg::with_name("comment").short("c").long("comment").takes_value(true).help("Comment")) + .arg(Arg::with_name("attributes").short("a").long("attributes").takes_value(true).help("Attributes")) + } + + fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + util_msg::set_logger_std_out(false); + + let filename_opt = sub_arg_matches.value_of("filename").map(ToString::to_string); + let comment_opt = sub_arg_matches.value_of("comment").map(ToString::to_string); + let attributes_opt = sub_arg_matches.value_of("attributes").map(ToString::to_string); + + let pin_opt = sub_arg_matches.value_of("pin"); + + let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"); + // TODO read from stream not in memory + let file_digest = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?; + debugging!("File digest: {}", hex::encode(&file_digest)); + + let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + + let slot_id = pivutil::get_slot_id(slot)?; + let key = find_key(&mut yk, &slot_id)?; + let key = opt_value_result!(key, "Cannot find key in slot: {}", slot_id); + + let certificate = key.certificate(); + // let tbs_certificate = &certificate.cert.tbs_certificate; + // TODO check certs matches with key in slot + let algorithm_id = opt_result!( + pivutil::get_algorithm_id_by_certificate(certificate), "Get slot key algorithm failed: {}"); + debugging!("PIV algorithm: {:?}", algorithm_id); + if let Some(pin) = pin_opt { + debugging!("PIN is assigned."); + opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); + } + + let sign_file_request = SignFileRequest { + filename: filename_opt, + digest: file_digest.clone(), + timestamp: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as i128, + attributes: attributes_opt, + comment: comment_opt, + }; + let tobe_signed = sign_file_request.get_tobe_signed(); + debugging!("Tobe signed: {}", hex::encode(&tobe_signed)); + let tobe_signed_digest = sha256_bytes(&tobe_signed); + debugging!("Tobe signed digest: {}", hex::encode(&tobe_signed_digest)); + + let signed_data = opt_result!(sign_data(&mut yk, &tobe_signed_digest, algorithm_id, slot_id), "Sign PIV failed: {}"); + let signature_bytes = signed_data.as_slice(); + debugging!("Tobe signed signature: {}", hex::encode(&signature_bytes)); + + let signature = SimpleSignFileSignature { + algorithm: SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA.to_string(), + signature: format!("{}", base64_encode(&signature_bytes)), + certificates: vec![], + }; + let simple_sig = SimpleSignFile { + specification: SIMPLE_SIG_SPECIFICATION.to_string(), + filename: sign_file_request.filename.clone(), + digest: format!("{}-{}", HASH_ALGORITHM_SHA256, hex::encode(&sign_file_request.digest)), + timestamp: sign_file_request.timestamp, + attributes: sign_file_request.attributes.clone(), + comment: sign_file_request.comment.clone(), + signatures: vec![signature], + }; + + println!("{}", serde_json::to_string_pretty(&simple_sig).unwrap()); + + Ok(None) + } +} + +fn find_key(yk: &mut YubiKey, slot_id: &SlotId) -> XResult> { + match Key::list(yk) { + Err(e) => warning!("List keys failed: {}", e), + Ok(keys) => for k in keys { + let slot_str = format!("{:x}", Into::::into(k.slot())); + if pivutil::slot_equals(slot_id, &slot_str) { + return Ok(Some(k)); + } + } + } + Ok(None) +} diff --git a/src/main.rs b/src/main.rs index 7f3b476..1f0e5ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,7 @@ mod cmd_challconfig; mod cmd_sshagent; mod cmd_pgpageaddress; mod cmd_signjwt; +mod cmd_signfile; pub struct DefaultCommandImpl; @@ -96,6 +97,7 @@ fn inner_main() -> CommandError { Box::new(cmd_sshagent::CommandImpl), Box::new(cmd_pgpageaddress::CommandImpl), Box::new(cmd_signjwt::CommandImpl), + Box::new(cmd_signfile::CommandImpl), ]; let mut app = App::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION"))