feat: add verify-file

This commit is contained in:
2024-04-21 16:08:49 +08:00
parent b3d8c791c4
commit 266cf01930
7 changed files with 280 additions and 118 deletions

30
Cargo.lock generated
View File

@@ -368,7 +368,7 @@ dependencies = [
[[package]]
name = "card-cli"
version = "1.8.6"
version = "1.9.0"
dependencies = [
"authenticator",
"base64 0.21.7",
@@ -389,7 +389,7 @@ dependencies = [
"pem",
"rand 0.8.5",
"reqwest",
"ring",
"ring 0.17.8",
"rust_util",
"sequoia-openpgp",
"serde",
@@ -1360,7 +1360,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7660d28d24a831d690228a275d544654a30f3b167a8e491cf31af5fe5058b546"
dependencies = [
"untrusted",
"untrusted 0.9.0",
]
[[package]]
@@ -2420,6 +2420,21 @@ dependencies = [
"subtle",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin 0.5.2",
"untrusted 0.7.1",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "ring"
version = "0.17.8"
@@ -2431,7 +2446,7 @@ dependencies = [
"getrandom 0.2.14",
"libc",
"spin 0.9.8",
"untrusted",
"untrusted 0.9.0",
"windows-sys 0.52.0",
]
@@ -3480,6 +3495,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -3884,6 +3905,7 @@ dependencies = [
"lazy_static",
"nom",
"oid-registry",
"ring 0.16.20",
"rusticata-macros",
"thiserror",
"time 0.3.36",

View File

@@ -1,6 +1,6 @@
[package]
name = "card-cli"
version = "1.8.6"
version = "1.9.0"
authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018"
@@ -29,7 +29,7 @@ pem = "3.0"
yubikey = { version = "0.8", features = ["untested"] }
yubico_manager = "0.9"
x509 = "0.2"
x509-parser = "0.15"
x509-parser = { version = "0.15", features = ["verify"] }
ssh-agent = { version = "0.2", features = ["agent"] }
p256 = { version = "0.13", features = ["pem", "ecdh"] }
p384 = { version = "0.13", features = ["pem", "ecdh"] }

View File

@@ -7,8 +7,14 @@ use rust_util::XResult;
use crate::digest::{sha256, sha256_bytes};
pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
if let Some(file) = sub_arg_matches.value_of("file") {
get_sha256_digest_or_hash_with_file_opt(sub_arg_matches, &None)
}
pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>) -> XResult<Vec<u8>> {
let file_opt = file_opt.as_ref().map(String::as_str);
if let Some(file) = sub_arg_matches.value_of("file").or(file_opt) {
let metadata = opt_result!(fs::metadata(file), "Read file: {} metadata filed: {}", file);
if !metadata.is_file() {
return simple_error!("Not a file: {}", file);

View File

@@ -3,7 +3,7 @@ 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::{Deserialize, Serialize};
use serde::Deserialize;
use spki::der::Encode;
use x509_parser::nom::AsBytes;
use yubikey::{Key, YubiKey};
@@ -11,118 +11,9 @@ use yubikey::piv::{sign_data, SlotId};
use crate::{argsutil, pivutil};
use crate::digest::sha256_bytes;
use crate::signfile::{CERTIFICATES_SEARCH_URL, HASH_ALGORITHM_SHA256, SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA, SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile, SimpleSignFileSignature};
use crate::util::base64_encode;
pub const SIMPLE_SIG_V1: &str = "v1";
pub const SIMPLE_SIG_SCHEMA: &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 const CERTIFICATES_SEARCH_URL: &str = "https://hatter.ink/ca/fetch_certificates.json?fingerprint=";
pub struct SignFileRequest {
pub filename: Option<String>,
pub digest: Vec<u8>,
pub timestamp: i64,
pub attributes: Option<String>,
pub comment: Option<String>,
}
impl SignFileRequest {
pub fn get_tobe_signed(&self) -> Vec<u8> {
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).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<String>),
Timestamp(i64),
Attributes(Option<String>),
Comment(Option<String>),
Digest(Vec<u8>),
}
impl SignFileTlv {
pub fn tag(&self) -> u8 {
match self {
SignFileTlv::Filename(_) => 0,
SignFileTlv::Timestamp(_) => 1,
SignFileTlv::Attributes(_) => 2,
SignFileTlv::Comment(_) => 3,
SignFileTlv::Digest(_) => 254,
}
}
pub fn to_byes(&self) -> Vec<u8> {
let mut bytes = vec![];
bytes.push(self.tag());
match self {
SignFileTlv::Timestamp(timestamp) => {
bytes.extend_from_slice(&timestamp.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<u8>, 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<String>,
}
#[derive(Serialize)]
pub struct SimpleSignFile {
pub schema: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
pub digest: String,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
pub signatures: Vec<SimpleSignFileSignature>,
}
pub struct CommandImpl;
// Format:

129
src/cmd_verifyfile.rs Normal file
View File

@@ -0,0 +1,129 @@
use std::fs;
use std::ops::Add;
use std::time::{Duration, SystemTime};
use clap::{App, Arg, ArgMatches, SubCommand};
use p384::ecdsa::signature::hazmat::PrehashVerifier;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use x509_parser::parse_x509_certificate;
use x509_parser::pem::parse_x509_pem;
use x509_parser::public_key::PublicKey;
use x509_parser::time::ASN1Time;
use crate::argsutil;
use crate::digest::sha256_bytes;
use crate::signfile::{SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile};
use crate::util::base64_decode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "verify-file" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV Verify(with SHA256) subcommand")
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).required(false).help("Input file"))
.arg(Arg::with_name("filename").short("n").long("filename").takes_value(true).help("Filename"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let filename = sub_arg_matches.value_of("filename").map(ToString::to_string).unwrap();
let file_content = opt_result!(fs::read_to_string(&filename), "Read file: {}, failed: {}", filename);
let simple_sign_file: SimpleSignFile = opt_result!(serde_json::from_str(&file_content), "Parse file: {}, failed: {}", filename);
if SIMPLE_SIG_SCHEMA != simple_sign_file.schema {
return simple_error!("File: {} format error: bad schema", filename);
}
information!("File name: {}", simple_sign_file.filename.as_deref().unwrap_or("<none>"));
information!("Digest: {}", &simple_sign_file.digest);
let sign_time = SystemTime::UNIX_EPOCH.add(Duration::from_millis(simple_sign_file.timestamp as u64));
let format_time = simpledateformat::fmt("yyyy-MM-dd HH:mm:ssz").unwrap().format_local(sign_time);
information!("Timestamp: {}", format_time);
if let Some(attributes) = &simple_sign_file.attributes {
information!("Attributes: {}", attributes);
}
if let Some(comment) = &simple_sign_file.comment {
information!("Comment: {}", comment);
}
let file_digest = argsutil::get_sha256_digest_or_hash_with_file_opt(sub_arg_matches, &simple_sign_file.filename)?;
debugging!("File digest: {}", hex::encode(&file_digest));
let file_digest_with_prefix = format!("sha256-{}", hex::encode(&file_digest));
if file_digest_with_prefix != simple_sign_file.digest {
failure!("File digest mismatch\nexpected: {}\nactual : {}", simple_sign_file.digest, file_digest_with_prefix);
return simple_error!("File digest mismatch");
}
let sign_file_request = SignFileRequest {
filename: simple_sign_file.filename.clone(),
digest: file_digest.clone(),
timestamp: simple_sign_file.timestamp,
attributes: simple_sign_file.attributes.clone(),
comment: simple_sign_file.comment.clone(),
};
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));
if simple_sign_file.signatures.is_empty() {
failure!("No signatures found.");
return simple_error!("No signatures found");
}
information!("Found {} signature(s)", simple_sign_file.signatures.len());
for (i, signature) in simple_sign_file.signatures.iter().enumerate() {
// check tobe_signed_digest by signature_bytes
information!("Check signature #{} of {}", i, simple_sign_file.signatures.len());
let signature_bytes = opt_result!(base64_decode(&signature.signature), "Parse signatures.signature failed: {}");
debugging!("Signature #{}: {}", i, hex::encode(&signature_bytes));
let mut cert_pems = vec![];
for certificate in &signature.certificates {
let (_, cert_pem) = opt_result!(parse_x509_pem(certificate.as_bytes()), "Parse certificate PEM failed: {}");
cert_pems.push(cert_pem);
}
let mut certificates = vec![];
for cert_pem in &cert_pems {
let (_, cert) = opt_result!(parse_x509_certificate(&cert_pem.contents), "Parse certificate failed: {}");
debugging!("Found certificate, subject: {}, issuer : {}", cert.subject.to_string(), cert.issuer.to_string());
let asn1_timestamp = opt_result!(ASN1Time::from_timestamp(simple_sign_file.timestamp/1000), "ASN1Time failed: {}");
if !cert.validity.is_valid_at(asn1_timestamp) {
failure!("Certificate validity is out of cate: {:?}", cert.validity);
return simple_error!("Certificate is invalid: {}, out of date", cert.subject.to_string());
}
certificates.push(cert);
}
let certificates_count = certificates.len();
for i in 0..certificates.len() {
let cert1 = &certificates[i];
let cert2_public_key = iff!(i < certificates_count -1, Some(certificates[i + 1].public_key()), None);
match cert1.verify_signature(cert2_public_key) {
Ok(_) => success!("Cert #{}: {} verify success", i, cert1.subject.to_string()),
Err(e) => failure!("Cert #{}: {} verify failed: {}", i, cert1.subject.to_string(), e),
}
}
let leaf_certificate = &certificates[0];
let leaf_public_key = opt_result!(leaf_certificate.public_key().parsed(), "Parse leaf certificate public key failed: {}");
match leaf_public_key {
// PublicKey::RSA(_) => {}
PublicKey::EC(ec_point) => {
if ec_point.key_size() != 384 {
return simple_error!("Only support p384");
}
let p384_verifying_key = opt_result!(p384::ecdsa::VerifyingKey::from_sec1_bytes(ec_point.data()), "Parse public key failed: {}");
let sig = opt_result!(p384::ecdsa::DerSignature::from_bytes(&signature_bytes), "Parse signature failed: {}");
match p384_verifying_key.verify_prehash(&tobe_signed_digest, &sig) {
Ok(_) => success!("Verify leaf certificate signature succeed."),
Err(e) => return simple_error!("Verify leaf certificate signature failed: {}", e),
}
}
_ => return simple_error!("Not supported public key: {:?}", leaf_public_key),
}
}
Ok(None)
}
}

View File

@@ -43,6 +43,8 @@ mod cmd_sshagent;
mod cmd_pgpageaddress;
mod cmd_signjwt;
mod cmd_signfile;
mod cmd_verifyfile;
mod signfile;
pub struct DefaultCommandImpl;
@@ -98,6 +100,7 @@ fn inner_main() -> CommandError {
Box::new(cmd_pgpageaddress::CommandImpl),
Box::new(cmd_signjwt::CommandImpl),
Box::new(cmd_signfile::CommandImpl),
Box::new(cmd_verifyfile::CommandImpl),
];
let mut app = App::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))

111
src/signfile.rs Normal file
View File

@@ -0,0 +1,111 @@
use serde::{Deserialize, Serialize};
pub const SIMPLE_SIG_V1: &str = "v1";
pub const SIMPLE_SIG_SCHEMA: &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 const CERTIFICATES_SEARCH_URL: &str = "https://hatter.ink/ca/fetch_certificates.json?fingerprint=";
pub struct SignFileRequest {
pub filename: Option<String>,
pub digest: Vec<u8>,
pub timestamp: i64,
pub attributes: Option<String>,
pub comment: Option<String>,
}
impl SignFileRequest {
pub fn get_tobe_signed(&self) -> Vec<u8> {
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).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 file digest: {}", hex::encode(&tobe_signed_digest));
tobe_signed.extend_from_slice(&tobe_signed_digest);
tobe_signed
}
}
pub enum SignFileTlv {
Filename(Option<String>),
Timestamp(i64),
Attributes(Option<String>),
Comment(Option<String>),
Digest(Vec<u8>),
}
impl SignFileTlv {
pub fn tag(&self) -> u8 {
match self {
SignFileTlv::Filename(_) => 0,
SignFileTlv::Timestamp(_) => 1,
SignFileTlv::Attributes(_) => 2,
SignFileTlv::Comment(_) => 3,
SignFileTlv::Digest(_) => 254,
}
}
pub fn to_byes(&self) -> Vec<u8> {
let mut bytes = vec![];
bytes.push(self.tag());
match self {
SignFileTlv::Timestamp(timestamp) => {
bytes.extend_from_slice(&timestamp.to_be_bytes());
}
SignFileTlv::Filename(value)
| SignFileTlv::Attributes(value)
| SignFileTlv::Comment(value) => {
Self::write_bytes(&mut bytes, match value {
None => &[],
Some(value) => value.as_bytes(),
});
}
SignFileTlv::Digest(digest) => {
Self::write_bytes(&mut bytes, digest);
}
}
bytes
}
fn write_bytes(bytes: &mut Vec<u8>, 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, Deserialize)]
pub struct SimpleSignFileSignature {
pub algorithm: String,
pub signature: String,
pub certificates: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct SimpleSignFile {
pub schema: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
pub digest: String,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
pub signatures: Vec<SimpleSignFileSignature>,
}