Compare commits

...

100 Commits

Author SHA1 Message Date
8a50024285 feat: v1.13.21 2025-10-18 14:29:51 +08:00
07e1671867 feat: v1.13.20, fix incomp with future compiler 2025-09-27 16:56:05 +08:00
4537b6ece9 feat: v1.13.19 2025-09-27 16:25:03 +08:00
f96144f450 feat: update dependencies 2025-09-27 12:12:57 +08:00
1d49b7c1c1 feat: external_ecdh for ML-KEM 2025-09-27 12:11:03 +08:00
10c38cda8a feat: external_public_key supports ML-KEM 2025-09-27 12:02:26 +08:00
9f544e3cb7 feat: ml-kem generate 2025-09-27 11:32:42 +08:00
3d29fe6a6d feat: add generate_ml_kem_768, pending implement ML-KEM later 2025-09-27 00:49:37 +08:00
1d23dba248 feat: updates 2025-07-24 22:43:47 +08:00
6f556cc2d6 feat: v1.13.18, external_sign supports --message-type 2025-07-19 13:58:51 +08:00
33a6661c3f feat: v1.13.17, claim support mutiple times 2025-07-14 22:19:10 +08:00
421f2e2ffe feat: update dependencies 2025-06-30 00:16:02 +08:00
3647515321 feat: v1.13.16, update dependencies 2025-06-30 00:05:01 +08:00
6bd4d0ba57 feat: 1.13.15, make clippy happy 2025-06-29 23:23:19 +08:00
d272904357 feat: v1.13.14 2025-06-29 00:11:10 +08:00
0bc671be7b feat: v1.13.13 2025-06-09 22:42:12 +08:00
a698a852fd feat: update ssh agent 2025-05-27 00:04:26 +08:00
febdf659cd feat: updates ssh agent 2025-05-26 00:16:39 +08:00
62110ed7fb feat: update ssh agent 2025-05-26 00:07:40 +08:00
149650bf15 feat: updates ssh agent 2025-05-26 00:06:11 +08:00
5b3e0bc8cb feat: update ssh agent 2025-05-25 23:17:46 +08:00
f870c07387 feat: ssh agent, not works yet 2025-05-25 22:39:02 +08:00
ecf034376d feat: update ssh agent 2025-05-25 20:56:20 +08:00
488db38387 feat: add ssh-agent-gpg 2025-05-25 20:23:15 +08:00
f6b9671872 feat: add ssh-agent-gpg 2025-05-25 20:22:57 +08:00
87e51cc7e4 feat: 1.13.12, support external command 2025-05-24 10:31:13 +08:00
bb8d804505 feat: openwebstandard.org/rfc1 2025-05-23 07:04:21 +08:00
d104d4405e feat: update readme 2025-05-22 23:57:08 +08:00
f5a15ca0ae feat: update readme 2025-05-22 23:56:54 +08:00
28c2f096f7 feat: update readme 2025-05-22 23:56:22 +08:00
9926dbf09d feat: update readme 2025-05-22 23:55:43 +08:00
b23f4a3a69 feat: v1.13.10 2025-05-17 22:08:46 +08:00
d42bfd4bcc feat: v1.13.9 2025-05-17 22:03:27 +08:00
f74820903a feat: 1.13.8 2025-05-17 21:55:58 +08:00
21b5cc8221 feat: v1.13.7, add subcommand yubikey 2025-05-15 22:56:33 +08:00
7fa6aa1146 feat: v1.13.6 2025-05-14 00:30:19 +08:00
b4beaa3a75 feat: v1.13.5 2025-05-14 00:08:56 +08:00
fb026c9f21 feat: v1.13.4 2025-05-12 23:38:45 +08:00
4431bff9e6 feat: update zeroize 2025-05-10 00:17:32 +08:00
58f665823d feat: v1.13.3 2025-05-09 23:47:10 +08:00
fdf02bc976 feat: v1.13.2 2025-05-09 23:33:11 +08:00
8e4cf5cec8 feat: se revover 2025-05-08 22:40:17 +08:00
0b9ec436ba feat: v1.13.1 2025-05-08 22:29:19 +08:00
a1ae0ff4dc feat: v1.13.0 2025-05-07 23:54:20 +08:00
06d2da4ddf feat: updates 2025-05-07 22:16:45 +08:00
0513dd2398 feat: update hmac decrypt 2025-05-06 23:52:07 +08:00
d6ecdb5ed4 feat: v1.12.10 2025-05-06 23:47:18 +08:00
81f7a6d77e feat: updates hmac enc 2025-05-06 23:19:40 +08:00
63fabc6054 feat: v1.12.9 2025-05-06 22:35:03 +08:00
57c3ec57df feat: pbe encryption 2025-05-05 23:35:41 +08:00
c0ea3b773d feat: use nonce, salt 16 bytes 2025-05-05 23:28:58 +08:00
67568f8f15 feat: v1.12.7 2025-05-05 23:22:16 +08:00
9435b287c8 feat: key type rs3072, rsa4096 2025-05-05 13:00:45 +08:00
96927e0dab feat: rsa3072,4096 2025-05-05 12:25:38 +08:00
8894a2156a feat: v1.12.6 2025-05-05 11:22:28 +08:00
e52e42d48c feat: v1.12.5 2025-05-02 13:13:32 +08:00
a3541e7b68 feat: v1.12.4 2025-05-02 12:43:26 +08:00
d7f52530df feat: update ecdsautil 2025-05-02 00:01:11 +08:00
3dae02e090 feat: v1.12.3 2025-05-01 23:38:36 +08:00
86489c5d29 feat: fix YubiKey typo 2025-05-01 22:41:09 +08:00
1773186dbf feat: update parse_ecdsa_private_key 2025-05-01 21:55:14 +08:00
cec27e0f88 feat: v1.12.2 2025-05-01 21:46:04 +08:00
9a749b63eb feat: v1.12.1 2025-05-01 10:30:24 +08:00
fcb10f5efa feat: update external_sign 2025-05-01 00:37:12 +08:00
5329108380 feat: update external_sign 2025-05-01 00:35:04 +08:00
b8f0be2023 feat: external_sign secure enclave only supports ES256 2025-05-01 00:29:01 +08:00
0ac9300262 feat: fix external_sign rsa1024 2025-05-01 00:27:27 +08:00
c270c2e369 feat: v1.12.0 2025-05-01 00:22:42 +08:00
3af863762f feat: update crates 2025-04-30 01:23:16 +08:00
21676451fd feat: v1.11.17 2025-04-30 01:19:21 +08:00
4dca8e0146 feat: updates 2025-03-31 00:40:35 +08:00
dc56f2df77 feat: updates 2025-03-31 00:32:29 +08:00
4dac890200 feat: udpates 2025-03-31 00:26:03 +08:00
e7b20abd6d feat: v1.11.16 2025-03-31 00:22:20 +08:00
492c434f62 feat: v1.11.15, add convert-jwk-to-pem 2025-03-30 23:13:30 +08:00
fe30f538ba feat: v1.11.14, add convert-pem-to-jwk 2025-03-29 17:09:13 +08:00
e6409174b6 feat: v1.11.13 2025-03-29 16:38:26 +08:00
bb02c7c823 feat: bse64 2025-03-29 16:31:08 +08:00
e9388eb164 feat: udptes 2025-03-29 16:16:38 +08:00
a6bff6d31c feat: updates 2025-03-29 00:04:25 +08:00
6d3298549e feat: updates 2025-03-28 23:28:20 +08:00
417e3f6a49 feat: v1.11.12, optimize no-pin 2025-03-28 23:25:53 +08:00
3a40d7f0ad feat: updates 2025-03-28 07:35:53 +08:00
1be5754ed1 feat: 1.11.10, piv slot support f9 2025-03-27 23:58:55 +08:00
7ec3a705cf feat: v1.11.9 2025-03-27 07:29:32 +08:00
e2fa3bba9f feat: update justfile 2025-03-26 23:41:59 +08:00
070161c056 feat: v1.11.8, add keypair-keychain-import/export 2025-03-26 23:37:21 +08:00
3fb43403aa feat: udptes 2025-03-26 23:10:09 +08:00
c2b3a779c8 feat: renames 2025-03-26 23:05:48 +08:00
755d61fa86 feat: v1.11.7, support keychain store 2025-03-26 22:51:52 +08:00
af20f4c4a0 feat: v1.11.6, rename file-sign, file-verify 2025-03-26 07:40:54 +08:00
1582f76cae feat: 1.11.5 2025-03-26 07:35:38 +08:00
3848b65ff1 feat: v1.11.4, support hmac encrypt in generate-keypair 2025-03-26 07:18:47 +08:00
d4fce3f4fc feat: v1.11.3 2025-03-24 23:28:24 +08:00
8aeb47c66f feat: v1.11.3 2025-03-24 23:25:31 +08:00
d04038ccd9 feat: v1.11.3 2025-03-24 23:21:47 +08:00
aee2f8d5d3 feat: v1.11.3 2025-03-24 23:12:42 +08:00
25e80661bb feat: update Cargo.toml 2025-03-24 07:35:36 +08:00
c5e35fd941 feat: v1.11.2 2025-03-24 07:31:29 +08:00
468701b3ff feat: v1.11.1 2025-03-24 01:14:42 +08:00
87 changed files with 5545 additions and 2134 deletions

2306
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,13 @@
[package]
name = "card-cli"
version = "1.11.0"
version = "1.13.21"
authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018"
description = "FIDO(U2F, WebAuthn), YubiKey, OpenPGP command line tool"
license = "MIT OR Apache-2.0"
repository = "https://git.hatter.ink/hatter/card-cli"
[features]
default = ["with-sequoia-openpgp"]
with-sequoia-openpgp = ["sequoia-openpgp", "openpgp-card-sequoia"]
@@ -16,15 +20,14 @@ digest = "0.10"
sha1 = "0.10"
sha2 = "0.10"
rand = "0.8"
base64 = "0.21"
base64 = "0.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hex = "0.4"
u2f = "0.2"
openpgp-card = "0.3"
openpgp-card-pcsc = "0.3"
openpgp-card-sequoia = { version = "0.1", optional = true }
sequoia-openpgp = { version = "1.0", optional = true }
openpgp-card-sequoia = { version = "0.2", optional = true }
sequoia-openpgp = { version = "2.0", optional = true }
chrono = "0.4"
simpledateformat = "0.1"
ring = "0.17"
@@ -33,28 +36,41 @@ pem = "3.0"
yubikey = { version = "0.8", features = ["untested"] }
yubico_manager = "0.9"
x509 = "0.2"
x509-parser = { version = "0.15", features = ["verify"] }
x509-parser = { version = "0.17", features = ["verify"] }
ssh-agent = { version = "0.2", features = ["agent"] }
p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa"] }
p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa"] }
p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
p521 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
spki = { version = "0.7", features = ["pem"] }
tabled = "0.14"
env_logger = "0.10"
tabled = "0.20"
env_logger = "0.11"
bech32 = "0.9"
ecdsa = { version = "0.16", features = ["verifying", "spki", "pem", "der"] }
jwt = "0.16"
reqwest = { version = "0.11", features = ["blocking"] }
pinentry = "0.5"
reqwest = { version = "0.12", features = ["blocking"] }
pinentry = "0.6"
rpassword = "7.3"
secrecy = "0.8"
der-parser = "9.0"
sshcerts = "0.13"
regex = "1.4.6"
aes-gcm-stream = "0.2.4"
swift-secure-enclave-tool-rs = "0.1.0"
secrecy = "0.10"
der-parser = "10.0"
sshcerts-hatter-fork = "0.14.1"
regex = "1.11"
aes-gcm-stream = "0.2"
swift-secure-enclave-tool-rs = "1.0"
u2f-hatter-fork = "0.2"
security-framework = { version = "3.0", features = ["OSX_10_15"] }
rsa = "0.9"
which = "8.0"
percent-encoding = "2.3.1"
external-command-rs = "0.1.1"
ssh-agent-lib = { version = "0.5.1" }
ssh-key = { version = "0.6", features = ["ecdsa", "alloc"] }
tokio = "1.45.1"
ssh-encoding = { version = "0.2.0", features = ["alloc"] }
zeroize = "1.8"
ml-kem = { version = "0.2.1", features = ["zeroize"] }
zeroizing-alloc = "0.1.0"
#lazy_static = "1.4.0"
#ssh-key = "0.4.0"
#ctap-hid-fido2 = "2.1.3"
[patch.crates-io]
u2f = { git = "https://github.com/jht5945/u2f-rs.git" }
#[patch.crates-io]
#ml-kem = { path = "externals/ml-kem" }

View File

@@ -247,6 +247,10 @@ SSH to server:
ssh -i id_user root@example.com
```
<br>
> `external_*` subcommands follow &lt;&lt;Cryptography external command specification&gt;&gt;<br>
> Specification: https://openwebstandard.org/rfc1
<br><br>

26
examples/rsa.rs Normal file
View File

@@ -0,0 +1,26 @@
use base64::Engine;
use rand::rngs::OsRng;
use rsa::pkcs1::LineEnding;
use rsa::pkcs8::DecodePrivateKey;
use rsa::pkcs8::EncodePrivateKey;
use rsa::traits::PublicKeyParts;
use rsa::RsaPrivateKey;
use spki::EncodePublicKey;
fn main() {
let key = RsaPrivateKey::new(&mut OsRng, 1024).unwrap();
let pem = key.to_pkcs8_pem(LineEnding::LF).unwrap();
println!("{}", pem.as_str());
let key2 = RsaPrivateKey::from_pkcs8_pem(pem.as_ref()).unwrap();
let pub_key = key2.to_public_key();
let public_key_pem = pub_key.to_public_key_pem(LineEnding::LF).unwrap();
println!("{}", public_key_pem);
let n = pub_key.n();
let e = pub_key.e();
let url_safe = base64::engine::general_purpose::URL_SAFE_NO_PAD;
println!("n: {}", url_safe.encode(&n.to_bytes_be()));
println!("e: {}", url_safe.encode(&e.to_bytes_be()));
}

View File

@@ -1,6 +1,10 @@
_:
@just --list
# publish
publish:
cargo publish --registry crates-io
# install card-cli
install:
cargo install --path .

View File

@@ -5,14 +5,22 @@ use std::io::Read;
use clap::ArgMatches;
use rust_util::XResult;
use crate::digest::{sha256, sha256_bytes};
use crate::digestutil::DigestAlgorithm;
pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
get_sha256_digest_or_hash_with_file_opt(sub_arg_matches, &None)
}
pub fn get_digest_or_hash(sub_arg_matches: &ArgMatches, digest: DigestAlgorithm) -> XResult<Vec<u8>> {
get_digest_or_hash_with_file_opt(sub_arg_matches, &None, digest)
}
pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>) -> XResult<Vec<u8>> {
get_digest_or_hash_with_file_opt(sub_arg_matches, file_opt, DigestAlgorithm::Sha256)
}
pub fn get_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>, digest: DigestAlgorithm) -> 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);
@@ -28,9 +36,9 @@ pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, fil
let mut f = opt_result!(File::open(file), "Open file: {} failed: {}", file);
let mut content = vec![];
opt_result!(f.read_to_end(&mut content), "Read file: {} failed: {}", file);
Ok(sha256_bytes(&content))
Ok(digest.digest(&content))
} else if let Some(input) = sub_arg_matches.value_of("input") {
Ok(sha256(input))
Ok(digest.digest_str(input))
} else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") {
Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}"))
} else {

View File

@@ -1,8 +1,7 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::hmacutil;
use crate::{cmdutil, hmacutil};
pub struct CommandImpl;
@@ -17,12 +16,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256"))
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA384"))
.arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA512"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
let hmac_result = hmacutil::compute_yubikey_hmac(&challenge_bytes)?;

View File

@@ -0,0 +1,46 @@
use crate::util::base64_encode;
use crate::{cmdutil, ecutil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"convert-jwk-to-pem"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Convert PEM to JWK")
.arg(
Arg::with_name("jwk")
.long("jwk")
.required(true)
.takes_value(true)
.help("JWK"),
)
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let jwk = sub_arg_matches.value_of("jwk").unwrap();
let json_output = cmdutil::check_json_output(sub_arg_matches);
let (public_key_pem, public_ker_der) = ecutil::convert_ec_jwk_to_public_key(jwk)?;
let mut json = BTreeMap::<&'_ str, String>::new();
if json_output {
json.insert("public_key_pem", public_key_pem);
json.insert("public_key_base64", base64_encode(&public_ker_der));
util::print_pretty_json(&json);
} else {
information!("Public key PEM:\n{}", &public_key_pem);
information!("\nPublic key base64:\n{}", base64_encode(&public_ker_der));
}
Ok(None)
}
}

View File

@@ -0,0 +1,42 @@
use crate::{ecutil, rsautil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use serde_json::Value;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"convert-pem-to-jwk"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Convert PEM to JWK")
.arg(
Arg::with_name("public-key")
.long("public-key")
.required(true)
.takes_value(true)
.help("Public key (PEM, base64(DER) format)"),
)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let public_key = sub_arg_matches.value_of("public-key").unwrap();
let jwk = match ecutil::convert_ec_public_key_to_jwk(public_key) {
Ok(jwk) => jwk,
Err(_) => match rsautil::convert_rsa_to_jwk(public_key) {
Ok(jwk) => jwk,
Err(_) => return simple_error!("Invalid public key."),
},
};
let jwk_value: Value = serde_json::from_str(&jwk).unwrap();
util::print_pretty_json(&jwk_value);
Ok(None)
}
}

View File

@@ -2,10 +2,9 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::ecdsautil::EcdsaAlgorithm;
use crate::{argsutil, ecdsautil};
use crate::{argsutil, cmdutil, ecdsautil, util};
pub struct CommandImpl;
@@ -19,12 +18,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
let public_key = if let Some(public_key_hex) = sub_arg_matches.value_of("public-key-hex") {
@@ -55,7 +53,7 @@ impl Command for CommandImpl {
json.insert("signature_hex", hex::encode(&signature));
}
match ecdsautil::ecdsaverify(ecdsa_algorithm, &public_key, &hash_bytes, &signature) {
match ecdsautil::ecdsa_verify(ecdsa_algorithm, &public_key, &hash_bytes, &signature) {
Ok(_) => {
success!("Verify ECDSA succeed.");
if json_output {
@@ -72,7 +70,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

142
src/cmd_external_ecdh.rs Normal file
View File

@@ -0,0 +1,142 @@
use crate::keyutil::{parse_key_uri, KeyAlgorithmId, KeyUri, KeyUsage};
use crate::pivutil::ToStr;
use crate::{cmd_hmac_decrypt, cmd_se_ecdh, cmdutil, ecdhutil, mlkemutil, pivutil, seutil, util, yubikeyutil};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value;
use std::collections::BTreeMap;
use yubikey::piv::{decrypt_data, AlgorithmId};
use crate::util::try_decode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_ecdh"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("External ECDH subcommand")
.arg(cmdutil::build_parameter_arg())
.arg(cmdutil::build_epk_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let epk = sub_arg_matches.value_of("epk").unwrap();
let ephemeral_public_key_der_bytes = cmd_se_ecdh::parse_epk(epk)?;
let mut json = BTreeMap::new();
let key_uri = parse_key_uri(parameter)?;
match ecdh(&ephemeral_public_key_der_bytes, key_uri, sub_arg_matches) {
Ok(shared_secret_bytes) => {
json.insert("success", Value::Bool(true));
json.insert(
"shared_secret_hex",
hex::encode(&shared_secret_bytes).into(),
);
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json);
Ok(None)
}
}
pub fn ecdh(
ephemeral_public_key_bytes: &[u8],
key_uri: KeyUri,
sub_arg_matches: &ArgMatches,
) -> XResult<Vec<u8>> {
match key_uri {
KeyUri::SecureEnclave(key) => {
if key.usage != KeyUsage::Singing {
return simple_error!("Not singing key");
}
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?;
seutil::secure_enclave_p256_dh(&private_key, ephemeral_public_key_bytes)
}
KeyUri::YubikeyPiv(key) => {
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let pin_opt = pivutil::check_read_pin(&mut yk, key.slot, sub_arg_matches);
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let algorithm = opt_value_result!(
KeyAlgorithmId::to_algorithm_id(key.algorithm),
"Yubikey not supported algorithm: {}",
key.algorithm.to_str()
);
let epk_bytes = match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
return simple_error!("Algorithm is not supported: {:?}", algorithm)
}
AlgorithmId::EccP256 => {
use p256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey};
use spki::DecodePublicKey;
let public_key = opt_result!(PublicKey::from_public_key_der(
ephemeral_public_key_bytes),"Parse P256 ephemeral public key failed: {}");
public_key.to_encoded_point(false).as_bytes().to_vec()
}
AlgorithmId::EccP384 => {
use p384::{elliptic_curve::sec1::ToEncodedPoint, PublicKey};
use spki::DecodePublicKey;
let public_key = opt_result!(PublicKey::from_public_key_der(
ephemeral_public_key_bytes), "Parse P384 ephemeral public key failed: {}");
public_key.to_encoded_point(false).as_bytes().to_vec()
}
};
let decrypted_shared_secret = opt_result!(
decrypt_data(&mut yk, &epk_bytes, algorithm, key.slot,),
"Decrypt piv failed: {}"
);
Ok(decrypted_shared_secret.to_vec())
}
KeyUri::YubikeyHmacEncSoft(key) => {
if key.algorithm.is_ecc() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_bytes = try_decode(&private_key)?;
if let Ok(shared_secret) = ecdhutil::parse_p256_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret.to_vec());
}
if let Ok(shared_secret) = ecdhutil::parse_p384_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret.to_vec());
}
if let Ok(shared_secret) = ecdhutil::parse_p521_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret.to_vec());
}
simple_error!("Invalid EC private key and/or ephemeral public key")
} else if key.algorithm.is_mlkem() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_bytes = try_decode(&private_key)?;
if let Ok((_, shared_secret)) = mlkemutil::try_parse_decapsulate_key_private_then_decapsulate(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret);
}
simple_error!("Invalid ML-KEM private key and/or ephemeral public key")
} else {
simple_error!("Invalid algorithm: {}", key.algorithm.to_str())
}
}
KeyUri::ExternalCommand(key) => {
let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?;
external_command_rs::external_ecdh(&key.external_command, &parameter, ephemeral_public_key_bytes)
}
}
}

View File

@@ -0,0 +1,121 @@
use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage};
use crate::util::{base64_decode, base64_encode};
use crate::yubikeyutil::find_key_or_error;
use crate::{cmd_hmac_decrypt, cmdutil, ecdsautil, seutil, util, yubikeyutil};
use clap::{App, Arg, ArgMatches, SubCommand};
use ecdsa::elliptic_curve::pkcs8::der::Encode;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value;
use std::collections::BTreeMap;
use rsa::RsaPrivateKey;
use spki::EncodePublicKey;
use x509_parser::parse_x509_certificate;
use crate::mlkemutil::{try_parse_decapsulate_key_private_get_encapsulate, try_parse_encapsulation_key_public_then_encapsulate};
use crate::pivutil::ToStr;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_public_key"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("External public key subcommand")
.arg(cmdutil::build_parameter_arg())
.arg(cmdutil::build_serial_arg())
.arg(Arg::with_name("x-generate-shared-secret").long("x-generate-shared-secret").help("Generate a shared secret"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let serial_opt = sub_arg_matches.value_of("serial");
let generate_shared_secret = sub_arg_matches.is_present("x-generate-shared-secret");
let mut json = BTreeMap::new();
match fetch_public_key(parameter, &serial_opt, generate_shared_secret, &mut json) {
Ok(public_key_bytes) => {
json.insert("success", Value::Bool(true));
json.insert("public_key_base64", base64_encode(&public_key_bytes).into());
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json);
Ok(None)
}
}
fn fetch_public_key(parameter: &str, serial_opt: &Option<&str>, generate_shared_secret: bool, json: &mut BTreeMap<&str, Value>) -> XResult<Vec<u8>> {
let key_uri = parse_key_uri(parameter)?;
match key_uri {
KeyUri::SecureEnclave(key) => {
if key.usage != KeyUsage::Singing {
simple_error!("Not singing key")
} else {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?;
let (_, public_key_der, _) =
seutil::recover_secure_enclave_p256_public_key(&private_key, true)?;
Ok(public_key_der)
}
}
KeyUri::YubikeyPiv(key) => {
let mut yk = yubikeyutil::open_yubikey_with_serial(serial_opt)?;
if let Some(key) = find_key_or_error(&mut yk, &key.slot)? {
let cert_der = key.certificate().cert.to_der()?;
let x509_certificate = parse_x509_certificate(cert_der.as_slice()).unwrap().1;
let public_key_bytes = x509_certificate.public_key().raw;
return Ok(public_key_bytes.to_vec());
}
simple_error!("Slot {} not found", key.slot)
}
KeyUri::YubikeyHmacEncSoft(key) => {
if key.algorithm.is_ecc() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let p256_public_key = ecdsautil::parse_p256_private_key_to_public_key(&private_key).ok();
let p384_public_key = ecdsautil::parse_p384_private_key_to_public_key(&private_key).ok();
let p521_public_key = ecdsautil::parse_p521_private_key_to_public_key(&private_key).ok();
if let Some(p256_public_key) = p256_public_key {
return Ok(p256_public_key);
}
if let Some(p384_public_key) = p384_public_key {
return Ok(p384_public_key);
}
if let Some(p521_public_key) = p521_public_key {
return Ok(p521_public_key);
}
simple_error!("Invalid hmac enc private key")
} else if key.algorithm.is_rsa() {
use rsa::pkcs8::DecodePrivateKey;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_der = base64_decode(&private_key)?;
let rsa_private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der)?;
Ok(rsa_private_key.to_public_key().to_public_key_der()?.to_vec())
} else if key.algorithm.is_mlkem() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_der = base64_decode(&private_key)?;
let (_, ek_public) = try_parse_decapsulate_key_private_get_encapsulate(&private_key_der)?;
if generate_shared_secret {
if let Ok((mlkem_len, ciphertext, shared_secret)) = try_parse_encapsulation_key_public_then_encapsulate(&ek_public) {
json.insert("algorithm", mlkem_len.to_str().into());
json.insert("ciphertext", base64_encode(ciphertext).into());
json.insert("shared_secret", hex::encode(&shared_secret).into());
}
}
Ok(ek_public)
} else {
simple_error!("Invalid algorithm: {}", key.algorithm.to_str())
}
}
KeyUri::ExternalCommand(key) => {
let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?;
external_command_rs::external_public_key(&key.external_command, &parameter)
}
}
}

169
src/cmd_external_sign.rs Normal file
View File

@@ -0,0 +1,169 @@
use crate::cmd_sign_jwt_piv::digest_by_jwt_algorithm;
use crate::cmd_sign_jwt_soft::{convert_jwt_algorithm_to_ecdsa_algorithm, parse_ecdsa_private_key};
use crate::ecdsautil::EcdsaSignType;
use crate::keyutil::{parse_key_uri, KeyAlgorithmId, KeyUri, KeyUsage, YubikeyPivKey};
use crate::pivutil::ToStr;
use crate::rsautil::RsaSignAlgorithm;
use crate::util::{base64_decode, base64_encode};
use crate::{cmd_hmac_decrypt, cmdutil, ecdsautil, pivutil, rsautil, seutil, util, yubikeyutil};
use clap::{App, ArgMatches, SubCommand};
use jwt::AlgorithmType;
use rsa::RsaPrivateKey;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value;
use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::DigestType;
use yubikey::piv::sign_data;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_sign"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("External sign subcommand")
.arg(cmdutil::build_alg_arg())
.arg(cmdutil::build_parameter_arg())
.arg(cmdutil::build_message_arg())
.arg(cmdutil::build_message_type_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let alg = sub_arg_matches.value_of("alg").unwrap();
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let message_base64 = sub_arg_matches.value_of("message-base64").unwrap();
let message_type = sub_arg_matches.value_of("message-type");
let message_bytes = base64_decode(message_base64)?;
let mut json = BTreeMap::new();
let key_uri = parse_key_uri(parameter)?;
match sign(alg, &message_bytes, message_type, key_uri, sub_arg_matches) {
Ok(signature_bytes) => {
json.insert("success", Value::Bool(true));
json.insert("signature_base64", base64_encode(&signature_bytes).into());
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json);
Ok(None)
}
}
pub fn sign(alg: &str, message: &[u8], message_type: Option<&str>, key_uri: KeyUri, sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
let digest_type = DigestType::parse(message_type)?;
if let Some(bytes_len) = digest_type.bytes() {
if message.len() != bytes_len as usize {
return simple_error!("Invalid message length, requires: {}, actual: {}", bytes_len, message.len());
}
}
let is_raw = DigestType::Raw == digest_type;
match key_uri {
KeyUri::SecureEnclave(key) => {
if "ES256" != alg {
return simple_error!("Invalid alg: {}", alg);
}
if key.usage != KeyUsage::Singing {
return simple_error!("Not singing key");
}
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?;
seutil::secure_enclave_p256_sign(&private_key, message, digest_type)
}
KeyUri::YubikeyPiv(key) => {
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let pin_opt = pivutil::check_read_pin(&mut yk, key.slot, sub_arg_matches);
// FIXME Check YubiKey slot algorithm
let jwt_algorithm = get_jwt_algorithm(&key, alg)?;
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let algorithm = opt_value_result!(
KeyAlgorithmId::to_algorithm_id(key.algorithm),
"Yubikey not supported algorithm: {}",
key.algorithm.to_str()
);
let raw_in = iff!(is_raw, digest_by_jwt_algorithm(jwt_algorithm, message)?, message.to_vec());
let signed_data = opt_result!(
sign_data(&mut yk, &raw_in, algorithm, key.slot),
"Sign YubiKey failed: {}"
);
Ok(signed_data.to_vec())
}
KeyUri::YubikeyHmacEncSoft(key) => {
if key.algorithm.is_ecc() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let (jwt_algorithm, private_key_d) = parse_ecdsa_private_key(&private_key)?;
let raw_in = iff!(is_raw, digest_by_jwt_algorithm(jwt_algorithm, message)?, message.to_vec());
let ecdsa_algorithm = convert_jwt_algorithm_to_ecdsa_algorithm(jwt_algorithm)?;
let signed_data = ecdsautil::ecdsa_sign(
ecdsa_algorithm,
&private_key_d,
&raw_in,
EcdsaSignType::Der,
)?;
Ok(signed_data)
} else if key.algorithm.is_rsa() {
use rsa::pkcs8::DecodePrivateKey;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_der = base64_decode(&private_key)?;
let rsa_private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der)?;
let rsa_sign_algorithm =
opt_value_result!(RsaSignAlgorithm::from_str(alg), "Invalid --alg: {}", alg);
rsautil::sign(&rsa_private_key, rsa_sign_algorithm, message, is_raw)
} else {
simple_error!("Invalid algorithm: {}", key.algorithm.to_str())
}
}
KeyUri::ExternalCommand(key) => {
let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?;
let alg = key.algorithm.as_jwa_name();
let signature = external_command_rs::external_sign_digested(
&key.external_command, &parameter, alg, message, digest_type.to_str())?;
Ok(signature)
}
}
}
fn get_jwt_algorithm(key: &YubikeyPivKey, alg: &str) -> XResult<AlgorithmType> {
let jwt_algorithm = match alg {
"ES256" => AlgorithmType::Es256,
"ES384" => AlgorithmType::Es384,
"ES512" => AlgorithmType::Es512,
"RS256" => AlgorithmType::Rs256,
_ => return simple_error!("Invalid alg: {}", alg),
};
if key.algorithm == KeyAlgorithmId::Rsa1024 {
return simple_error!("Invalid algorithm: RSA1024");
}
let is_p256_mismatch =
key.algorithm == KeyAlgorithmId::EccP256 && jwt_algorithm != AlgorithmType::Es256;
let is_p384_mismatch =
key.algorithm == KeyAlgorithmId::EccP384 && jwt_algorithm != AlgorithmType::Es384;
let is_p521_mismatch =
key.algorithm == KeyAlgorithmId::EccP521 && jwt_algorithm != AlgorithmType::Es512;
let is_rsa_mismatch =
key.algorithm == KeyAlgorithmId::Rsa2048 && jwt_algorithm != AlgorithmType::Rs256;
if is_p256_mismatch || is_p384_mismatch || is_p521_mismatch || is_rsa_mismatch {
return simple_error!("Invalid algorithm: {} vs {}", key.algorithm.to_str(), alg);
}
Ok(jwt_algorithm)
}

40
src/cmd_external_spec.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::util;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use serde_json::Value;
use std::collections::BTreeMap;
pub struct CommandImpl;
// https://openwebstandard.org/rfc1
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_spec"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("External spec subcommand")
.arg(Arg::with_name("external-command").long("external-command").takes_value(true).help("External command"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let external_command_opt = sub_arg_matches.value_of("external-command");
if let Some(external_command) = external_command_opt {
let spec = external_command_rs::external_spec(external_command)?;
util::print_pretty_json(&spec);
} else {
let mut json = BTreeMap::new();
json.insert("success", Value::Bool(true));
json.insert(
"agent",
format!("card-external-provider/{}", env!("CARGO_PKG_VERSION")).into(),
);
json.insert("specification", "External/1.0.0-alpha".into());
json.insert("commands", vec!["external_public_key", "external_sign", "external_ecdh"].into());
util::print_pretty_json(&json);
}
Ok(None)
}
}

View File

@@ -10,8 +10,8 @@ use x509_parser::nom::AsBytes;
use yubikey::{Key, YubiKey};
use yubikey::piv::{sign_data, SlotId};
use crate::{argsutil, pinutil, pivutil};
use crate::digest::sha256_bytes;
use crate::{argsutil, cmdutil, pinutil, pivutil};
use crate::digestutil::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;
@@ -36,14 +36,13 @@ pub struct CommandImpl;
// all hex is in lower case default
// file ext: *.simple-sig
impl Command for CommandImpl {
fn name(&self) -> &str { "sign-file" }
fn name(&self) -> &str { "file-sign" }
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("no-pin").long("no-pin").help("No 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(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.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("sign-file").short("S").long("sign-file").takes_value(false).help("Sign file"))

View File

@@ -11,14 +11,14 @@ use x509_parser::public_key::PublicKey;
use x509_parser::time::ASN1Time;
use crate::argsutil;
use crate::digest::sha256_bytes;
use crate::digestutil::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 name(&self) -> &str { "file-verify" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV verify(with SHA256) subcommand")

View File

@@ -1,58 +0,0 @@
use crate::ecdsautil;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"generate-keypair"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Generate software keypair")
.arg(
Arg::with_name("type")
.long("type")
.required(true)
.takes_value(true)
.help("Key type (e.g. p256, p384)"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let key_type = sub_arg_matches.value_of("type").unwrap().to_lowercase();
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let (pkcs8_base64, secret_key_pem, public_key_pem) = match key_type.as_str() {
"p256" => ecdsautil::generate_p256_keypair()?,
"p384" => ecdsautil::generate_p384_keypair()?,
_ => {
return simple_error!("Key type must be p256 or p384");
}
};
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("private_key_base64", pkcs8_base64);
json.insert("private_key_pem", secret_key_pem);
json.insert("public_key_pem", public_key_pem);
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} else {
information!("Private key base64:\n{}\n", pkcs8_base64);
information!("Private key PEM:\n{}\n", secret_key_pem);
information!("Public key PEM:\n{}", public_key_pem);
}
Ok(None)
}
}

70
src/cmd_hmac_decrypt.rs Normal file
View File

@@ -0,0 +1,70 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use rust_util::XResult;
use crate::{cmdutil, pbeutil, util};
use crate::hmacutil::{hmac_decrypt_to_string, is_hmac_encrypted};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-decrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("YubiKey HMAC decrypt")
.arg(Arg::with_name("ciphertext").long("ciphertext").short("t").takes_value(true).required(true).help("Ciphertext"), )
.arg(Arg::with_name("auto-pbe").long("auto-pbe").help("Auto PBE decryption"))
.arg(Arg::with_name("password").long("password").short("P").takes_value(true).help("Password"))
.arg(Arg::with_name("outputs-password").long("outputs-password").help("Outputs password"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let ciphertext = sub_arg_matches.value_of("ciphertext").unwrap();
let mut pin_opt = sub_arg_matches.value_of("password").map(|p| p.to_string());
let auto_pbe = sub_arg_matches.is_present("auto-pbe");
let outputs_password = sub_arg_matches.is_present("outputs-password");
let text = try_decrypt_with_pbe_option(&mut pin_opt, ciphertext, auto_pbe)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("plaintext", text);
if let (true, Some(pin)) = (outputs_password, pin_opt.as_ref()) {
json.insert("password", pin.to_string());
}
util::print_pretty_json(&json);
} else {
success!("Plaintext: {}", text);
}
Ok(None)
}
}
pub fn try_decrypt(pin_opt: &mut Option<String>,ciphertext: &str) -> XResult<String> {
try_decrypt_with_pbe_option(pin_opt, ciphertext, true)
}
pub fn try_decrypt_with_pbe_option(pin_opt: &mut Option<String>, ciphertext: &str, auto_pbe: bool) -> XResult<String> {
if is_hmac_encrypted(ciphertext) {
hmac_decrypt(pin_opt, ciphertext, auto_pbe)
} else if pbeutil::is_simple_pbe_encrypted(ciphertext) {
pbeutil::simple_pbe_decrypt_with_prompt_to_string(pin_opt, ciphertext)
} else {
Ok(ciphertext.to_string())
}
}
pub fn hmac_decrypt(pin_opt: &mut Option<String>, ciphertext: &str, auto_pbe: bool) -> XResult<String> {
let text = hmac_decrypt_to_string(ciphertext)?;
if auto_pbe && pbeutil::is_simple_pbe_encrypted(&text) {
pbeutil::simple_pbe_decrypt_with_prompt_to_string(pin_opt, &text)
} else {
Ok(text)
}
}

81
src/cmd_hmac_encrypt.rs Normal file
View File

@@ -0,0 +1,81 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use rust_util::XResult;
use crate::{cmdutil, hmacutil, pbeutil, util};
use crate::pinutil::get_pin;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-encrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("YubiKey HMAC encrypt")
.arg(Arg::with_name("plaintext").long("plaintext").short("t").takes_value(true).required(true).help("Plaintext, @@PIN_ENTRY@@ means read from pin entry"))
.arg(Arg::with_name("password").long("password").short("P").takes_value(true).help("Password"))
.arg(cmdutil::build_with_pbe_encrypt_arg())
.arg(cmdutil::build_double_pin_check_arg())
.arg(cmdutil::build_pbe_iteration_arg())
.arg(Arg::with_name("without-hmac-encrypt").long("without-hmac-encrypt").help("Without HMAC encrypt"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let without_hmac_encrypt = sub_arg_matches.is_present("without-hmac-encrypt");
if without_hmac_encrypt && !sub_arg_matches.is_present("with-pbe-encrypt") {
return simple_error!("hmac and pbe encryption must present at least one");
}
let text = sub_arg_matches.value_of("plaintext").unwrap().to_string();
let text = if text == "@@PIN_ENTRY@@" {
match get_pin(None) {
None => return simple_error!(""),
Some(text) => text,
}
} else {
text
};
let mut pin_opt = sub_arg_matches.value_of("password").map(|p| p.to_string());
let ciphertext = do_encrypt(&text, &mut pin_opt, sub_arg_matches)?;
let ciphertext = if without_hmac_encrypt {
ciphertext
} else {
hmacutil::hmac_encrypt_from_string(&ciphertext)?
};
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("ciphertext", ciphertext);
util::print_pretty_json(&json);
} else {
success!("HMAC encrypt ciphertext: {}", ciphertext);
}
Ok(None)
}
}
pub fn do_encrypt(text: &str, password_opt: &mut Option<String>, sub_arg_matches: &ArgMatches) -> XResult<String> {
let with_hmac_encrypt = sub_arg_matches.is_present("with-hmac-encrypt");
let with_pbe_encrypt = sub_arg_matches.is_present("with-pbe-encrypt");
let text = if with_pbe_encrypt {
let double_pin_check = sub_arg_matches.is_present("double-pin-check");
let iteration = sub_arg_matches.value_of("pbe-iteration")
.map(|x| x.parse::<u32>().unwrap()).unwrap_or(100000);
pbeutil::simple_pbe_encrypt_with_prompt_from_string(iteration, text, password_opt, double_pin_check)?
} else {
text.to_string()
};
if with_hmac_encrypt {
Ok(hmacutil::hmac_encrypt_from_string(&text)?)
} else {
Ok(text)
}
}

View File

@@ -1,8 +1,8 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::{util_msg, XResult};
use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError};
use crate::hmacutil;
use crate::{cmdutil, hmacutil};
pub struct CommandImpl;
@@ -19,12 +19,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256"))
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA256"))
.arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA256"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let variable = sub_arg_matches.is_present("variable");
let secret_bytes = get_secret_bytes(sub_arg_matches)?;

View File

@@ -1,49 +0,0 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
use crate::hmacutil;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-decrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Yubikey HMAC decrypt")
.arg(
Arg::with_name("ciphertext")
.long("ciphertext")
.takes_value(true)
.help("Ciphertext"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let ciphertext = sub_arg_matches.value_of("ciphertext").unwrap();
let plaintext = hmacutil::hmac_decrypt_to_string(ciphertext)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("plaintext", plaintext);
println!(
"{}",
serde_json::to_string_pretty(&json).expect("Convert to JSON failed!")
);
} else {
success!("Plaintext: {}", plaintext);
}
Ok(None)
}
}

View File

@@ -1,49 +0,0 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
use crate::hmacutil;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-encrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Yubikey HMAC encrypt")
.arg(
Arg::with_name("plaintext")
.long("plaintext")
.takes_value(true)
.help("Plaintext"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let plaintext = sub_arg_matches.value_of("plaintext").unwrap();
let hmac_encrypt_ciphertext = hmacutil::hmac_encrypt_from_string(plaintext)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("ciphertext", hmac_encrypt_ciphertext);
println!(
"{}",
serde_json::to_string_pretty(&json).expect("Convert to JSON failed!")
);
} else {
success!("HMAC encrypt ciphertext: {}", hmac_encrypt_ciphertext);
}
Ok(None)
}
}

169
src/cmd_keypair_generate.rs Normal file
View File

@@ -0,0 +1,169 @@
use crate::ecdsautil::EcdsaAlgorithm;
use crate::keychain::{KeychainKey, KeychainKeyValue};
use crate::keyutil::{KeyAlgorithmId, KeyUri, YubikeyHmacEncSoftKey};
use crate::pivutil::FromStr;
use crate::util::base64_encode;
use crate::{cmd_hmac_encrypt, cmdutil, ecdsautil, hmacutil, mlkemutil, pbeutil, rsautil, util, yubikeyutil};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"keypair-generate"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Generate software keypair")
.arg(
Arg::with_name("type")
.long("type")
.required(true)
.takes_value(true)
.help("Key type (e.g. p256, p384, p521, rsa1024, rsa2048, rsa3072, rsa4096, mlkem512, mlkem768, mlkem1024)"),
)
.arg(cmdutil::build_with_hmac_encrypt_arg())
.arg(cmdutil::build_with_pbe_encrypt_arg())
.arg(cmdutil::build_double_pin_check_arg())
.arg(cmdutil::build_pbe_iteration_arg())
.arg(cmdutil::build_keychain_name_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let key_type = sub_arg_matches.value_of("type").unwrap().to_lowercase();
let keychain_name = sub_arg_matches.value_of("keychain-name");
if let Some(keychain_name) = keychain_name {
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
if keychain_key.get_password()?.is_some() {
return simple_error!("Keychain key URI: {} exists", keychain_key.to_key_uri());
}
}
let ecdsa_algorithm = match key_type.as_str() {
"p256" => Some(EcdsaAlgorithm::P256),
"p384" => Some(EcdsaAlgorithm::P384),
"p521" => Some(EcdsaAlgorithm::P521),
_ => None,
};
let rsa_bit_size: Option<usize> = match key_type.as_str() {
"rsa1024" => Some(1024),
"rsa2048" => Some(2048),
"rsa3072" => Some(3072),
"rsa4096" => Some(4096),
_ => None,
};
let mlkem_len: Option<usize> = match key_type.as_str() {
"mlkem512" => Some(512),
"mlkem768" => Some(768),
"mlkem1024" => Some(1024),
_ => None,
};
let (pkcs8_base64, secret_key_pem, public_key_pem, public_key_der, jwk_key) =
if let Some(ecdsa_algorithm) = ecdsa_algorithm {
ecdsautil::generate_ecdsa_keypair(ecdsa_algorithm)?
} else if let Some(rsa_bit_size) = rsa_bit_size {
rsautil::generate_rsa_keypair(rsa_bit_size)?
} else if let Some(mlkem_len) = mlkem_len {
mlkemutil::generate_mlkem_keypair(mlkem_len)?
} else {
return simple_error!("Unsupported key type: {}", key_type);
};
let mut password_opt = None;
let (pkcs8_base64, secret_key_pem) = (
cmd_hmac_encrypt::do_encrypt(&pkcs8_base64, &mut password_opt, sub_arg_matches)?,
cmd_hmac_encrypt::do_encrypt(&secret_key_pem, &mut password_opt, sub_arg_matches)?,
);
let public_key_base64 = base64_encode(&public_key_der);
let keychain_key_uri = if let Some(keychain_name) = keychain_name {
let keychain_key_value = KeychainKeyValue {
keychain_name: keychain_name.to_string(),
pkcs8_base64: pkcs8_base64.clone(),
secret_key_pem: secret_key_pem.clone(),
public_key_pem: public_key_pem.clone(),
public_key_jwk: jwk_key.clone(),
};
let keychain_key_value_json = serde_json::to_string(&keychain_key_value)?;
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
keychain_key.set_password(keychain_key_value_json.as_bytes())?;
Some(keychain_key.to_key_uri())
} else {
None
};
let algorithm_id = KeyAlgorithmId::from_str(&key_type);
let with_encrypt = hmacutil::is_hmac_encrypted(&pkcs8_base64)
|| pbeutil::is_simple_pbe_encrypted(&pkcs8_base64);
let yubikey_hmac_enc_soft_key_uri =
if let (true, Some(algorithm_id)) = (with_encrypt, algorithm_id) {
let yubikey_name = match yubikeyutil::open_yubikey() {
Ok(yk) => format!("yubikey{}-{}", yk.version().major, yk.serial().0),
Err(_) => "yubikey-unknown".to_string(),
};
let yubikey_hmac_enc_soft_key = YubikeyHmacEncSoftKey {
key_name: yubikey_name,
algorithm: algorithm_id,
hmac_enc_private_key: pkcs8_base64.clone(),
};
Some(KeyUri::YubikeyHmacEncSoft(yubikey_hmac_enc_soft_key).to_string())
} else {
None
};
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
match keychain_key_uri {
None => {
json.insert("private_key_base64", pkcs8_base64);
json.insert("private_key_pem", secret_key_pem);
if let Some(yubikey_hmac_enc_soft_key_uri) = yubikey_hmac_enc_soft_key_uri {
json.insert("key_uri", yubikey_hmac_enc_soft_key_uri.to_string());
}
}
Some(keychain_key_uri) => {
json.insert("keychain_key_uri", keychain_key_uri);
}
}
json.insert("public_key_pem", public_key_pem);
json.insert("public_key_base64", public_key_base64);
if !jwk_key.is_empty() {
json.insert("public_key_jwk", jwk_key);
}
util::print_pretty_json(&json);
} else {
match keychain_key_uri {
None => {
information!("Private key base64:\n{}\n", pkcs8_base64);
information!("Private key PEM:\n{}\n", secret_key_pem);
if let Some(yubikey_hmac_enc_soft_key_uri) = yubikey_hmac_enc_soft_key_uri {
information!("Key URI:\n{}\n", yubikey_hmac_enc_soft_key_uri);
}
}
Some(keychain_key_uri) => {
information!("Keychain key URI:\n{}\n", keychain_key_uri);
}
}
information!("Public key PEM:\n{}", public_key_pem);
information!("Public key Base64:\n{}\n", public_key_base64);
if !jwk_key.is_empty() {
information!("Public key JWK:\n{}", jwk_key);
}
}
Ok(None)
}
}

View File

@@ -0,0 +1,38 @@
use crate::{cmdutil, util};
use crate::keychain::{KeychainKey, KeychainKeyValue};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"keypair-keychain-export"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Export software keypair from keychain")
.arg(cmdutil::build_keychain_name_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let keychain_name = sub_arg_matches.value_of("keychain-name");
if let Some(keychain_name) = keychain_name {
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
if let Some(keychain_key_value_bytes) = keychain_key.get_password()? {
let keychain_key_value: KeychainKeyValue =
serde_json::from_slice(&keychain_key_value_bytes)?;
util_msg::set_logger_std_out(false);
information!("Keychain key URI: {}", keychain_key.to_key_uri());
util::print_pretty_json(&keychain_key_value);
} else {
return simple_error!("Keychain key URI: {} not found", keychain_key.to_key_uri());
}
}
Ok(None)
}
}

View File

@@ -0,0 +1,42 @@
use crate::cmdutil;
use crate::keychain::KeychainKey;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"keypair-keychain-import"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Import software keypair to keychain")
.arg(cmdutil::build_keychain_name_arg())
.arg(
Arg::with_name("import-key-value")
.long("import-key-value")
.takes_value(true)
.help("Import key value"),
)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let keychain_name = sub_arg_matches.value_of("keychain-name");
let import_key_value = sub_arg_matches.value_of("import-key-value");
if let Some(keychain_name) = keychain_name {
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
if keychain_key.get_password()?.is_some() {
return simple_error!("Keychain key URI: {} exists", keychain_key.to_key_uri());
}
if let Some(import_key_value) = import_key_value {
keychain_key.set_password(import_key_value.as_bytes())?;
}
}
Ok(None)
}
}

View File

@@ -1,9 +1,9 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::YubiKey;
use serde_json::Value;
use crate::{cmdutil, util, yubikeyutil};
pub struct CommandImpl;
@@ -12,34 +12,37 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("YubiKey list")
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("name", yk.name().to_string());
json.insert("version", yk.version().to_string());
json.insert("serial", yk.serial().0.to_string());
let mut json = BTreeMap::<&'_ str, Value>::new();
json.insert("name", yk.name().into());
json.insert("version", yk.version().to_string().into());
json.insert("serial", yk.serial().0.into());
if let Ok(pin_retries) = yk.get_pin_retries() {
json.insert("pin_retries", pin_retries.to_string());
json.insert("pin_retries", pin_retries.into());
}
if let Ok(chuid) = yk.chuid() {
json.insert("chuid", chuid.to_string());
json.insert("chuid", chuid.to_string().into());
}
if let Ok(ccuid) = yk.cccid() {
json.insert("ccuid", ccuid.to_string());
json.insert("ccuid", ccuid.to_string().into());
}
if let Ok(piv_keys) = yk.piv_keys() {
json.insert("keys", piv_keys.iter().map(|k| format!("{}", k.slot())).collect::<Vec<_>>().join(", "));
let key_list = piv_keys.iter().map(|k| Value::String(format!("{}", k.slot()))).collect::<Vec<_>>();
json.insert("key_list", key_list.into());
let keys = piv_keys.iter().map(|k| format!("{}", k.slot())).collect::<Vec<_>>().join(", ");
json.insert("keys", keys.into());
}
println!("{}", serde_json::to_string_pretty(&json).expect("Convert to JSON failed!"));
util::print_pretty_json(&json);
} else {
success!("Name: {}", yk.name());
success!("Version: {}", yk.version());

View File

@@ -3,8 +3,7 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::{cmdutil, util};
use crate::ecdsautil::parse_ecdsa_r_and_s;
use crate::util::try_decode;
@@ -25,14 +24,11 @@ impl Command for CommandImpl {
.takes_value(true)
.help("ECDSA signature"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut json = BTreeMap::<&'_ str, String>::new();
@@ -55,7 +51,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

View File

@@ -1,5 +1,3 @@
use std::ops::Deref;
use chrono::{DateTime, Local};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
@@ -21,7 +19,7 @@ impl Command for CommandImpl {
.arg(Arg::with_name("in").short("i").long("in").takes_value(true).help("File input, *.pgp or *.asc"))
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("verbose").long("verbose").help("Verbose output"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
// .arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -151,7 +149,7 @@ impl Command for CommandImpl {
debugging!("Found PKESK: {:?}", pkesk);
match pkesk {
PKESK::V3(pkesk3) => {
information!("Found public key encrypted session key, key ID: {}, alog: {}", pkesk3.recipient(), pkesk3.pk_algo());
information!("Found public key encrypted session key, key ID: {:?}, alog: {}", pkesk3.recipient(), pkesk3.pk_algo());
}
unknown => warning!("Unknown PKESK: {:?}", unknown),
}
@@ -162,19 +160,21 @@ impl Command for CommandImpl {
Packet::SEIP(seip) => {
debugging!("Found SEIP: {:?}", seip);
match seip {
SEIP::V1(seip1) => match seip1.deref().body() {
SEIP::V1(seip1) => match seip1.body() {
Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()),
Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()),
}
SEIP::V2(seip2) => match seip2.body() {
Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()),
Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()),
}
_ => {}
}
}
#[allow(deprecated)]
Packet::MDC(mdc) => {
information!("Found MDC: {:?}", mdc);
}
Packet::AED(aed) => {
information!("Found AED: {:?}", aed);
}
Packet::Unknown(unknown) => {
warning!("Found unknown: {:?}", unknown);
}

View File

@@ -1,12 +1,12 @@
use bech32::{ToBase32, Variant};
use clap::{App, Arg, ArgMatches, SubCommand};
use clap::{App, ArgMatches, SubCommand};
use openpgp_card::algorithm::{Algo, Curve};
use openpgp_card::crypto_data::{EccType, PublicKeyMaterial};
use openpgp_card::{KeyType, OpenPgp};
use openpgp_card_pcsc::PcscBackend;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
use crate::{cmdutil, util};
const AGE_PUBLIC_KEY_PREFIX: &str = "age";
@@ -17,12 +17,11 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card encryption key to age address")
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
@@ -67,7 +66,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&cards_output).unwrap());
util::print_pretty_json(&cards_output);
}
Ok(None)
}

View File

@@ -2,10 +2,10 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::crypto_data::Cryptogram;
use rust_util::{util_msg, XResult};
use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError};
use crate::{pgpcardutil, pinutil};
use crate::{cmdutil, pgpcardutil, pinutil, util};
use crate::util::{base64_encode, read_stdin, try_decode};
#[derive(Debug, Clone, Copy)]
@@ -36,12 +36,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("ciphertext").short("c").long("ciphertext").takes_value(true).help("Cipher text (HEX or Base64)"))
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(Arg::with_name("algo").long("algo").takes_value(true).help("Algo: RSA, X25519/ECDH"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
let pin_opt = pinutil::get_pin(pin_opt);
@@ -89,7 +88,7 @@ impl Command for CommandImpl {
json.insert("text_utf8", text);
}
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

View File

@@ -4,8 +4,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::{KeyType, OpenPgp};
use openpgp_card_pcsc::PcscBackend;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::{cmdutil, util};
use crate::pkiutil::openpgp_card_public_key_pem as public_key_pem;
pub struct CommandImpl;
@@ -16,13 +15,13 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card list subcommand")
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let detail_output = sub_arg_matches.is_present("detail");
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let mut jsons = vec![];
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
@@ -136,7 +135,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&jsons).unwrap());
util::print_pretty_json(&jsons);
}
Ok(None)
}

View File

@@ -1,15 +1,15 @@
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{ErrorKind, Read};
use std::ops::Deref;
use clap::{App, Arg, ArgMatches, SubCommand};
use digest::Digest;
use openpgp_card::crypto_data::Hash;
use rust_util::{util_msg, XResult};
use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError};
use sha2::{Sha256, Sha384, Sha512};
use crate::{pgpcardutil, pinutil};
use crate::{cmdutil, pgpcardutil, pinutil, util};
use crate::util::base64_encode;
const BUFF_SIZE: usize = 512 * 1024;
@@ -49,12 +49,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("use-sha384").long("use-sha384").help("Use SHA384 for file in"))
.arg(Arg::with_name("use-sha512").long("use-sha512").help("Use SHA512 for file in"))
.arg(Arg::with_name("algo").long("algo").takes_value(true).help("Algorithm, rsa, ecdsa, eddsa"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
let pin_opt = pinutil::get_pin(pin_opt);
@@ -115,7 +114,7 @@ impl Command for CommandImpl {
if let Some(sha256) = sha256 {
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}");
let sha256_hex = crate::digest::copy_sha256(&sha256_hex)?;
let sha256_hex = crate::digestutil::copy_sha256(&sha256_hex)?;
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!");
let sig = match algo {
@@ -134,7 +133,7 @@ impl Command for CommandImpl {
}
if let Some(sha384) = sha384 {
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}");
let sha384_hex = crate::digest::copy_sha384(&sha384_hex)?;
let sha384_hex = crate::digestutil::copy_sha384(&sha384_hex)?;
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!");
let sig = match algo {
@@ -153,7 +152,7 @@ impl Command for CommandImpl {
}
if let Some(sha512) = sha512 {
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}");
let sha512_hex = crate::digest::copy_sha512(&sha512_hex)?;
let sha512_hex = crate::digestutil::copy_sha512(&sha512_hex)?;
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!");
let sig = match algo {
@@ -172,7 +171,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
@@ -190,7 +189,7 @@ where
debugging!("File: {}, length: {}", file_name, file_len);
loop {
let len = match f.read(&mut buf) {
Ok(0) => return Ok(hasher.finalize().as_slice().to_vec()),
Ok(0) => return Ok(hasher.finalize().deref().to_vec()),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return simple_error!("Calc file digest failed: {}", e),

View File

@@ -10,7 +10,7 @@ use spki::der::Encode;
use x509_parser::parse_x509_certificate;
use yubikey::{Certificate, YubiKey};
use yubikey::piv::SlotId;
use crate::{cmdutil, yubikeyutil};
use crate::pivutil::get_algorithm_id;
use crate::pkiutil::{bytes_to_pem, get_pki_algorithm};
@@ -23,6 +23,7 @@ impl Command for CommandImpl {
SubCommand::with_name(self.name()).about("PIV subcommand")
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("show-config").long("show-config").help("Show config output"))
.arg(cmdutil::build_serial_arg())
// .arg(Arg::with_name("json").long("json").help("JSON output"))
}
@@ -30,7 +31,7 @@ impl Command for CommandImpl {
let detail_output = sub_arg_matches.is_present("detail");
let show_config = sub_arg_matches.is_present("show-config");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
success!("Name: {}", yk.name());
information!("Version: {}", yk.version());
information!("Serial: {}", yk.serial());

View File

@@ -2,11 +2,9 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::piv::AlgorithmId;
use yubikey::YubiKey;
use crate::{pinutil, pivutil};
use crate::{cmdutil, pinutil, pivutil, util, yubikeyutil};
use crate::util::{read_stdin, try_decode};
pub struct CommandImpl;
@@ -16,17 +14,17 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV decrypt(RSA) subcommand")
.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("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("ciphertext").long("ciphertext").short("c").takes_value(true).help("Encrypted data (HEX or Base64)"))
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
@@ -40,7 +38,7 @@ impl Command for CommandImpl {
return simple_error!("Argument --ciphertext must be assigned");
};
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
@@ -75,7 +73,8 @@ impl Command for CommandImpl {
json.insert("decrypted_data_hex", hex::encode(decrypted_data_bytes));
json.insert("clear_data_hex", hex::encode(clear_data));
json.insert("clear_data", String::from_utf8_lossy(clear_data).to_string());
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

View File

@@ -4,11 +4,10 @@ use std::fs;
use clap::{App, Arg, ArgMatches, SubCommand};
use rand::rngs::OsRng;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::{PinPolicy, YubiKey};
use yubikey::PinPolicy;
use yubikey::piv::{AlgorithmId, decrypt_data, metadata};
use crate::{ecdhutil, pinutil, pivutil};
use crate::{cmdutil, ecdhutil, pinutil, pivutil, util, yubikeyutil};
use crate::pivutil::get_algorithm_id;
pub struct CommandImpl;
@@ -18,9 +17,9 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV ECDH subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ..."))
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("public-256").long("public-256").help("Public key (P-256)"))
.arg(Arg::with_name("public-384").long("public-384").help("Public key (P-384)"))
.arg(Arg::with_name("private").long("private").help("Private key(PIV)"))
@@ -28,12 +27,12 @@ impl Command for CommandImpl {
.arg(Arg::with_name("public-key").long("public-key").takes_value(true).help("Public key"))
.arg(Arg::with_name("public-key-file").long("public-key-file").takes_value(true).help("Public key"))
.arg(Arg::with_name("public-key-point-hex").long("public-key-point-hex").takes_value(true).help("Public key point hex"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let public256 = sub_arg_matches.is_present("public-256");
let public384 = sub_arg_matches.is_present("public-384");
@@ -76,7 +75,7 @@ impl Command for CommandImpl {
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ...");
let epk = opt_value_result!(sub_arg_matches.value_of("epk"), "--epk must assigned");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let slot_id = pivutil::get_slot_id(slot)?;
debugging!("Slot id: {}", slot_id);
if let Ok(meta) = metadata(&mut yk, slot_id) {
@@ -128,7 +127,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

View File

@@ -2,13 +2,12 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use x509_parser::nom::AsBytes;
use yubikey::piv::{metadata, sign_data, AlgorithmId, ManagementAlgorithmId};
use yubikey::YubiKey;
use crate::util::base64_encode;
use crate::{argsutil, pivutil};
use crate::{argsutil, cmdutil, pivutil, util, yubikeyutil};
use crate::digestutil::DigestAlgorithm;
pub struct CommandImpl;
@@ -16,32 +15,32 @@ impl Command for CommandImpl {
fn name(&self) -> &str { "piv-ecsign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV EC 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("no-pin").long("no-pin").help("No 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"))
SubCommand::with_name(self.name()).about("PIV EC sign(with SHA256/SHA384) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("algorithm").short("a").long("algorithm").takes_value(true).help("Algorithm, p256 or p384"))
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut json = BTreeMap::<&'_ str, String>::new();
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
let (algorithm, algorithm_str) = match sub_arg_matches.value_of("algorithm") {
None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256"),
Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha256"),
let (algorithm, algorithm_str, digest_algorithm) = match sub_arg_matches.value_of("algorithm") {
None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256", DigestAlgorithm::Sha256),
Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha384", DigestAlgorithm::Sha384),
Some(unknown_algorithm) => return simple_error!("Unknown algorithm {}, e.g. p256 or p384", unknown_algorithm),
};
let hash_bytes = argsutil::get_digest_or_hash(sub_arg_matches, digest_algorithm)?;
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let slot_id = pivutil::get_slot_id(slot)?;
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches);
@@ -82,7 +81,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

View File

@@ -1,10 +1,9 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::{PinPolicy, piv, TouchPolicy, YubiKey};
use yubikey::{PinPolicy, piv, TouchPolicy};
use yubikey::piv::{AlgorithmId, SlotId};
use crate::pinutil;
use crate::{cmdutil, pinutil, yubikeyutil};
pub struct CommandImpl;
@@ -13,15 +12,13 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV generate subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(cmdutil::build_pin_arg())
.arg(Arg::with_name("force").long("force").help("Force generate"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_serial_arg())
// .arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
warning!("This feature is not works");
let pin_opt = sub_arg_matches.value_of("pin");
let pin_opt = pinutil::get_pin(pin_opt);
@@ -32,7 +29,7 @@ impl Command for CommandImpl {
failure_and_exit!("--force must be assigned");
}
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
let public_key_info = opt_result!(piv::generate(&mut yk,SlotId::Signature, AlgorithmId::Rsa2048,
@@ -40,7 +37,6 @@ impl Command for CommandImpl {
success!("Generate key success: {:?}", public_key_info);
Ok(None)
}
}

View File

@@ -1,15 +1,16 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use clap::{App, ArgMatches, SubCommand};
use p256::pkcs8::der::Encode;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use rust_util::util_msg::MessageType;
use x509_parser::parse_x509_certificate;
use yubikey::{Key, YubiKey};
use yubikey::Key;
use yubikey::piv::{AlgorithmId, metadata};
use crate::pivutil;
use crate::{cmdutil, pivutil, util, yubikeyutil};
use crate::keyutil::{KeyAlgorithmId, KeyUri, YubikeyPivKey};
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::pkiutil::bytes_to_pem;
use crate::sshutil::SshVecWriter;
@@ -22,19 +23,19 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV meta subcommand")
.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("json").long("json").help("JSON output"))
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut json = BTreeMap::<&'_ str, String>::new();
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 mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let slot_id = pivutil::get_slot_id(slot)?;
json.insert("slot", pivutil::to_slot_hex(&slot_id));
@@ -97,11 +98,18 @@ impl Command for CommandImpl {
ssh_public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes());
ssh_public_key.write_string(pk_point_hex);
let ssh_public_key_str = format!(
"ecdsa-sha2-nistp{} {} PIV:{}", ec_bit_len, base64_encode(ssh_public_key), slot_id);
"ecdsa-sha2-nistp{} {} YubiKey-PIV-{}", ec_bit_len, base64_encode(ssh_public_key), slot_id);
json.insert("ssh_public_key", ssh_public_key_str.to_string());
}
_ => {}
}
let yubikey_piv_key = YubikeyPivKey {
key_name: format!("yubikey{}-{}", yk.version().major, yk.serial().0),
algorithm: KeyAlgorithmId::from_algorithm_id(algorithm_id),
slot: slot_id,
};
json.insert("key_uri", KeyUri::YubikeyPiv(yubikey_piv_key).to_string());
}
let serial_lower = cert.serial_number.to_string().to_lowercase();
json.insert("serial", if serial_lower.starts_with("00:") { serial_lower.chars().skip(3).collect() } else { serial_lower });
@@ -135,7 +143,7 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

View File

@@ -2,11 +2,10 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::{piv, YubiKey};
use yubikey::piv;
use yubikey::piv::{AlgorithmId, SlotId};
use crate::{pinutil, pivutil, rsautil};
use crate::{cmdutil, pinutil, pivutil, rsautil, util, yubikeyutil};
use crate::util::base64_encode;
pub struct CommandImpl;
@@ -16,22 +15,22 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV RSA sign(with SHA256) subcommand")
.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("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 HEX"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pin_opt = pinutil::read_pin(sub_arg_matches);
let sha256_hex_opt = sub_arg_matches.value_of("sha256").map(|s| s.to_string());
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
@@ -55,7 +54,8 @@ impl Command for CommandImpl {
json.insert("hash_hex", hex::encode(&sha256));
json.insert("sign_hex", hex::encode(sign_bytes));
json.insert("sign_base64", base64_encode(sign_bytes));
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
} else {
success!("Signature HEX: {}", hex::encode(sign_bytes));
success!("Signature base64: {}", base64_encode(sign_bytes));

View File

@@ -1,6 +1,6 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_msg, XResult};
use rust_util::XResult;
use serde::Serialize;
use serde_json::{Map, Value};
use spki::der::Encode;
@@ -9,7 +9,7 @@ use tabled::{Table, Tabled};
use x509_parser::parse_x509_certificate;
use yubikey::piv::{metadata, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::{cmdutil, util, yubikeyutil};
use crate::pivutil::{get_algorithm_id_by_certificate, ToStr, ORDERED_SLOTS};
const NA: &str = "N/A";
@@ -35,20 +35,21 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV subcommand")
.arg(Arg::with_name("table").long("table").help("Show table"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(Arg::with_name("all").long("all").help("Show all"))
.arg(Arg::with_name("ordered").long("ordered").help("Show ordered"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let show_table = sub_arg_matches.is_present("table");
let show_all = sub_arg_matches.is_present("all");
let show_ordered = sub_arg_matches.is_present("ordered");
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let mut output = Map::new();
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
success!("Name: {}", yk.name());
information!("Version: {}", yk.version());
@@ -99,7 +100,7 @@ impl Command for CommandImpl {
output.insert("piv_slots".to_string(), Value::Array(piv_slots_values));
}
if json_output {
println!("{}", serde_json::to_string_pretty(&output).unwrap());
util::print_pretty_json(&output);
}
Ok(None)

View File

@@ -2,14 +2,11 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::rsa::{Padding, Rsa};
use rust_util::{util_msg, XResult};
use rust_util::util_clap::{Command, CommandError};
use yubikey::{Key, YubiKey};
use yubikey::piv::{AlgorithmId, SlotId};
use yubikey::piv::AlgorithmId;
use crate::{argsutil, ecdsautil, pivutil};
use crate::{argsutil, cmdutil, ecdsautil, pivutil, util, yubikeyutil};
use crate::ecdsautil::EcdsaAlgorithm;
use crate::pivutil::slot_equals;
pub struct CommandImpl;
@@ -18,17 +15,17 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV verify subcommand")
.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(cmdutil::build_slot_arg())
.arg(Arg::with_name("signature-hex").short("t").long("signature-hex").takes_value(true).help("Signature"))
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
let signature = if let Some(signature_hex) = sub_arg_matches.value_of("signature-hex") {
@@ -43,7 +40,7 @@ impl Command for CommandImpl {
let slot_id = pivutil::get_slot_id(slot)?;
json.insert("slot", pivutil::to_slot_hex(&slot_id));
if let Some(key) = find_key(&slot_id)? {
if let Some(key) = yubikeyutil::open_and_find_key(&slot_id, sub_arg_matches)? {
let certificate = key.certificate();
let tbs_certificate = &certificate.cert.tbs_certificate;
if let Ok(algorithm_id) = pivutil::get_algorithm_id_by_certificate(certificate) {
@@ -61,7 +58,7 @@ impl Command for CommandImpl {
}
let algorithm = iff!(algorithm_id == AlgorithmId::EccP256, EcdsaAlgorithm::P256, EcdsaAlgorithm::P384);
match ecdsautil::ecdsaverify(algorithm, pk_point, &hash_bytes, &signature) {
match ecdsautil::ecdsa_verify(algorithm, pk_point, &hash_bytes, &signature) {
Ok(_) => {
success!("Verify ECDSA succeed.");
if json_output {
@@ -100,22 +97,8 @@ impl Command for CommandImpl {
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}
}
fn find_key(slot_id: &SlotId) -> XResult<Option<Key>> {
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
match Key::list(&mut yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => for k in keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(slot_id, &slot_str) {
return Ok(Some(k));
}
},
}
Ok(None)
}

View File

@@ -8,7 +8,7 @@ use openssl::rsa::Rsa;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use rust_util::util_msg::MessageType;
use crate::{cmdutil, util};
use crate::util::{read_stdin, try_decode};
pub struct CommandImpl;
@@ -25,12 +25,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(Arg::with_name("padding").long("padding").takes_value(true)
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pri_key_in = opt_value_result!(sub_arg_matches.value_of("pri-key-in"), "Require private key in");
let pri_key_bytes = opt_result!(std::fs::read(pri_key_in), "Read file: {}, failed: {}", pri_key_in);
@@ -39,8 +38,6 @@ impl Command for CommandImpl {
let padding = crate::rsautil::parse_padding(padding_opt)?;
let padding_str = crate::rsautil::padding_to_string(padding);
let mut json = BTreeMap::new();
let keypair = opt_result!(Rsa::private_key_from_pem(&pri_key_bytes), "Parse RSA failed: {}");
let keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey failed: {}");
@@ -81,13 +78,12 @@ impl Command for CommandImpl {
success!("Message HEX: {}", hex::encode(&data));
success!("Message: {}", String::from_utf8_lossy(&data));
if json_output {
let mut json = BTreeMap::new();
json.insert("data", hex::encode(&data));
json.insert("padding", padding_str.to_string());
json.insert("encrypted", encrypted_hex);
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)

View File

@@ -6,9 +6,8 @@ use openssl::encrypt::Encrypter;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::digest::sha256_bytes;
use crate::{cmdutil, util};
use crate::digestutil::sha256_bytes;
pub struct CommandImpl;
@@ -23,12 +22,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("data-hex").long("data-hex").takes_value(true).help("Data in HEX"))
.arg(Arg::with_name("padding").long("padding").takes_value(true)
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pub_key_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in");
let pub_key_bytes = opt_result!(fs::read(pub_key_in), "Read file: {}, failed: {}", pub_key_in);
@@ -37,8 +35,6 @@ impl Command for CommandImpl {
let padding = crate::rsautil::parse_padding(padding_opt)?;
let padding_str = crate::rsautil::padding_to_string(padding);
let mut json = BTreeMap::new();
let keypair = opt_result!(Rsa::public_key_from_pem(&pub_key_bytes), "Parse RSA failed: {}");
let pub_key_der = opt_result!(keypair.public_key_to_der(), "RSA public key to der failed: {}");
let pub_key_fingerprint = hex::encode(sha256_bytes(&pub_key_der));
@@ -66,14 +62,13 @@ impl Command for CommandImpl {
information!("Public key fingerprint: {}", pub_key_fingerprint);
success!("Encrypted message: {}", encrypted_hex);
if json_output {
let mut json = BTreeMap::new();
json.insert("data", hex::encode(&data));
json.insert("public_key_fingerprint", pub_key_fingerprint);
json.insert("padding", padding_str.to_string());
json.insert("encrypted", encrypted_hex);
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)

View File

@@ -12,7 +12,7 @@ use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg::MessageType;
use rust_util::{util_msg, XResult};
use crate::digest::sha256_bytes;
use crate::digestutil::sha256_bytes;
pub struct CommandImpl;

View File

@@ -1,6 +1,7 @@
use crate::seutil;
use crate::{cmdutil, seutil, util};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
pub struct CommandImpl;
@@ -10,15 +11,28 @@ impl Command for CommandImpl {
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Secure Enclave subcommand")
// .arg(Arg::with_name("json").long("json").help("JSON output"))
SubCommand::with_name(self.name())
.about("Secure Enclave subcommand")
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, _sub_arg_matches: &ArgMatches) -> CommandError {
if seutil::is_support_se() {
success!("Secure Enclave is supported.")
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
if which::which("swift-secure-enclave-tool").is_err() {
failure!("Secure Enclave tool not found.");
}
if json_output {
let mut json = BTreeMap::new();
json.insert("se_supported", seutil::is_support_se());
util::print_pretty_json(&json);
} else {
failure!("Secure Enclave is NOT supported.")
success!(
"Secure Enclave is {}supported.",
iff!(seutil::is_support_se(), "", "NOT ")
);
}
Ok(None)
}

View File

@@ -1,12 +1,13 @@
use crate::keyutil::{parse_key_uri, KeyUri};
use crate::seutil;
use clap::{App, Arg, ArgMatches, SubCommand};
use crate::keyutil::parse_key_uri;
use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
use clap::{App, ArgMatches, SubCommand};
use p256::elliptic_curve::sec1::FromEncodedPoint;
use p256::{EncodedPoint, PublicKey};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use spki::EncodePublicKey;
use std::collections::BTreeMap;
use rust_util::XResult;
use crate::util::base64_decode;
pub struct CommandImpl;
@@ -18,39 +19,46 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave ECDH subcommand")
.arg(
Arg::with_name("key")
.long("key")
.required(true)
.takes_value(true)
.help("Key uri"),
)
.arg(
Arg::with_name("epk")
.long("epk")
.required(true)
.takes_value(true)
.help("E-Public key"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_key_uri_arg())
.arg(cmdutil::build_epk_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() {
return simple_error!("Secure Enclave is NOT supported.");
}
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap();
let epk = sub_arg_matches.value_of("epk").unwrap();
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let ephemeral_public_key_der_bytes = if epk.starts_with("04") {
let ephemeral_public_key_der_bytes = parse_epk(epk)?;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let dh = seutil::secure_enclave_p256_dh(
&private_key,
&ephemeral_public_key_der_bytes,
)?;
let dh_hex = hex::encode(&dh);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("shared_secret_hex", dh_hex);
util::print_pretty_json(&json);
} else {
information!("Shared secret: {}", dh_hex);
}
Ok(None)
}
}
pub fn parse_epk(epk: &str) -> XResult<Vec<u8>> {
if epk.starts_with("04") {
let ephemeral_public_key_point_bytes = opt_result!(
hex::decode(epk),
"Decode public key point from hex failed: {}"
@@ -64,26 +72,14 @@ impl Command for CommandImpl {
return simple_error!("Parse public key failed.");
}
let public_key = public_key_opt.unwrap();
public_key.to_public_key_der()?.as_bytes().to_vec()
Ok(public_key.to_public_key_der()?.as_bytes().to_vec())
} else {
opt_result!(hex::decode(epk), "Decode public key from hex failed: {}")
};
let dh = seutil::secure_enclave_p256_dh(
&se_key_uri.private_key,
&ephemeral_public_key_der_bytes,
)?;
let dh_hex = hex::encode(&dh);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("shared_secret_hex", dh_hex);
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} else {
information!("Shared secret: {}", dh_hex);
match hex::decode(epk) {
Ok(epk_bytes) => Ok(epk_bytes),
Err(e) => match base64_decode(epk) {
Ok(epk_bytes) => Ok(epk_bytes),
Err(_) => simple_error!("Decode public key from hex failed: {}", e)
}
}
Ok(None)
}
}

View File

@@ -1,10 +1,10 @@
use crate::keyutil::{parse_key_uri, KeyUri};
use crate::seutil;
use crate::keyutil::parse_key_uri;
use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
use crate::util::{base64_decode, base64_encode};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::DigestType;
pub struct CommandImpl;
@@ -16,13 +16,7 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave EC sign subcommand")
.arg(
Arg::with_name("key")
.long("key")
.required(true)
.takes_value(true)
.help("Key uri"),
)
.arg(cmdutil::build_key_uri_arg())
.arg(
Arg::with_name("input")
.short("i")
@@ -36,13 +30,13 @@ impl Command for CommandImpl {
.takes_value(true)
.help("Input in base64"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() {
return simple_error!("Secure Enclave is NOT supported.");
}
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap();
let input_bytes = match sub_arg_matches.value_of("input") {
None => match sub_arg_matches.value_of("input-base64") {
@@ -51,22 +45,20 @@ impl Command for CommandImpl {
},
Some(input) => input.as_bytes().to_vec(),
};
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let signature = seutil::secure_enclave_p256_sign(&se_key_uri.private_key, &input_bytes)?;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let signature = seutil::secure_enclave_p256_sign(&private_key, &input_bytes, DigestType::Raw)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("signature_base64", base64_encode(&signature));
json.insert("signature_hex", hex::encode(&signature));
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
} else {
success!("Signature: {}", base64_encode(&signature));
}

View File

@@ -1,10 +1,13 @@
use crate::cmd_hmac_encrypt;
use crate::pkiutil::bytes_to_pem;
use crate::seutil;
use crate::util::base64_encode;
use crate::{cmdutil, seutil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use p256::PublicKey;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use spki::DecodePublicKey;
use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::{ControlFlag, KeyMlKem};
pub struct CommandImpl;
@@ -30,50 +33,105 @@ impl Command for CommandImpl {
.takes_value(true)
.help("Host name"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(
Arg::with_name("control-flag")
.long("control-flag")
.required(true)
.takes_value(true)
.help("Control flag, e.g. none, user-presence, device-passcode, biometry-any, biometry-current-set"),
)
.arg(
Arg::with_name("algorithm")
.long("algorithm")
.required(true)
.takes_value(true)
.default_value("p256")
.help("Algorithm, e.g. p256, mlkem768, mlkem1024"),
)
.arg(cmdutil::build_with_hmac_encrypt_arg())
.arg(cmdutil::build_with_pbe_encrypt_arg())
.arg(cmdutil::build_double_pin_check_arg())
.arg(cmdutil::build_pbe_iteration_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() {
return simple_error!("Secure Enclave is NOT supported.");
}
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let ty = sub_arg_matches.value_of("type").unwrap();
let host = sub_arg_matches.value_of("host").unwrap_or("macbook");
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let sign = match ty {
"signing" | "ecsign" | "sign" => true,
"key_agreement" | "ecdh" | "dh" => false,
_ => return simple_error!("Invalid type: {}", ty),
};
let control_flag = sub_arg_matches.value_of("control-flag").unwrap();
let control_flag = match control_flag {
"none" => ControlFlag::None,
"user-presence" | "up" => ControlFlag::UserPresence,
"device-passcode" | "passcode" | "pass" => ControlFlag::DevicePasscode,
"biometry-any" | "bio-any" => ControlFlag::BiometryAny,
"biometry-current-set" | "bio-current" => ControlFlag::BiometryCurrentSet,
_ => return simple_error!("Invalid control flag: {}", control_flag),
};
let algorithm = sub_arg_matches.value_of("algorithm").unwrap();
let (public_key_point, public_key_der, private_key) = match algorithm {
"p256" => {
seutil::generate_secure_enclave_p256_keypair(sign, control_flag)?
}
"mlkem768" | "mlkem1024" => {
if sign {
return simple_error!("Algorithm: {} only supports key_agreement", algorithm);
}
seutil::generate_secure_enclave_mlkem_keypair(
iff!(algorithm == "mlkem768", KeyMlKem::MlKem768, KeyMlKem::MlKem1024), control_flag)?
}
_ => return simple_error!("Unknown algorithm: {}", algorithm),
};
let (public_key_point, public_key_der, private_key) =
seutil::generate_secure_enclave_p256_keypair(sign)?;
let public_key_point_hex = hex::encode(&public_key_point);
let public_key_pem = bytes_to_pem("PUBLIC KEY", &*public_key_der);
let key = format!(
"key://{}:se/p256:{}:{}",
let private_key = cmd_hmac_encrypt::do_encrypt(&private_key, &mut None, sub_arg_matches)?;
let key_uri = format!(
"key://{}:se/{}:{}:{}",
host,
algorithm,
iff!(sign, "signing", "key_agreement"),
private_key,
);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("public_key_point", public_key_point_hex);
json.insert("public_key_pem", base64_encode(&*public_key_der));
json.insert("key", key);
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} else {
success!("Public key(point): {}", public_key_point_hex);
success!("Public key PEM: \n{}", public_key_pem);
success!("Key: {}", key);
}
print_se_key(json_output, &public_key_point, &public_key_der, &key_uri);
Ok(None)
}
}
pub fn print_se_key(
json_output: bool,
public_key_point: &[u8],
public_key_der: &[u8],
key_uri: &str,
) {
let public_key_point_hex = hex::encode(public_key_point);
let public_key_pem = bytes_to_pem("PUBLIC KEY", public_key_der);
let public_key = PublicKey::from_public_key_pem(&public_key_pem).ok();
let public_key_jwk = public_key.map(|key| key.to_jwk_string());
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("public_key_point", public_key_point_hex);
json.insert("public_key_pem", base64_encode(public_key_der));
if let Some(public_key_jwk) = public_key_jwk {
json.insert("public_key_jwk", base64_encode(public_key_jwk));
}
json.insert("key_uri", key_uri.to_string());
util::print_pretty_json(&json);
} else {
success!("Public key(point): {}", public_key_point_hex);
success!("Public key PEM: \n{}", public_key_pem);
if let Some(public_key_jwk) = public_key_jwk {
success!("Public key JWK: \n{}", &public_key_jwk);
}
success!("Key: {}", key_uri);
}
}

View File

@@ -1,11 +1,8 @@
use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage};
use crate::pkiutil::bytes_to_pem;
use crate::seutil;
use crate::util::base64_encode;
use clap::{App, Arg, ArgMatches, SubCommand};
use crate::cmd_se_generate::print_se_key;
use crate::keyutil::{parse_key_uri, KeyUsage};
use crate::{cmd_hmac_decrypt, cmdutil, seutil};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
pub struct CommandImpl;
@@ -17,50 +14,27 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave recover subcommand")
.arg(
Arg::with_name("key")
.long("key")
.required(true)
.takes_value(true)
.help("Key uri"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_key_uri_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() {
return simple_error!("Secure Enclave is NOT supported.");
}
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap();
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let (public_key_point, public_key_der, _private_key) =
seutil::recover_secure_enclave_p256_public_key(
&se_key_uri.private_key,
&private_key,
se_key_uri.usage == KeyUsage::Singing,
)?;
let public_key_point_hex = hex::encode(&public_key_point);
let public_key_pem = bytes_to_pem("PUBLIC KEY", &*public_key_der);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("public_key_point", public_key_point_hex);
json.insert("public_key_pem", base64_encode(&*public_key_der));
json.insert("key", key.to_string());
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} else {
success!("Public key(point): {}", public_key_point_hex);
success!("Public key PEM: \n{}", public_key_pem);
success!("Key: {}", key);
}
print_se_key(json_output, &public_key_point, &public_key_der, key);
Ok(None)
}

85
src/cmd_sign_jwt.rs Normal file
View File

@@ -0,0 +1,85 @@
use crate::cmd_sign_jwt_piv::{
build_jwt_parts, fill_sign_jwt_app_args, merge_header_claims,
merge_payload_claims, print_jwt_token,
};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::keyutil::parse_key_uri;
use crate::{cmd_external_sign, cmdutil, util};
use clap::{App, ArgMatches, SubCommand};
use jwt::ToBase64;
use jwt::{AlgorithmType, Header};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::{Map, Value};
use crate::pivutil::ToStr;
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name())
.about("Sign JWT subcommand")
.arg(cmdutil::build_key_uri_arg().required(false))
.arg(cmdutil::build_parameter_arg().required(false))
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_serial_arg())
.arg(cmdutil::build_json_arg());
fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let token_string = sign_jwt(header, &payload, &jwt_claims, sub_arg_matches)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
fn sign_jwt(
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
sub_arg_matches: &ArgMatches,
) -> XResult<String> {
let key = match sub_arg_matches.value_of("key") {
Some(key) => key,
None => match sub_arg_matches.value_of("parameter") {
Some(parameter) => parameter,
None => return simple_error!("Parameter --key or --parameter required"),
}
};
let key_uri = parse_key_uri(key)?;
let jwt_algorithm = key_uri.get_preferred_algorithm_type();
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let signature = cmd_external_sign::sign(jwt_algorithm.to_str(), &tobe_signed, None, key_uri, sub_arg_matches)?;
let signed_data = match jwt_algorithm {
AlgorithmType::Rs256 => signature,
AlgorithmType::Es256 | AlgorithmType::Es384 | AlgorithmType::Es512 => {
parse_ecdsa_to_rs(signature.as_slice())?
}
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}

302
src/cmd_sign_jwt_piv.rs Normal file
View File

@@ -0,0 +1,302 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use jwt::header::HeaderType;
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_time, XResult};
use serde_json::{Map, Number, Value};
use yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::{cmdutil, digestutil, pivutil, rsautil, util};
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-piv"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name()).about("Sign JWT(PIV) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(cmdutil::build_json_arg());
fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let slot = opt_value_result!(
sub_arg_matches.value_of("slot"),
"--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"
);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let mut yk = opt_result!(YubiKey::open(), "Find YubiKey failed: {}");
let slot_id = opt_result!(pivutil::get_slot_id(slot), "Get slot id failed: {}");
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches);
let token_string = sign_jwt(&mut yk, slot_id, &pin_opt, header, &payload, &jwt_claims)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
pub fn print_jwt_token(json_output: bool, token_string: String) {
if json_output {
debugging!("Singed JWT: {}", token_string);
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("token", token_string.clone());
util::print_pretty_json(&json);
} else {
success!("Singed JWT: {}", token_string);
}
}
pub fn fill_sign_jwt_app_args<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
app.arg(Arg::with_name("key-id").short("K").long("key-id").takes_value(true).help("Header key ID"))
.arg(Arg::with_name("claims").short("C").long("claims").takes_value(true).multiple(true).help("Claims, key:value"))
.arg(Arg::with_name("payload").short("P").long("payload").takes_value(true).help("Claims in JSON"))
.arg(Arg::with_name("jti").long("jti").help("Claims jti"))
.arg(Arg::with_name("validity").long("validity").takes_value(true)
.help("Claims validity period e.g. 10m means 10 minutes (s - second, m - minute, h - hour, d - day)"))
}
fn sign_jwt(
yk: &mut YubiKey,
slot_id: SlotId,
pin_opt: &Option<String>,
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
) -> XResult<String> {
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let cert = match Certificate::read(yk, slot_id) {
Ok(c) => c,
Err(e) => return simple_error!("Read YubiKey certificate failed: {}", e),
};
let piv_algorithm_id =
pivutil::get_algorithm_id(&cert.cert.tbs_certificate.subject_public_key_info)?;
let (jwt_algorithm, yk_algorithm) = match piv_algorithm_id {
AlgorithmId::Rsa1024 => return simple_error!("RSA 1024 bits not supported."),
AlgorithmId::Rsa2048 => (AlgorithmType::Rs256, AlgorithmId::Rsa2048),
AlgorithmId::EccP256 => (AlgorithmType::Es256, AlgorithmId::EccP256),
AlgorithmId::EccP384 => (AlgorithmType::Es384, AlgorithmId::EccP384),
};
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let raw_in = digest_by_jwt_algorithm(jwt_algorithm, &tobe_signed)?;
let signed_data = opt_result!(
sign_data(yk, &raw_in, yk_algorithm, slot_id),
"Sign YubiKey failed: {}"
);
let signed_data = match jwt_algorithm {
AlgorithmType::Rs256 => signed_data.to_vec(),
AlgorithmType::Es256 | AlgorithmType::Es384 => parse_ecdsa_to_rs(signed_data.as_slice())?,
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}
pub fn digest_by_jwt_algorithm(jwt_algorithm: AlgorithmType, tobe_signed: &[u8]) -> XResult<Vec<u8>> {
Ok(match jwt_algorithm {
AlgorithmType::Rs256 => {
rsautil::pkcs15_sha256_rsa_2048_padding_for_sign(&digestutil::sha256_bytes(tobe_signed))
}
AlgorithmType::Es256 => digestutil::sha256_bytes(tobe_signed),
AlgorithmType::Es384 => digestutil::sha384_bytes(tobe_signed),
AlgorithmType::Es512 => digestutil::sha512_bytes(tobe_signed),
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
})
}
pub fn merge_header_claims(header: &[u8], claims: &[u8]) -> Vec<u8> {
let mut tobe_signed = vec![];
tobe_signed.extend_from_slice(header);
tobe_signed.extend_from_slice(SEPARATOR.as_bytes());
tobe_signed.extend_from_slice(claims);
tobe_signed
}
pub fn merge_payload_claims<'a>(
payload: &'a Option<String>,
claims: &'a Map<String, Value>,
) -> XResult<Cow<'a, str>> {
Ok(match (payload, claims.is_empty()) {
(Some(payload), true) => {
Cow::Owned(util::base64_encode_url_safe_no_pad(payload.as_bytes()))
}
(_, _) => opt_result!(claims.to_base64(), "Claims to base64 failed: {}"),
})
}
pub fn build_jwt_parts(
sub_arg_matches: &ArgMatches,
) -> XResult<(Header, Option<String>, Map<String, Value>)> {
let key_id = sub_arg_matches.value_of("key-id");
let claims = sub_arg_matches.values_of("claims");
let payload = sub_arg_matches.value_of("payload");
let validity = sub_arg_matches.value_of("validity");
let jti = sub_arg_matches.is_present("jti");
let header = Header {
key_id: key_id.map(ToString::to_string),
type_: Some(HeaderType::JsonWebToken),
..Default::default()
};
let mut jwt_claims = Map::new();
if let Some(payload) = payload {
match serde_json::from_str::<Value>(payload) {
Ok(Value::Object(claims_map)) => {
claims_map.into_iter().for_each(|(k, v)| {
jwt_claims.insert(k, v);
});
}
Ok(value) => {
warning!("Not valid payload map: {}", value);
}
Err(e) => {
warning!("Not valid payload value: {}", e);
}
};
}
match (payload, claims) {
(Some(_), None) => {}
(_, Some(claims)) => {
for claim in claims {
match split_claim(claim) {
None => {
warning!("Claim '{}' do not contains ':'", claim);
}
Some((k, v)) => {
match jwt_claims.get_mut(&k) {
None => { jwt_claims.insert(k, v); },
Some(val) => {
match val {
Value::Array(arr) => {
arr.push(v);
}
_ => {
let mut arr = vec![];
arr.push(val.clone());
arr.push(v);
jwt_claims.insert(k, Value::Array(arr));
}
}
}
}
// jwt_claims.insert(k, v);
}
}
}
if !jwt_claims.contains_key("sub") {
return simple_error!("Claim sub is not assigned.");
}
}
_ => return simple_error!("Payload or Claims is required."),
}
// set jti, iat and sub
if jti && !jwt_claims.contains_key("jti") {
jwt_claims.insert(
"jti".to_string(),
Value::String(format!("jti-{}", util_time::get_current_millis())),
);
}
if let Some(validity) = validity {
match util_time::parse_duration(validity) {
None => {
warning!("Bad validity: {}", validity)
}
Some(validity) => {
let current_secs = (util_time::get_current_millis() / 1000) as u64;
jwt_claims.insert("iat".to_string(), Value::Number(Number::from(current_secs)));
jwt_claims.insert(
"exp".to_string(),
Value::Number(Number::from(current_secs + validity.as_secs())),
);
}
}
}
Ok((header, payload.map(ToString::to_string), jwt_claims))
}
pub fn split_claim(claim: &str) -> Option<(String, Value)> {
let mut k = String::new();
let mut v = String::new();
let mut claim_chars = claim.chars().peekable();
let ty = if let Some('^') = claim_chars.peek() {
let _ = claim_chars.next();
claim_chars.next()
} else {
None
};
let mut is_k = true;
for c in claim_chars {
if is_k {
if c == ':' {
is_k = false;
} else {
k.push(c);
}
} else {
v.push(c);
}
}
if is_k {
return None;
}
match ty {
None | Some('s') => Some((k, Value::String(v))),
Some('b') => Some((k, Value::Bool(["true", "yes", "1"].contains(&v.as_str())))),
Some('i') | Some('n') => {
if let Ok(i) = v.parse::<i64>() {
return Some((k, Value::Number(Number::from(i))));
}
if let Ok(f) = v.parse::<f64>() {
if let Some(number_f64) = Number::from_f64(f) {
return Some((k, Value::Number(number_f64)));
}
}
warning!("Bad number: {} in claim: {}", v, claim);
None
}
_ => {
warning!("Unknown type: {} in claim: {}", ty.unwrap(), claim);
None
}
}
}

74
src/cmd_sign_jwt_se.rs Normal file
View File

@@ -0,0 +1,74 @@
use clap::{App, ArgMatches, SubCommand};
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::{Map, Value};
use crate::cmd_sign_jwt_piv::{build_jwt_parts, merge_header_claims, merge_payload_claims, print_jwt_token};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::keyutil::parse_key_uri;
use crate::{cmd_hmac_decrypt, cmd_sign_jwt_piv, cmdutil, util};
use crate::util::base64_decode;
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-se"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name()).about("Sign JWT(SE) subcommand")
.arg(cmdutil::build_key_uri_arg())
.arg(cmdutil::build_json_arg());
cmd_sign_jwt_piv::fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let private_key = opt_value_result!(
sub_arg_matches.value_of("key"),
"Private key PKCS#8 DER base64 encoded or PEM"
);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, private_key)?;
let key_uri = parse_key_uri(&private_key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let token_string = sign_jwt(&se_key_uri.private_key, header, &payload, &jwt_claims)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
fn sign_jwt(
private_key: &str,
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
) -> XResult<String> {
header.algorithm = AlgorithmType::Es256;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let private_key_representation = base64_decode(private_key)?;
let signed_data_der =
swift_secure_enclave_tool_rs::private_key_sign(&private_key_representation, &tobe_signed)?;
let signed_data = parse_ecdsa_to_rs(signed_data_der.as_slice())?;
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}

109
src/cmd_sign_jwt_soft.rs Normal file
View File

@@ -0,0 +1,109 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::{Map, Value};
use crate::cmd_sign_jwt_piv::{build_jwt_parts, digest_by_jwt_algorithm, merge_header_claims, merge_payload_claims, print_jwt_token};
use crate::keychain::{KeychainKey, KeychainKeyValue};
use crate::{cmd_hmac_decrypt, cmd_sign_jwt_piv, cmdutil, ecdsautil, keychain, util};
use crate::ecdsautil::{EcdsaAlgorithm, EcdsaSignType};
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-soft"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name()).about("Sign JWT(Soft EC) subcommand")
.arg(Arg::with_name("private-key").short("k").long("private-key").takes_value(true).help("Private key PKCS#8"))
.arg(cmdutil::build_json_arg());
cmd_sign_jwt_piv::fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let private_key = opt_value_result!(
sub_arg_matches.value_of("private-key"),
"Private key PKCS#8 DER base64 encoded or PEM"
);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, private_key)?;
let private_key = if keychain::is_keychain_key_uri(&private_key) {
debugging!("Private key keychain key URI: {}", &private_key);
let keychain_key = KeychainKey::parse_key_uri(&private_key)?;
let keychain_key_value_bytes = opt_value_result!(
keychain_key.get_password()?,
"Keychain key URI: {} not found",
&private_key
);
let keychain_key_value: KeychainKeyValue =
serde_json::from_slice(&keychain_key_value_bytes)?;
debugging!("Keychain key value {:?}", &keychain_key_value);
keychain_key_value.pkcs8_base64
} else {
private_key
};
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
fn sign_jwt(
private_key: &str,
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
) -> XResult<String> {
let (jwt_algorithm, private_key_d) = parse_ecdsa_private_key(private_key)?;
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let raw_in = digest_by_jwt_algorithm(jwt_algorithm, &tobe_signed)?;
let ecdsa_algorithm = convert_jwt_algorithm_to_ecdsa_algorithm(jwt_algorithm)?;
let signed_data = ecdsautil::ecdsa_sign(ecdsa_algorithm, &private_key_d, &raw_in, EcdsaSignType::Rs)?;
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}
pub fn convert_jwt_algorithm_to_ecdsa_algorithm(jwt_algorithm: AlgorithmType) -> XResult<EcdsaAlgorithm> {
match jwt_algorithm {
AlgorithmType::Es256 => Ok(EcdsaAlgorithm::P256),
AlgorithmType::Es384 => Ok(EcdsaAlgorithm::P384),
AlgorithmType::Es512 => Ok(EcdsaAlgorithm::P521),
_ => simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
}
}
pub fn parse_ecdsa_private_key(private_key: &str) -> XResult<(AlgorithmType, Vec<u8>)> {
let p256_private_key_d = ecdsautil::parse_p256_private_key(private_key).ok();
let p384_private_key_d = ecdsautil::parse_p384_private_key(private_key).ok();
let p521_private_key_d = ecdsautil::parse_p521_private_key(private_key).ok();
let (jwt_algorithm, private_key_d) = match (p256_private_key_d, p384_private_key_d, p521_private_key_d) {
(Some(p256_private_key_d), None, None) => (AlgorithmType::Es256, p256_private_key_d),
(None, Some(p384_private_key_d), None) => (AlgorithmType::Es384, p384_private_key_d),
(None, None, Some(p521_private_key_d)) => (AlgorithmType::Es512, p521_private_key_d),
_ => return simple_error!("Invalid private key: {}", private_key),
};
Ok((jwt_algorithm, private_key_d))
}

View File

@@ -1,216 +0,0 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use jwt::{AlgorithmType, Header, ToBase64};
use jwt::header::HeaderType;
use rust_util::{util_msg, util_time, XResult};
use rust_util::util_clap::{Command, CommandError};
use serde_json::{Map, Number, Value};
use yubikey::{Certificate, YubiKey};
use yubikey::piv::{AlgorithmId, sign_data, SlotId};
use crate::{digest, pivutil, rsautil, util};
use crate::ecdsautil::parse_ecdsa_to_rs;
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "sign-jwt" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Sign JWT subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(Arg::with_name("no-pin").long("no-pin").help("No 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("key-id").short("K").long("key-id").takes_value(true).help("Header key ID"))
.arg(Arg::with_name("claims").short("C").long("claims").takes_value(true).multiple(true).help("Claims, key:value"))
.arg(Arg::with_name("payload").short("P").long("payload").takes_value(true).help("Claims in JSON"))
.arg(Arg::with_name("jti").long("jti").help("Claims jti"))
.arg(Arg::with_name("validity").long("validity").takes_value(true).help("Claims validity period e.g. 10m means 10 minutes (s - second, m - minute, h - hour, d - day)"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let mut json = BTreeMap::<&'_ str, String>::new();
let slot = opt_value_result!(
sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let key_id = sub_arg_matches.value_of("key-id");
let claims = sub_arg_matches.values_of("claims");
let payload = sub_arg_matches.value_of("payload");
let validity = sub_arg_matches.value_of("validity");
let jti = sub_arg_matches.is_present("jti");
let header = Header {
key_id: key_id.map(ToString::to_string),
type_: Some(HeaderType::JsonWebToken),
..Default::default()
};
let mut jwt_claims = Map::new();
if let Some(payload) = payload {
match serde_json::from_str::<Value>(payload) {
Ok(Value::Object(claims_map)) => {
claims_map.into_iter().for_each(|(k, v)| {
jwt_claims.insert(k, v);
});
}
Ok(value) => { warning!("Not valid payload map: {}", value); }
Err(e) => { warning!("Not valid payload value: {}", e); }
};
}
match (payload, claims) {
(Some(_), None) => {}
(_, Some(claims)) => {
for claim in claims {
match split_claim(claim) {
None => { warning!("Claim '{}' do not contains ':'", claim); }
Some((k, v)) => { jwt_claims.insert(k, v); }
}
}
if !jwt_claims.contains_key("sub") {
return simple_error!("Claim sub is not assigned.");
}
}
_ => return simple_error!("Payload or Claims is required."),
}
// set jti, iat and sub
if jti && !jwt_claims.contains_key("jti") {
jwt_claims.insert("jti".to_string(), Value::String(format!("jti-{}", util_time::get_current_millis())));
}
if let Some(validity) = validity {
match util_time::parse_duration(validity) {
None => { warning!("Bad validity: {}", validity) }
Some(validity) => {
let current_secs = (util_time::get_current_millis() / 1000) as u64;
jwt_claims.insert("iat".to_string(), Value::Number(Number::from(current_secs)));
jwt_claims.insert("exp".to_string(), Value::Number(Number::from(current_secs + validity.as_secs())));
}
}
}
let mut yk = opt_result!(YubiKey::open(), "Find YubiKey failed: {}");
let slot_id = opt_result!(pivutil::get_slot_id(slot), "Get slot id failed: {}");
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches);
let token_string = sign_jwt(&mut yk, slot_id, &pin_opt, header, &payload, &jwt_claims)?;
debugging!("Singed JWT: {}", token_string);
if json_output { json.insert("token", token_string.clone()); }
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None)
}
}
fn sign_jwt(yk: &mut YubiKey, slot_id: SlotId, pin_opt: &Option<String>, mut header: Header, payload: &Option<&str>, claims: &Map<String, Value>) -> XResult<String> {
if let Some(pin) = pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
let cert = match Certificate::read(yk, slot_id) {
Ok(c) => c,
Err(e) => return simple_error!("Read YubiKey certificate failed: {}", e),
};
let piv_algorithm_id = pivutil::get_algorithm_id(&cert.cert.tbs_certificate.subject_public_key_info)?;
let (jwt_algorithm, yk_algorithm) = match piv_algorithm_id {
AlgorithmId::Rsa1024 => return simple_error!("RSA 1024 bits not supported."),
AlgorithmId::Rsa2048 => (AlgorithmType::Rs256, AlgorithmId::Rsa2048),
AlgorithmId::EccP256 => (AlgorithmType::Es256, AlgorithmId::EccP256),
AlgorithmId::EccP384 => (AlgorithmType::Es384, AlgorithmId::EccP384),
};
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = match (payload, claims.is_empty()) {
(Some(payload), true) => Cow::Owned(util::base64_encode_url_safe_no_pad(payload.as_bytes())),
(_, _) => opt_result!(claims.to_base64(), "Claims to base64 failed: {}"),
};
let mut tobe_signed = vec![];
tobe_signed.extend_from_slice(header.as_bytes());
tobe_signed.extend_from_slice(SEPARATOR.as_bytes());
tobe_signed.extend_from_slice(claims.as_bytes());
let raw_in = match jwt_algorithm {
AlgorithmType::Rs256 => rsautil::pkcs15_sha256_rsa_2048_padding_for_sign(
&digest::sha256_bytes(&tobe_signed)),
AlgorithmType::Es256 => digest::sha256_bytes(&tobe_signed),
AlgorithmType::Es384 => digest::sha384_bytes(&tobe_signed),
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signed_data = opt_result!(
sign_data(yk, &raw_in, yk_algorithm, slot_id), "Sign YubiKey failed: {}");
let signed_data = match jwt_algorithm {
AlgorithmType::Rs256 => signed_data.to_vec(),
AlgorithmType::Es256 | AlgorithmType::Es384 => parse_ecdsa_to_rs(signed_data.as_slice())?,
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}
pub fn split_claim(claim: &str) -> Option<(String, Value)> {
let mut k = String::new();
let mut v = String::new();
let mut claim_chars = claim.chars().peekable();
let ty = if let Some('^') = claim_chars.peek() {
let _ = claim_chars.next();
claim_chars.next()
} else {
None
};
let mut is_k = true;
for c in claim_chars {
if is_k {
if c == ':' {
is_k = false;
} else {
k.push(c);
}
} else {
v.push(c);
}
}
if is_k {
return None;
}
match ty {
None | Some('s') => Some((k, Value::String(v))),
Some('b') => Some((k, Value::Bool(["true", "yes", "1"].contains(&v.as_str())))),
Some('i') | Some('n') => {
if let Ok(i) = v.parse::<i64>() {
return Some((k, Value::Number(Number::from(i))));
}
if let Ok(f) = v.parse::<f64>() {
if let Some(number_f64) = Number::from_f64(f) {
return Some((k, Value::Number(number_f64)));
}
}
warning!("Bad number: {} in claim: {}", v, claim);
None
}
_ => {
warning!("Unknown type: {} in claim: {}", ty.unwrap(), claim);
None
}
}
}

View File

@@ -1,180 +0,0 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use jwt::header::HeaderType;
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_msg, util_time, XResult};
use serde_json::{Map, Number, Value};
use crate::{cmd_signjwt, digest, ecdsautil, hmacutil, rsautil, util};
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-soft"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Sign JWT subcommand")
.arg(Arg::with_name("private-key").short("k").long("private-key").takes_value(true).help("Private key PKCS#8"))
.arg(Arg::with_name("key-id").short("K").long("key-id").takes_value(true).help("Header key ID"))
.arg(Arg::with_name("claims").short("C").long("claims").takes_value(true).multiple(true).help("Claims, key:value"))
.arg(Arg::with_name("payload").short("P").long("payload").takes_value(true).help("Claims in JSON"))
.arg(Arg::with_name("jti").long("jti").help("Claims jti"))
.arg(Arg::with_name("validity").long("validity").takes_value(true).help("Claims validity period e.g. 10m means 10 minutes (s - second, m - minute, h - hour, d - day)"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let mut json = BTreeMap::<&'_ str, String>::new();
let private_key = opt_value_result!(
sub_arg_matches.value_of("private-key"),
"Private key PKCS#8 DER base64 encoded or PEM"
);
let private_key = hmacutil::try_hmac_decrypt_to_string(private_key)?;
let key_id = sub_arg_matches.value_of("key-id");
let claims = sub_arg_matches.values_of("claims");
let payload = sub_arg_matches.value_of("payload");
let validity = sub_arg_matches.value_of("validity");
let jti = sub_arg_matches.is_present("jti");
let header = Header {
key_id: key_id.map(ToString::to_string),
type_: Some(HeaderType::JsonWebToken),
..Default::default()
};
let mut jwt_claims = Map::new();
if let Some(payload) = payload {
match serde_json::from_str::<Value>(payload) {
Ok(Value::Object(claims_map)) => {
claims_map.into_iter().for_each(|(k, v)| {
jwt_claims.insert(k, v);
});
}
Ok(value) => {
warning!("Not valid payload map: {}", value);
}
Err(e) => {
warning!("Not valid payload value: {}", e);
}
};
}
match (payload, claims) {
(Some(_), None) => {}
(_, Some(claims)) => {
for claim in claims {
match cmd_signjwt::split_claim(claim) {
None => {
warning!("Claim '{}' do not contains ':'", claim);
}
Some((k, v)) => {
jwt_claims.insert(k, v);
}
}
}
if !jwt_claims.contains_key("sub") {
return simple_error!("Claim sub is not assigned.");
}
}
_ => return simple_error!("Payload or Claims is required."),
}
// set jti, iat and sub
if jti && !jwt_claims.contains_key("jti") {
jwt_claims.insert(
"jti".to_string(),
Value::String(format!("jti-{}", util_time::get_current_millis())),
);
}
if let Some(validity) = validity {
match util_time::parse_duration(validity) {
None => {
warning!("Bad validity: {}", validity)
}
Some(validity) => {
let current_secs = (util_time::get_current_millis() / 1000) as u64;
jwt_claims.insert("iat".to_string(), Value::Number(Number::from(current_secs)));
jwt_claims.insert(
"exp".to_string(),
Value::Number(Number::from(current_secs + validity.as_secs())),
);
}
}
}
let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?;
debugging!("Singed JWT: {}", token_string);
if json_output {
json.insert("token", token_string.clone());
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None)
}
}
fn sign_jwt(
private_key: &str,
mut header: Header,
payload: &Option<&str>,
claims: &Map<String, Value>,
) -> XResult<String> {
let p256_private_key_d = ecdsautil::parse_p256_private_key(private_key).ok();
let p384_private_key_d = ecdsautil::parse_p384_private_key(private_key).ok();
let (jwt_algorithm, private_key_d) = match (p256_private_key_d, p384_private_key_d) {
(Some(p256_private_key_d), None) => (AlgorithmType::Es256, p256_private_key_d),
(None, Some(p384_private_key_d)) => (AlgorithmType::Es384, p384_private_key_d),
_ => return simple_error!("Invalid private key: {}", private_key),
};
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = match (payload, claims.is_empty()) {
(Some(payload), true) => {
Cow::Owned(util::base64_encode_url_safe_no_pad(payload.as_bytes()))
}
(_, _) => opt_result!(claims.to_base64(), "Claims to base64 failed: {}"),
};
let mut tobe_signed = vec![];
tobe_signed.extend_from_slice(header.as_bytes());
tobe_signed.extend_from_slice(SEPARATOR.as_bytes());
tobe_signed.extend_from_slice(claims.as_bytes());
let raw_in = match jwt_algorithm {
AlgorithmType::Rs256 => {
rsautil::pkcs15_sha256_rsa_2048_padding_for_sign(&digest::sha256_bytes(&tobe_signed))
}
AlgorithmType::Es256 => digest::sha256_bytes(&tobe_signed),
AlgorithmType::Es384 => digest::sha384_bytes(&tobe_signed),
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signed_data = match jwt_algorithm {
AlgorithmType::Es256 => ecdsautil::sign_p256_rs(&private_key_d, &raw_in)?,
AlgorithmType::Es384 => ecdsautil::sign_p384_rs(&private_key_d, &raw_in)?,
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}

209
src/cmd_ssh_agent.rs Normal file
View File

@@ -0,0 +1,209 @@
use std::fs::remove_file;
use std::path::PathBuf;
use crate::cmdutil;
use crate::ecdsautil::{parse_ec_public_key_to_point, parse_ecdsa_r_and_s};
use crate::util::base64_encode;
use clap::{App, Arg, ArgMatches, SubCommand};
use rsa::RsaPublicKey;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use spki::DecodePublicKey;
use ssh_agent_lib::agent::{listen, Session};
use ssh_agent_lib::error::AgentError;
use ssh_agent_lib::proto::{Extension, Identity, SignRequest};
use ssh_agent_lib::ssh_encoding::Encode;
use ssh_agent_lib::ssh_key::public::KeyData;
use ssh_agent_lib::ssh_key::{Algorithm, Signature};
use ssh_key::public::EcdsaPublicKey;
use ssh_key::{EcdsaCurve, Mpint};
use std::convert::TryFrom;
use tokio::net::UnixListener as Listener;
#[derive(Default, Clone)]
struct MySshAgent {
parameter: String,
comment: String,
}
impl MySshAgent {
fn new(parameter: &str) -> XResult<Self> {
Ok(MySshAgent {
parameter: parameter.to_string(),
comment: parameter.to_string(),
})
}
}
#[ssh_agent_lib::async_trait]
impl Session for MySshAgent {
async fn request_identities(&mut self) -> Result<Vec<Identity>, AgentError> {
information!("request_identities");
let identity = match get_identity(&self.parameter, &self.comment) {
Ok(identity) => identity,
Err(e) => {
failure!("Get identity failed: {}", e);
return Err(AgentError::Failure);
}
};
let mut writer = vec![];
identity.pubkey.encode(&mut writer).unwrap();
println!("{}", base64_encode(&writer));
Ok(vec![identity])
}
async fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
information!("sign, request: {:?}", request);
let algorithm = &request.pubkey.algorithm();
match algorithm {
Algorithm::Ecdsa { curve: _ } => {}
Algorithm::Rsa { hash: _ } => {}
Algorithm::Ed25519 => {
debugging!("Algorithm::Ed25519 not supported");
return Err(AgentError::Failure);
}
Algorithm::Dsa => {
debugging!("Algorithm::Dsa not supported");
return Err(AgentError::Failure);
}
Algorithm::SkEcdsaSha2NistP256 => {
debugging!("Algorithm::SkEcdsaSha2NistP256 not supported");
return Err(AgentError::Failure);
}
Algorithm::SkEd25519 => {
debugging!("Algorithm::SkEd25519 not supported");
return Err(AgentError::Failure);
}
Algorithm::Other(algorithm_name) => {
debugging!(
"Algorithm::Other not supported, name: {}",
algorithm_name.as_str()
);
return Err(AgentError::Failure);
}
&_ => {
debugging!("Algorithm::Unknown not supported");
return Err(AgentError::Failure);
}
}
let signature = external_command_rs::external_sign(
"card-cli",
self.parameter.as_str(),
"ES384",
&request.data,
)
.unwrap();
information!("{}", hex::encode(&signature));
let (r, s) = parse_ecdsa_r_and_s(signature.as_slice()).unwrap();
let mut ssh_signature = vec![];
let r_mpint = Mpint::from_bytes(&r).unwrap();
let s_mpint = Mpint::from_bytes(&s).unwrap();
r_mpint.encode(&mut ssh_signature).unwrap();
s_mpint.encode(&mut ssh_signature).unwrap();
Ok(Signature::new(
Algorithm::Ecdsa {
curve: EcdsaCurve::NistP384,
},
ssh_signature,
)
.map_err(AgentError::other)?)
}
async fn extension(&mut self, extension: Extension) -> Result<Option<Extension>, AgentError> {
information!("extension: {:?}", extension);
Ok(None)
}
}
fn get_identity(uri: &str, comment: &str) -> XResult<Identity> {
let public_key_bytes = external_command_rs::external_public_key("card-cli", uri)?;
if let Ok(ec_point) = parse_ec_public_key_to_point(&public_key_bytes) {
let identity = Identity {
pubkey: KeyData::Ecdsa(EcdsaPublicKey::from_sec1_bytes(&ec_point).unwrap()),
comment: comment.to_string(),
};
return Ok(identity);
}
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_der(&public_key_bytes) {
let identity = Identity {
pubkey: KeyData::Rsa(ssh_key::public::RsaPublicKey::try_from(&rsa_public_key).unwrap()),
comment: comment.to_string(),
};
return Ok(identity);
}
simple_error!("Unknown uri algorithm: {}", uri)
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"ssh-agent"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("SSH-Agent subcommand")
.arg(
Arg::with_name("sock-file")
.long("sock-file")
.default_value("connect.ssh")
.help("Sock file, usage SSH_AUTH_SOCK=sock-file ssh ..."),
)
.arg(cmdutil::build_parameter_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
warning!("Not works yet.");
debugging!("Sub args: {:?}", sub_arg_matches);
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let sock_file = sub_arg_matches.value_of("sock-file").unwrap();
information!("Sock file: {}", sock_file);
let sock_file_path = PathBuf::from(".");
match std::fs::canonicalize(sock_file_path) {
Ok(canonicalized_sock_file_path) => information!(
"SSH_AUTH_SOCK={}/{}",
canonicalized_sock_file_path.to_str().unwrap_or("-"),
sock_file
),
Err(e) => warning!("Get canonicalized sock file path failed: {}", e),
}
// let ssh_agent = SshAgent::new()?;
// // TODO information!("{}", &ssh_agent.ssh_string);
let _ = remove_file(sock_file);
information!("Start unix socket: {}", sock_file);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
listen(
Listener::bind(sock_file).unwrap(),
MySshAgent::new(parameter).unwrap(),
)
.await
.unwrap();
});
// opt_result!(
// ssh_agent.run_unix(sock_file),
// "Run unix socket: {}, failed: {}",
// sock_file
// );
Ok(None)
}
}

View File

@@ -14,7 +14,7 @@ use ssh_agent::proto::{from_bytes, RsaPublicKey, signature, Signature, to_bytes}
use ssh_agent::proto::message::{self, Message};
use ssh_agent::proto::public_key::PublicKey;
use crate::digest::{copy_sha256, copy_sha512};
use crate::digestutil::{copy_sha256, copy_sha512};
use crate::pinutil;
use crate::sshutil::{generate_ssh_string, with_sign};
@@ -139,10 +139,10 @@ impl Agent for SshAgent {
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-agent" }
fn name(&self) -> &str { "ssh-agent-gpg" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH-Agent subcommand")
SubCommand::with_name(self.name()).about("SSH-Agent OpenPGP card subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").default_value("123456").help("OpenPGP card user pin"))
.arg(Arg::with_name("pgp").long("pgp").help("Use PGP"))
.arg(Arg::with_name("pgp-sign").long("pgp-sign").help("Use PGP sign"))

View File

@@ -1,7 +1,7 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use sshcerts::ssh::PublicKeyKind;
use sshcerts::{Certificate, PrivateKey, PublicKey};
use sshcerts_hatter_fork::ssh::PublicKeyKind;
use sshcerts_hatter_fork::{Certificate, PrivateKey, PublicKey};
use std::fs;
pub struct CommandImpl;

View File

@@ -3,19 +3,19 @@ use ecdsa::elliptic_curve::pkcs8::der::Encode;
use rand::random;
use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_time, XResult};
use sshcerts::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner};
use sshcerts::utils::format_signature_for_ssh;
use sshcerts::x509::extract_ssh_pubkey_from_x509_certificate;
use sshcerts::{CertType, Certificate, PublicKey};
use sshcerts_hatter_fork::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner};
use sshcerts_hatter_fork::utils::format_signature_for_ssh;
use sshcerts_hatter_fork::x509::extract_ssh_pubkey_from_x509_certificate;
use sshcerts_hatter_fork::{CertType, Certificate, PublicKey};
use std::fs;
use std::sync::Mutex;
use std::time::SystemTime;
use yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Key, YubiKey};
use crate::digest::{sha256_bytes, sha384_bytes};
use crate::digestutil::{sha256_bytes, sha384_bytes};
use crate::pivutil::slot_equals;
use crate::{pinutil, pivutil, util};
use crate::{cmdutil, pinutil, pivutil, util};
pub struct CommandImpl;
@@ -27,9 +27,9 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH PIV sign cert subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(Arg::with_name("no-pin").long("no-pin").help("No 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(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("key-id").short("k").long("key-id").takes_value(true).default_value("default_key_id").help("SSH user CA key id"))
.arg(Arg::with_name("principal").short("P").long("principal").takes_value(true).default_value("root").multiple(true).help("SSH user CA principal"))
.arg(Arg::with_name("pub").short("f").long("pub").alias("pub-file").required(true).takes_value(true).help("SSH public key file"))

View File

@@ -6,7 +6,7 @@ use rust_util::util_msg;
use yubikey::{Key, YubiKey};
use yubikey::piv::{AlgorithmId, sign_data};
use crate::{pinutil, pivutil, util};
use crate::{cmdutil, pinutil, pivutil, util};
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::sshutil::SshVecWriter;
@@ -18,9 +18,9 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH piv sign subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
.arg(Arg::with_name("no-pin").long("no-pin").help("No 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(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("namespace").short("n").long("namespace").takes_value(true).help("Namespace"))
.arg(Arg::with_name("in").long("in").required(true).takes_value(true).help("In file, - for stdin"))
}
@@ -91,14 +91,14 @@ impl Command for CommandImpl {
sign_message.write_string(namespace.as_bytes());
sign_message.write_string("".as_bytes());
sign_message.write_string("sha512".as_bytes());
let data_digest = crate::digest::sha512_bytes(&data);
let data_digest = crate::digestutil::sha512_bytes(&data);
debugging!("Data digest: {} (sha512)", hex::encode(&data_digest));
sign_message.write_string(&data_digest);
debugging!("Singed message: {}", hex::encode(&sign_message));
let tobe_signed_data = if ec_bit_len == 256 {
crate::digest::sha256_bytes(&sign_message)
crate::digestutil::sha256_bytes(&sign_message)
} else {
crate::digest::sha384_bytes(&sign_message)
crate::digestutil::sha384_bytes(&sign_message)
};
debugging!("Digest of signed message: {}", hex::encode(&tobe_signed_data));

129
src/cmd_ssh_pub_key.rs Normal file
View File

@@ -0,0 +1,129 @@
use crate::digestutil::sha256_bytes;
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::sshutil::SshVecWriter;
use crate::{cmdutil, pivutil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use yubikey::piv::AlgorithmId;
use yubikey::{Key, YubiKey};
use crate::util::base64_encode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"ssh-pub-key"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("SSH public key subcommand")
.arg(cmdutil::build_slot_arg())
.arg(Arg::with_name("ca").long("ca").help("SSH cert-authority"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let slot = opt_value_result!(
sub_arg_matches.value_of("slot"),
"--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"
);
let ca = sub_arg_matches.is_present("ca");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = pivutil::get_slot_id(slot)?;
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::<u8>::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 ssh_algorithm = match algorithm_id {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => panic!("Not supported."),
AlgorithmId::EccP256 => "nistp256",
AlgorithmId::EccP384 => "nistp384",
};
information!("SSH algorithm: {}", ssh_algorithm);
information!("ECDSA public key: {}", hex::encode(&ec_key_point));
// ECDSA SSH public key format:
// string ecdsa-sha2-[identifier]
// byte[n] ecc_key_blob
//
// ecc_key_blob:
// string [identifier]
// string Q
//
// [identifier] will be nistp256 or nistp384
let mut ssh_pub_key = vec![];
ssh_pub_key.write_string(format!("ecdsa-sha2-{}", ssh_algorithm).as_bytes());
let mut ecc_key_blob = vec![];
ecc_key_blob.write_string(ssh_algorithm.as_bytes());
ecc_key_blob.write_string(&ec_key_point);
ssh_pub_key.write_bytes(&ecc_key_blob);
let ssh_pub_key_sha256 = sha256_bytes(&ssh_pub_key);
information!(
"SSH key SHA256: {} (base64)",
base64_encode(&ssh_pub_key_sha256)
);
information!("SSH key SHA256: {} (hex)", hex::encode(&ssh_pub_key_sha256));
eprintln!();
let ssh_pub_key = format!(
"{}ecdsa-sha2-{} {} YubiKey-PIV-{}",
if ca {
"cert-authority,principals=\"root\" "
} else {
""
},
ssh_algorithm,
base64_encode(&ssh_pub_key),
slot_id
);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("ssh_pub_key", ssh_pub_key);
util::print_pretty_json(&json);
} else {
println!("{}", &ssh_pub_key);
}
Ok(None)
}
}

View File

@@ -1,99 +0,0 @@
use crate::digest::sha256_bytes;
use crate::pivutil;
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::sshutil::SshVecWriter;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::piv::AlgorithmId;
use yubikey::{Key, YubiKey};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-pub-key" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH public key subcommand")
.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("ca").long("ca").help("SSH cert-authority"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let ca = sub_arg_matches.is_present("ca");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = pivutil::get_slot_id(slot)?;
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::<u8>::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 ssh_algorithm = match algorithm_id {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => panic!("Not supported."),
AlgorithmId::EccP256 => "nistp256",
AlgorithmId::EccP384 => "nistp384",
};
information!("SSH algorithm: {}", ssh_algorithm);
information!("ECDSA public key: {}", hex::encode(&ec_key_point));
// ECDSA SSH public key format:
// string ecdsa-sha2-[identifier]
// byte[n] ecc_key_blob
//
// ecc_key_blob:
// string [identifier]
// string Q
//
// [identifier] will be nistp256 or nistp384
let mut ssh_pub_key = vec![];
ssh_pub_key.write_string(format!("ecdsa-sha2-{}", ssh_algorithm).as_bytes());
let mut ecc_key_blob = vec![];
ecc_key_blob.write_string(ssh_algorithm.as_bytes());
ecc_key_blob.write_string(&ec_key_point);
ssh_pub_key.write_bytes(&ecc_key_blob);
let ssh_pub_key_sha256 = sha256_bytes(&ssh_pub_key);
information!("SSH key SHA256: {} (base64)", STANDARD.encode(&ssh_pub_key_sha256));
information!("SSH key SHA256: {} (hex)", hex::encode(&ssh_pub_key_sha256));
eprintln!();
println!(
"{}ecdsa-sha2-{} {} Yubikey-PIV-{}",
if ca { "cert-authority,principals=\"root\" " } else { "" },
ssh_algorithm,
STANDARD.encode(&ssh_pub_key),
slot_id
);
Ok(None)
}
}

View File

@@ -9,13 +9,12 @@ use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use x509_parser::certificate::X509Certificate;
use x509_parser::prelude::FromDer;
use crate::digest;
use crate::fido;
use crate::fido::{U2fRegistrationData, U2fV2Challenge};
use crate::{cmdutil, digestutil, util};
use crate::fidoutil;
use crate::fidoutil::{U2fRegistrationData, U2fV2Challenge};
use crate::util::base64_encode;
pub struct CommandImpl;
@@ -29,12 +28,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("timeout").short("t").long("timeout").default_value("30").help("Timeout in seconds"))
.arg(Arg::with_name("challenge").long("challenge").takes_value(true).help("Challenge HEX"))
.arg(Arg::with_name("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64,
@@ -48,12 +46,12 @@ impl Command for CommandImpl {
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
let u2fv2_challenge_str = u2fv2_challenge.to_json();
let app_id_hash = digest::sha256(app_id);
let challenge_hash = digest::sha256(&u2fv2_challenge_str);
let app_id_hash = digestutil::sha256(app_id);
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
let flags = RegisterFlags::empty();
let status_tx = fido::start_status_updater();
let status_tx = fidoutil::start_status_updater();
let (register_tx, register_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
@@ -98,8 +96,8 @@ impl Command for CommandImpl {
+ u2f_registration_data.attestation_cert.as_ref().map(|c| c.len()).unwrap_or(0);
let sign = &register_result.0[sign_prefix_len..];
let mut json = BTreeMap::new();
if json_output {
let mut json = BTreeMap::new();
// println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
if let Some(device_name) = u2f_registration_data.device_name {
json.insert("device_name", device_name);
@@ -117,6 +115,8 @@ impl Command for CommandImpl {
json.insert("app_id_hash", hex::encode(&app_id_hash));
json.insert("challenge", u2fv2_challenge_str);
json.insert("challenge_hash", hex::encode(&challenge_hash));
util::print_pretty_json(&json);
} else {
success!("Device info: {}", u2f_registration_data.device_info);
information!("Register challenge: {}", u2fv2_challenge_str);
@@ -149,9 +149,6 @@ impl Command for CommandImpl {
warning!("Cannot find attestation cert!");
}
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None)
}
}

View File

@@ -13,9 +13,9 @@ use openssl::pkey::PKey;
use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError};
use crate::digest;
use crate::fido;
use crate::fido::U2fV2Challenge;
use crate::{cmdutil, digestutil, util};
use crate::fidoutil;
use crate::fidoutil::U2fV2Challenge;
use crate::util::base64_encode;
pub struct CommandImpl;
@@ -31,12 +31,11 @@ impl Command for CommandImpl {
.arg(Arg::with_name("challenge").long("challenge").takes_value(true).help("Challenge HEX"))
.arg(Arg::with_name("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
.arg(Arg::with_name("key-handle").short("k").long("key-handle").takes_value(true).multiple(true).help("Key handle"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let json_output = cmdutil::check_json_output(sub_arg_matches);
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64,
@@ -72,10 +71,10 @@ impl Command for CommandImpl {
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
let u2fv2_challenge_str = u2fv2_challenge.to_json();
let app_id_hash = digest::sha256(app_id);
let challenge_hash = digest::sha256(&u2fv2_challenge_str);
let app_id_hash = digestutil::sha256(app_id);
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
let status_tx = fido::start_status_updater();
let status_tx = fidoutil::start_status_updater();
information!("App id: {}, Start sign...", app_id);
debugging!("Wait timeout: {} ms", timeout_ms);
@@ -163,7 +162,7 @@ impl Command for CommandImpl {
}
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
util::print_pretty_json(&json);
}
Ok(None)
}

44
src/cmd_yubikey.rs Normal file
View File

@@ -0,0 +1,44 @@
use crate::util;
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use serde_json::Value;
use std::collections::BTreeMap;
use yubikey::Context;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"yubikey"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Yubikey subcommand")
}
fn run(&self, _arg_matches: &ArgMatches, _sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let mut list = vec![];
let mut readers = Context::open()?;
for reader in readers.iter()? {
let yubikey = match reader.open() {
Ok(yk) => yk,
Err(e) => {
warning!("Error opening YubiKey: {}", e);
continue;
}
};
let mut key = BTreeMap::new();
key.insert("serial", Value::Number(yubikey.serial().0.into()));
key.insert("version", yubikey.version().to_string().into());
key.insert("name", yubikey.name().into());
list.push(key);
}
util::print_pretty_json(&list);
Ok(None)
}
}

82
src/cmdutil.rs Normal file
View File

@@ -0,0 +1,82 @@
use clap::{Arg, ArgMatches};
use rust_util::util_msg;
pub fn build_slot_arg() -> Arg<'static, 'static> {
Arg::with_name("slot")
.short("s")
.long("slot")
.takes_value(true)
.help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e")
}
pub fn build_with_hmac_encrypt_arg() -> Arg<'static, 'static> {
Arg::with_name("with-hmac-encrypt").long("with-hmac-encrypt").help("With HMAC encrypt")
}
pub fn build_with_pbe_encrypt_arg() -> Arg<'static, 'static> {
Arg::with_name("with-pbe-encrypt").long("with-pbe-encrypt").help("With PBE encryption")
}
pub fn build_double_pin_check_arg() -> Arg<'static, 'static> {
Arg::with_name("double-pin-check").long("double-pin-check").help("Double PIN check")
}
pub fn build_pbe_iteration_arg() -> Arg<'static, 'static> {
Arg::with_name("pbe-iteration").long("pbe-iteration").takes_value(true).help("PBE iteration, default 100000")
}
pub fn build_serial_arg() -> Arg<'static, 'static> {
Arg::with_name("serial").long("serial").takes_value(true).help("Serial number")
}
pub fn build_key_uri_arg() -> Arg<'static, 'static> {
Arg::with_name("key").long("key").required(true).takes_value(true).help("Key uri")
}
pub fn build_pin_arg() -> Arg<'static, 'static> {
Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN")
}
pub fn build_alg_arg() -> Arg<'static, 'static> {
Arg::with_name("alg").long("alg").takes_value(true).required(true).help("Algorithm, e.g. RS256, ES256, ES384")
}
pub fn build_parameter_arg() -> Arg<'static, 'static> {
Arg::with_name("parameter").long("parameter").takes_value(true).required(true).help("Parameter")
}
pub fn build_epk_arg() -> Arg<'static, 'static> {
Arg::with_name("epk").long("epk").required(true).takes_value(true).help("E-Public key")
}
pub fn build_message_arg() -> Arg<'static, 'static> {
Arg::with_name("message-base64").long("message-base64").takes_value(true).required(true).help("Message in base64")
}
pub fn build_message_type_arg() -> Arg<'static, 'static> {
Arg::with_name("message-type").long("message-type").takes_value(true).help("Optional, message type, raw, sha256, sha384 or sha512")
}
pub fn build_no_pin_arg() -> Arg<'static, 'static> {
Arg::with_name("no-pin").long("no-pin").help("No PIN")
}
pub fn build_keychain_name_arg() -> Arg<'static, 'static> {
Arg::with_name("keychain-name")
.long("keychain-name")
.takes_value(true)
.help("Key chain name")
}
pub fn build_json_arg() -> Arg<'static, 'static> {
Arg::with_name("json").long("json").help("JSON output")
}
pub fn check_json_output(sub_arg_matches: &ArgMatches) -> bool {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
json_output
}

View File

@@ -1,6 +1,25 @@
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512};
pub enum DigestAlgorithm {
Sha256,
#[allow(dead_code)]
Sha384,
}
impl DigestAlgorithm {
pub fn digest(&self, data: &[u8]) -> Vec<u8> {
match self {
DigestAlgorithm::Sha256 => sha256_bytes(data),
DigestAlgorithm::Sha384 => sha384_bytes(data),
}
}
pub fn digest_str(&self, s: &str) -> Vec<u8> {
self.digest(s.as_bytes())
}
}
pub fn sha256(input: &str) -> Vec<u8> {
sha256_bytes(input.as_bytes())
}

View File

@@ -2,8 +2,7 @@
macro_rules! piv_ecdh {
($p_algo: tt, $public_key_pem_opt: expr, $sub_arg_matches: expr, $json: expr, $json_output: expr) => ({
use $p_algo::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use $p_algo::ecdh::EphemeralSecret;
use $p_algo::{EncodedPoint, PublicKey};
use $p_algo::{EncodedPoint, PublicKey, ecdh::EphemeralSecret};
let public_key;
if let Some(public_key_pem) = $public_key_pem_opt {
public_key = opt_result!(public_key_pem.parse::<PublicKey>(), "Parse public key failed: {}");
@@ -35,4 +34,30 @@ macro_rules! piv_ecdh {
})
}
macro_rules! parse_private_and_ecdh {
($algo: tt, $private_key_bytes: tt, $ephemeral_public_key_bytes: tt) => ({
use $algo::{SecretKey, PublicKey, ecdh::diffie_hellman, pkcs8::DecodePrivateKey};
use spki::DecodePublicKey;
let secret_key= SecretKey::from_pkcs8_der($private_key_bytes)?;
let public_key = opt_result!(PublicKey::from_public_key_der(
$ephemeral_public_key_bytes),"Parse ephemeral public key failed: {}");
let shared_secret = diffie_hellman(secret_key.to_nonzero_scalar(), public_key.as_affine());
Ok(shared_secret.raw_secret_bytes().to_vec())
})
}
pub fn parse_p256_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
parse_private_and_ecdh!(p256, private_key_bytes, ephemeral_public_key_bytes)
}
pub fn parse_p384_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
parse_private_and_ecdh!(p384, private_key_bytes, ephemeral_public_key_bytes)
}
pub fn parse_p521_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
parse_private_and_ecdh!(p521, private_key_bytes, ephemeral_public_key_bytes)
}
use rust_util::XResult;
pub(crate) use piv_ecdh;

View File

@@ -5,6 +5,7 @@ use p256::NistP256;
use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p384::NistP384;
use p256::pkcs8::EncodePrivateKey;
use p521::NistP521;
use rust_util::XResult;
use spki::EncodePublicKey;
use crate::util::{base64_encode, try_decode};
@@ -13,6 +14,12 @@ use crate::util::{base64_encode, try_decode};
pub enum EcdsaAlgorithm {
P256,
P384,
P521,
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum EcdsaSignType {
Der, Rs,
}
pub fn parse_ecdsa_to_rs(signature_der: &[u8]) -> XResult<Vec<u8>> {
@@ -30,22 +37,14 @@ pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)>
match &seq[0].content {
BerObjectContent::Integer(r) => {
debugging!("Signature r: {}", hex::encode(r));
if r.len() == ((256 / 8) + 1) || r.len() == ((384 / 8) + 1) {
vec_r = r[1..].to_vec();
} else {
vec_r = r.to_vec();
}
vec_r = trim_ecdsa_point_coord(r);
}
_ => return simple_error!("Parse signature failed: [0]not integer"),
}
match &seq[1].content {
BerObjectContent::Integer(s) => {
debugging!("Signature s: {}", hex::encode(s));
if s.len() == ((256 / 8) + 1) || s.len() == ((384 / 8) + 1) {
vec_s = s[1..].to_vec();
} else {
vec_s = s.to_vec();
}
vec_s = trim_ecdsa_point_coord(s);
}
_ => return simple_error!("Parse signature failed: [1]not integer"),
}
@@ -55,26 +54,112 @@ pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)>
Ok((vec_r, vec_s))
}
pub fn generate_p256_keypair() -> XResult<(String, String, String)> {
let secret_key = p256::SecretKey::random(&mut rand::thread_rng());
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes());
let secret_key_pem = secret_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
let public_key_pem = secret_key.public_key().to_public_key_pem(LineEnding::LF)?;
Ok((secret_key_der_base64, secret_key_pem, public_key_pem))
const P256_LEN: usize = 32;
const P384_LEN: usize = 48;
const P521_LEN: usize = 66;
fn trim_ecdsa_point_coord(p: &[u8]) -> Vec<u8> {
if p.len() == (P256_LEN + 1) || p.len() == (P384_LEN + 1) || p.len() == (P521_LEN + 1) {
p[1..].to_vec()
} else if p.len() == (P256_LEN - 1) || p.len() == (P384_LEN - 1) || p.len() == (P521_LEN - 1) {
let mut v = vec![];
v.push(0_u8);
v.extend_from_slice(p);
v
} else {
p.to_vec()
}
}
pub fn generate_p384_keypair() -> XResult<(String, String, String)> {
let secret_key = p384::SecretKey::random(&mut rand::thread_rng());
macro_rules! generate_inner_ecdsa_keypair {
($algo: tt) => ({
use $algo::SecretKey;
let secret_key = SecretKey::random(&mut rand::thread_rng());
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes());
let secret_key_pem = secret_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
let public_key_pem = secret_key.public_key().to_public_key_pem(LineEnding::LF)?;
Ok((secret_key_der_base64, secret_key_pem, public_key_pem))
let public_key_der = secret_key.public_key().to_public_key_der()?.to_vec();
let jwk_ec_key = secret_key.public_key().to_jwk().to_string();
Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key))
})
}
pub fn generate_ecdsa_keypair(algo: EcdsaAlgorithm) -> XResult<(String, String, String, Vec<u8>, String)> {
match algo {
EcdsaAlgorithm::P256 => generate_inner_ecdsa_keypair!(p256),
EcdsaAlgorithm::P384 => generate_inner_ecdsa_keypair!(p384),
EcdsaAlgorithm::P521 => generate_inner_ecdsa_keypair!(p521),
}
}
pub fn parse_ec_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
match parse_p521_public_key_to_point(public_key_bytes) {
Ok(point) => Ok(point),
Err(_) => match parse_p384_public_key_to_point(public_key_bytes) {
Ok(point) => Ok(point),
Err(_) => parse_p256_public_key_to_point(public_key_bytes),
}
}
}
pub fn parse_p256_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use p256::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use spki::DecodePublicKey;
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
}
pub fn parse_p384_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use p384::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use spki::DecodePublicKey;
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
}
pub fn parse_p521_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use p521::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use spki::DecodePublicKey;
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
}
macro_rules! parse_ecdsa_private_key_to_public_key {
($algo: tt, $parse_ecdsa_private_key: tt) => ({
use $algo::{SecretKey, pkcs8::DecodePrivateKey};
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
Ok(secret_key) => secret_key,
Err(_) => match try_decode($parse_ecdsa_private_key) {
Ok(private_key_der) => match SecretKey::from_pkcs8_der(&private_key_der) {
Ok(secret_key) => secret_key,
Err(e) => return simple_error!("Invalid PKCS#8 private key {}, error: {}", $parse_ecdsa_private_key, e),
}
Err(_) => return simple_error!("Invalid PKCS#8 private key: {}", $parse_ecdsa_private_key),
}
};
let public_key_document = opt_result!(secret_key.public_key().to_public_key_der(), "Conver to public key failed: {}");
Ok(public_key_document.to_vec())
})
}
pub fn parse_p256_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key_to_public_key!(p256, private_key_pkcs8)
}
pub fn parse_p384_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key_to_public_key!(p384, private_key_pkcs8)
}
pub fn parse_p521_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key_to_public_key!(p521, private_key_pkcs8)
}
macro_rules! parse_ecdsa_private_key {
($algo: tt, $parse_ecdsa_private_key: tt) => ({
use $algo::pkcs8::DecodePrivateKey;
use $algo::SecretKey;
use $algo::{SecretKey, pkcs8::DecodePrivateKey};
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
Ok(secret_key) => secret_key,
@@ -98,26 +183,36 @@ pub fn parse_p384_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key!(p384, private_key_pkcs8)
}
pub fn sign_p256_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult<Vec<u8>> {
use p256::ecdsa::{SigningKey, Signature};
use p256::ecdsa::signature::hazmat::PrehashSigner;
let signing_key = SigningKey::from_slice(private_key_d)?;
let signature: Signature = signing_key.sign_prehash(pre_hash)?;
Ok(signature.to_bytes().to_vec())
pub fn parse_p521_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key!(p521, private_key_pkcs8)
}
pub fn sign_p384_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult<Vec<u8>> {
use p384::ecdsa::{SigningKey, Signature};
use p384::ecdsa::signature::hazmat::PrehashSigner;
let signing_key = SigningKey::from_slice(private_key_d)?;
let signature: Signature = signing_key.sign_prehash(pre_hash)?;
macro_rules! sign_ecdsa_rs_or_der {
($algo: tt, $private_key_d: tt, $pre_hash: tt, $is_rs: tt) => ({
use $algo::ecdsa::{SigningKey, Signature, signature::hazmat::PrehashSigner};
let signing_key = SigningKey::from_slice($private_key_d)?;
let signature: Signature = signing_key.sign_prehash($pre_hash)?;
if $is_rs {
Ok(signature.to_bytes().to_vec())
} else {
Ok(signature.to_der().as_bytes().to_vec())
}
})
}
pub fn ecdsa_sign(algo: EcdsaAlgorithm, private_key_d: &[u8], pre_hash: &[u8], sign_type: EcdsaSignType) -> XResult<Vec<u8>> {
let is_rs = sign_type == EcdsaSignType::Rs;
match algo {
EcdsaAlgorithm::P256 => sign_ecdsa_rs_or_der!(p256, private_key_d, pre_hash, is_rs),
EcdsaAlgorithm::P384 => sign_ecdsa_rs_or_der!(p384, private_key_d, pre_hash, is_rs),
EcdsaAlgorithm::P521 => sign_ecdsa_rs_or_der!(p521, private_key_d, pre_hash, is_rs),
}
}
macro_rules! ecdsa_verify_signature {
($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({
use ecdsa::Signature;
@@ -133,10 +228,11 @@ macro_rules! ecdsa_verify_signature {
})
}
pub fn ecdsaverify(algo: EcdsaAlgorithm, pk_point: &[u8], prehash: &[u8], signature: &[u8]) -> XResult<()> {
pub fn ecdsa_verify(algo: EcdsaAlgorithm, pk_point: &[u8], prehash: &[u8], signature: &[u8]) -> XResult<()> {
match algo {
EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature),
EcdsaAlgorithm::P384 => ecdsa_verify_signature!(NistP384, pk_point, prehash, signature),
EcdsaAlgorithm::P521 => ecdsa_verify_signature!(NistP521, pk_point, prehash, signature),
}
Ok(())
}

66
src/ecutil.rs Normal file
View File

@@ -0,0 +1,66 @@
use crate::util::base64_decode;
use p256::pkcs8::LineEnding;
use rust_util::XResult;
use spki::{DecodePublicKey, EncodePublicKey};
pub fn convert_ec_public_key_to_jwk(public_key: &str) -> XResult<String> {
if let Ok(jwk) = convert_ec_public_key_p256_to_jwk(public_key) {
return Ok(jwk);
}
if let Ok(jwk) = convert_ec_public_key_p384_to_jwk(public_key) {
return Ok(jwk);
}
simple_error!("Parse public key failed, MUST be pem or base64 encoded DER.")
}
pub fn convert_ec_public_key_p256_to_jwk(public_key: &str) -> XResult<String> {
let public_key_p256 = if public_key.contains("BEGIN PUBLIC KEY") {
debugging!("Try parse P256 public key PEM.");
p256::PublicKey::from_public_key_pem(public_key)?
} else {
debugging!("Try parse P256 public key DER.");
let der = base64_decode(public_key)?;
p256::PublicKey::from_public_key_der(&der)?
};
Ok(public_key_p256.to_jwk_string())
}
pub fn convert_ec_public_key_p384_to_jwk(public_key: &str) -> XResult<String> {
let public_key_p384 = if public_key.contains("BEGIN PUBLIC KEY") {
debugging!("Try parse P384 public key PEM.");
p384::PublicKey::from_public_key_pem(public_key)?
} else {
debugging!("Try parse P384 public key DER.");
let der = base64_decode(public_key)?;
p384::PublicKey::from_public_key_der(&der)?
};
Ok(public_key_p384.to_jwk_string())
}
pub fn convert_ec_jwk_to_public_key(jwk: &str) -> XResult<(String, Vec<u8>)> {
if let Ok(public_key) = convert_ec_jwk_p256_to_public_key(jwk) {
return Ok(public_key);
}
if let Ok(public_key) = convert_ec_jwk_p384_to_public_key(jwk) {
return Ok(public_key);
}
simple_error!("Parse JWK failed, MUST be P256 or P384.")
}
pub fn convert_ec_jwk_p256_to_public_key(jwk: &str) -> XResult<(String, Vec<u8>)> {
debugging!("Try parse P256 JWK.");
let public_key = p256::PublicKey::from_jwk_str(jwk)?;
Ok((
public_key.to_public_key_pem(LineEnding::LF)?,
public_key.to_public_key_der()?.to_vec(),
))
}
pub fn convert_ec_jwk_p384_to_public_key(jwk: &str) -> XResult<(String, Vec<u8>)> {
debugging!("Try parse P384 JWK.");
let public_key = p384::PublicKey::from_jwk_str(jwk)?;
Ok((
public_key.to_public_key_pem(LineEnding::LF)?,
public_key.to_public_key_der()?.to_vec(),
))
}

View File

@@ -4,14 +4,12 @@ use std::thread;
use std::time::SystemTime;
use authenticator::{RegisterResult, StatusUpdate};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rand::Rng;
use rust_util::XResult;
use serde::{Deserialize, Serialize};
use crate::pkiutil::bytes_to_pem;
use crate::util::base64_encode;
use crate::util::{base64_encode, base64_encode_url_safe_no_pad};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct U2FDeviceInfo {
@@ -103,7 +101,7 @@ impl U2fV2Challenge {
None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix),
Some(challenge_hex) => {
let challenge_bytes = opt_result!(hex::decode(challenge_hex), "Decode challenge hex failed: {}");
let challenge = URL_SAFE_NO_PAD.encode(challenge_bytes);
let challenge = base64_encode_url_safe_no_pad(challenge_bytes);
U2fV2Challenge::new(challenge, app_id)
}
})
@@ -122,7 +120,7 @@ impl U2fV2Challenge {
rand_bytes[..8].clone_from_slice(&timestamp_be_bytes[..8]);
}
let challenge = URL_SAFE_NO_PAD.encode(rand_bytes);
let challenge = base64_encode_url_safe_no_pad(rand_bytes);
Self::new(challenge, app_id)
}

View File

@@ -8,8 +8,9 @@ use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::hmacmode::HmacKey;
use yubico_manager::sec::hmac_sha1;
use yubico_manager::Yubico;
use crate::digest::{copy_sha256, sha256_bytes};
use crate::util::{base64_decode, base64_encode};
use crate::digestutil::{copy_sha256, sha256_bytes};
use crate::util;
use crate::util::{base64_decode, base64_encode, base64_encode_url_safe_no_pad, base64_uri_decode};
const HMAC_ENC_PREFIX: &str = "hmac_enc:";
@@ -19,7 +20,7 @@ pub fn hmac_encrypt_from_string(plaintext: &str) -> XResult<String> {
}
pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
let hmac_nonce: [u8; 8] = random();
let hmac_nonce: [u8; 16] = random();
let aes_gcm_nonce: [u8; 16] = random();
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
@@ -33,8 +34,8 @@ pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
Ok(format!("{}{}:{}:{}",
HMAC_ENC_PREFIX,
hex::encode(hmac_nonce),
hex::encode(aes_gcm_nonce),
base64_encode_url_safe_no_pad(hmac_nonce),
base64_encode_url_safe_no_pad(aes_gcm_nonce),
base64_encode(&ciphertext)
))
}
@@ -43,14 +44,6 @@ pub fn is_hmac_encrypted(ciphertext: &str) -> bool {
ciphertext.starts_with(HMAC_ENC_PREFIX)
}
pub fn try_hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
if is_hmac_encrypted(ciphertext) {
hmac_decrypt_to_string(ciphertext)
} else {
Ok(ciphertext.to_string())
}
}
pub fn hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
let plaintext = hmac_decrypt(ciphertext)?;
Ok(String::from_utf8(plaintext)?)
@@ -61,8 +54,8 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
return simple_error!("Invalid ciphertext: {}", ciphertext);
}
let parts = ciphertext.split(":").collect::<Vec<_>>();
let hmac_nonce = hex::decode(parts[1])?;
let aes_gcm_nonce = hex::decode(parts[2])?;
let hmac_nonce = try_decode_hmac_val(parts[1])?;
let aes_gcm_nonce = try_decode_hmac_val(parts[2])?;
let ciphertext = base64_decode(parts[3])?;
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
@@ -76,6 +69,16 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
Ok(plaintext)
}
pub fn try_decode_hmac_val(s: &str) -> XResult<Vec<u8>> {
match hex::decode(s) {
Ok(v) => Ok(v),
Err(e) => match base64_uri_decode(s) {
Ok(v) => Ok(v),
Err(_) => simple_error!("Try decode failed: {}", e)
}
}
}
pub fn compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
let mut yubi = Yubico::new();
let device = match yubi.find_yubikey() {
@@ -128,10 +131,10 @@ pub fn output_hmac_result(sub_arg_matches: &ArgMatches, json_output: bool, chall
let sha512_output = sub_arg_matches.is_present("sha512");
let hex_string = hex::encode(result);
let hex_sha1 = iff!(sha1_output, Some(crate::digest::sha1_bytes(result)), None);
let hex_sha256 = iff!(sha256_output, Some(crate::digest::sha256_bytes(result)), None);
let hex_sha384 = iff!(sha384_output, Some(crate::digest::sha384_bytes(result)), None);
let hex_sha512 = iff!(sha512_output, Some(crate::digest::sha512_bytes(result)), None);
let hex_sha1 = iff!(sha1_output, Some(crate::digestutil::sha1_bytes(result)), None);
let hex_sha256 = iff!(sha256_output, Some(crate::digestutil::sha256_bytes(result)), None);
let hex_sha384 = iff!(sha384_output, Some(crate::digestutil::sha384_bytes(result)), None);
let hex_sha512 = iff!(sha512_output, Some(crate::digestutil::sha512_bytes(result)), None);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
@@ -142,7 +145,7 @@ pub fn output_hmac_result(sub_arg_matches: &ArgMatches, json_output: bool, chall
hex_sha384.map(|hex_sha384| json.insert("response_sha384_hex", hex::encode(hex_sha384)));
hex_sha512.map(|hex_sha512| json.insert("response_sha512_hex", hex::encode(hex_sha512)));
println!("{}", serde_json::to_string_pretty(&json).expect("Convert to JSON failed!"));
util::print_pretty_json(&json);
} else {
success!("Challenge HEX: {}", hex::encode(challenge_bytes));
success!("Response HEX: {}", hex_string);

168
src/keychain.rs Normal file
View File

@@ -0,0 +1,168 @@
use rust_util::{util_file, XResult};
use security_framework::os::macos::keychain::{CreateOptions, SecKeychain};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const KEYCHAIN_KEY_PREFIX: &str = "keychain:";
const DEFAULT_SERVICE_NAME: &str = "card-cli";
pub struct KeychainKey {
pub keychain_name: String,
pub service_name: String,
pub key_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeychainKeyValue {
pub keychain_name: String,
pub pkcs8_base64: String,
pub secret_key_pem: String,
pub public_key_pem: String,
pub public_key_jwk: String,
}
pub fn is_keychain_key_uri(name: &str) -> bool {
name.starts_with(KEYCHAIN_KEY_PREFIX)
}
impl KeychainKey {
pub fn from_key_name_default(key_name: &str) -> Self {
Self::from("", DEFAULT_SERVICE_NAME, key_name)
}
pub fn from(keychain_name: &str, service_name: &str, key_name: &str) -> Self {
debugging!(
"Keychain key: {} - {} - {}",
keychain_name,
service_name,
key_name
);
Self {
keychain_name: keychain_name.to_string(),
service_name: service_name.to_string(),
key_name: key_name.to_string(),
}
}
pub fn parse_key_uri(keychain_key: &str) -> XResult<Self> {
if !keychain_key.starts_with(KEYCHAIN_KEY_PREFIX) {
return simple_error!("Not a valid keychain key: {}", keychain_key);
}
//keychain:keychain_name:service_name:key_name
let keychain_key_parts = keychain_key.split(':').collect::<Vec<_>>();
if keychain_key_parts.len() != 4 {
return simple_error!("Not a valid keychain key: {}", keychain_key);
}
Ok(Self {
keychain_name: keychain_key_parts[1].to_string(),
service_name: keychain_key_parts[2].to_string(),
key_name: keychain_key_parts[3].to_string(),
})
}
pub fn to_key_uri(&self) -> String {
let mut s = String::new();
s.push_str(KEYCHAIN_KEY_PREFIX);
s.push_str(&self.keychain_name);
s.push(':');
s.push_str(&self.service_name);
s.push(':');
s.push_str(&self.key_name);
s
}
pub fn get_password(&self) -> XResult<Option<Vec<u8>>> {
let sec_keychain = self.get_keychain()?;
debugging!(
"Try find generic password: {}.{}",
&self.service_name,
&self.key_name
);
match sec_keychain.find_generic_password(&self.service_name, &self.key_name) {
Ok((item_password, _keychain_item)) => Ok(Some(item_password.as_ref().to_vec())),
Err(e) => {
debugging!("Get password: {} failed: {}", &self.to_key_uri(), e);
Ok(None)
}
}
}
pub fn set_password(&self, password: &[u8]) -> XResult<()> {
let sec_keychain = self.get_keychain()?;
if sec_keychain
.find_generic_password(&self.service_name, &self.key_name)
.is_ok()
{
return simple_error!("Password {}.{} exists", &self.service_name, &self.key_name);
}
opt_result!(
sec_keychain.set_generic_password(&self.service_name, &self.key_name, password),
"Set password {}.{} error: {}",
&self.service_name,
&self.key_name
);
Ok(())
}
fn get_keychain(&self) -> XResult<SecKeychain> {
if !self.keychain_name.is_empty() {
let keychain_file_name = format!("{}.keychain", &self.keychain_name);
debugging!("Open or create keychain: {}", &keychain_file_name);
let keychain_exists = check_keychain_exists(&keychain_file_name);
if keychain_exists {
Ok(opt_result!(
SecKeychain::open(&keychain_file_name),
"Open keychain: {}, failed: {}",
&keychain_file_name
))
} else {
match CreateOptions::new()
.prompt_user(true)
.create(&keychain_file_name)
{
Ok(sec_keychain) => Ok(sec_keychain),
Err(ce) => match SecKeychain::open(&keychain_file_name) {
Ok(sec_keychain) => Ok(sec_keychain),
Err(oe) => simple_error!(
"Create keychain: {}, failed: {}, open also failed: {}",
&self.keychain_name,
ce,
oe
),
},
}
}
} else {
Ok(opt_result!(
SecKeychain::default(),
"Get keychain failed: {}"
))
}
}
}
fn check_keychain_exists(keychain_file_name: &str) -> bool {
let keychain_path = PathBuf::from(util_file::resolve_file_path("~/Library/Keychains/"));
match keychain_path.read_dir() {
Ok(read_dir) => {
for dir in read_dir {
match dir {
Ok(dir) => {
if let Some(file_name) = dir.file_name().to_str() {
if file_name.starts_with(keychain_file_name) {
debugging!("Found key chain file: {:?}", dir);
return true;
}
}
}
Err(e) => {
debugging!("Read path sub dir: {:?} failed: {}", keychain_path, e);
}
}
}
}
Err(e) => {
debugging!("Read path: {:?} failed: {}", keychain_path, e);
}
}
false
}

View File

@@ -1,29 +1,233 @@
use crate::pivutil::{FromStr, ToStr};
use jwt::AlgorithmType;
use percent_encoding::NON_ALPHANUMERIC;
use regex::Regex;
use rust_util::XResult;
use yubikey::piv::{AlgorithmId, SlotId};
// reference: https://git.hatter.ink/hatter/card-cli/issues/6
#[derive(Debug)]
pub enum KeyUri {
SecureEnclaveKey(SecureEnclaveKey),
SecureEnclave(SecureEnclaveKey),
YubikeyPiv(YubikeyPivKey),
YubikeyHmacEncSoft(YubikeyHmacEncSoftKey),
ExternalCommand(ExternalCommandKey),
}
// #[derive(Debug, PartialEq, Eq)]
// pub enum KeyModule {
// SecureEnclave,
// OpenPgpCard,
// PersonalIdentityVerification,
// }
//
// impl KeyModule {
// pub fn from(module: &str) -> Option<Self> {
// match module {
// "se" => Some(Self::SecureEnclave),
// "pgp" => Some(Self::OpenPgpCard),
// "piv" => Some(Self::PersonalIdentityVerification),
// _ => None,
// }
// }
// }
impl KeyUri {
pub fn as_secure_enclave_key(&self) -> XResult<&SecureEnclaveKey> {
match self {
KeyUri::SecureEnclave(key) => Ok(key),
_ => simple_error!("Not a secure enclave key."),
}
}
pub fn get_preferred_algorithm_type(&self) -> AlgorithmType {
let algorithm_id = match &self {
KeyUri::SecureEnclave(_) => return AlgorithmType::Es256,
KeyUri::YubikeyPiv(key) => key.algorithm,
KeyUri::YubikeyHmacEncSoft(key) => key.algorithm,
KeyUri::ExternalCommand(key) => key.algorithm,
};
match algorithm_id {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256,
KeyAlgorithmId::EccP256 => AlgorithmType::Es256,
KeyAlgorithmId::EccP384 => AlgorithmType::Es384,
KeyAlgorithmId::EccP521 => AlgorithmType::Es512,
// ML-KEM not supports JWS
KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => AlgorithmType::None,
}
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for KeyUri {
fn to_string(&self) -> String {
let mut key_uri = String::with_capacity(64);
key_uri.push_str("key://");
match self {
// key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)
// key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)
KeyUri::SecureEnclave(key) => {
key_uri.push_str(&key.host);
key_uri.push_str(":se/p256:");
key_uri.push_str(&key.usage.to_string());
key_uri.push(':');
key_uri.push_str(&key.private_key);
}
// key://yubikey-5n:piv/p256::9a
KeyUri::YubikeyPiv(key) => {
key_uri.push_str(&key.key_name);
key_uri.push_str(":piv/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(key.slot.to_str());
}
// key://-:soft/p256::hmac_enc:...
KeyUri::YubikeyHmacEncSoft(key) => {
key_uri.push_str(&key.key_name);
key_uri.push_str(":soft/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(key.hmac_enc_private_key.as_str());
}
// key://external-command-file-name:external_command/p256::parameter
KeyUri::ExternalCommand(key) => {
let encoded_external_command =
percent_encoding::utf8_percent_encode(&key.external_command, NON_ALPHANUMERIC)
.to_string();
key_uri.push_str(&encoded_external_command);
key_uri.push_str(":external_command/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(&key.parameter);
}
}
key_uri
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyAlgorithmId {
Rsa1024,
Rsa2048,
Rsa3072,
Rsa4096,
EccP256,
EccP384,
EccP521,
MlKem512,
MlKem768,
MlKem1024,
}
impl KeyAlgorithmId {
pub fn from_algorithm_id(algorithm_id: AlgorithmId) -> Self {
match algorithm_id {
AlgorithmId::Rsa1024 => Self::Rsa1024,
AlgorithmId::Rsa2048 => Self::Rsa2048,
AlgorithmId::EccP256 => Self::EccP256,
AlgorithmId::EccP384 => Self::EccP384,
}
}
pub fn to_algorithm_id(self) -> Option<AlgorithmId> {
match self {
KeyAlgorithmId::Rsa1024 => Some(AlgorithmId::Rsa1024),
KeyAlgorithmId::Rsa2048 => Some(AlgorithmId::Rsa2048),
KeyAlgorithmId::Rsa3072 => None,
KeyAlgorithmId::Rsa4096 => None,
KeyAlgorithmId::EccP256 => Some(AlgorithmId::EccP256),
KeyAlgorithmId::EccP384 => Some(AlgorithmId::EccP384),
KeyAlgorithmId::EccP521 => None,
KeyAlgorithmId::MlKem512 => None,
KeyAlgorithmId::MlKem768 => None,
KeyAlgorithmId::MlKem1024 => None,
}
}
pub fn is_rsa(&self) -> bool {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => true,
KeyAlgorithmId::EccP256
| KeyAlgorithmId::EccP384
| KeyAlgorithmId::EccP521
| KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => false,
}
}
pub fn is_ecc(&self) -> bool {
match self {
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => true,
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096
| KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => false,
}
}
pub fn is_mlkem(&self) -> bool {
match self {
| KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => true,
KeyAlgorithmId::EccP256
| KeyAlgorithmId::EccP384
| KeyAlgorithmId::EccP521
| KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => false,
}
}
pub fn as_jwa_name(&self) -> &str {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => "RS256",
KeyAlgorithmId::EccP256 => "ES256,",
KeyAlgorithmId::EccP384 => "ES384",
KeyAlgorithmId::EccP521 => "ES512",
KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => "__UNKNOWN__",
}
}
}
impl FromStr for KeyAlgorithmId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
match s {
"rsa1024" => Some(KeyAlgorithmId::Rsa1024),
"rsa2048" => Some(KeyAlgorithmId::Rsa2048),
"rsa3072" => Some(KeyAlgorithmId::Rsa3072),
"rsa4096" => Some(KeyAlgorithmId::Rsa4096),
"p256" => Some(KeyAlgorithmId::EccP256),
"p384" => Some(KeyAlgorithmId::EccP384),
"p521" => Some(KeyAlgorithmId::EccP521),
"mlkem512" => Some(KeyAlgorithmId::MlKem512),
"mlkem768" => Some(KeyAlgorithmId::MlKem768),
"mlkem1024" => Some(KeyAlgorithmId::MlKem1024),
_ => None,
}
}
}
impl ToStr for KeyAlgorithmId {
fn to_str(&self) -> &str {
match self {
KeyAlgorithmId::Rsa1024 => "rsa1024",
KeyAlgorithmId::Rsa2048 => "rsa2048",
KeyAlgorithmId::Rsa3072 => "rsa3072",
KeyAlgorithmId::Rsa4096 => "rsa4096",
KeyAlgorithmId::EccP256 => "p256",
KeyAlgorithmId::EccP384 => "p384",
KeyAlgorithmId::EccP521 => "p521",
KeyAlgorithmId::MlKem512 => "mlkem512",
KeyAlgorithmId::MlKem768 => "mlkem768",
KeyAlgorithmId::MlKem1024 => "mlkem1024",
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum KeyUsage {
@@ -43,7 +247,18 @@ impl KeyUsage {
}
}
#[allow(dead_code)]
#[allow(clippy::to_string_trait_impl)]
impl ToString for KeyUsage {
fn to_string(&self) -> String {
match self {
KeyUsage::Any => "*",
KeyUsage::Singing => "signing",
KeyUsage::KeyAgreement => "key_agreement",
}
.to_string()
}
}
#[derive(Debug)]
pub struct SecureEnclaveKey {
pub host: String,
@@ -51,21 +266,41 @@ pub struct SecureEnclaveKey {
pub private_key: String,
}
#[derive(Debug)]
pub struct YubikeyPivKey {
pub key_name: String,
pub algorithm: KeyAlgorithmId,
pub slot: SlotId,
}
#[derive(Debug)]
pub struct YubikeyHmacEncSoftKey {
pub key_name: String,
pub algorithm: KeyAlgorithmId,
pub hmac_enc_private_key: String,
}
#[derive(Debug)]
pub struct ExternalCommandKey {
pub external_command: String,
pub algorithm: KeyAlgorithmId,
pub parameter: String,
}
pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
let regex = Regex::new(r##"^key://([a-zA-Z\-\._]*):(\w+)/(\w+):(\w+)?:(.*)$"##).unwrap();
let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap();
let captures = match regex.captures(key_uri) {
None => return simple_error!("Invalid key uri: {}", key_uri),
Some(captures) => captures,
};
let host = captures.get(1).unwrap().as_str();
let host_or_name = captures.get(1).unwrap().as_str();
let module = captures.get(2).unwrap().as_str();
let algorithm = captures.get(3).unwrap().as_str();
let usage = captures.get(4).unwrap().as_str();
let left_part = captures.get(5).unwrap().as_str();
if "se" != module {
return simple_error!("Key uri's module must be se.");
}
match module {
"se" => {
if "p256" != algorithm {
return simple_error!("Key uri's algorithm must be p256.");
}
@@ -75,40 +310,136 @@ pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
}
Some(key_usage) => key_usage,
};
let parsed_key_uri = KeyUri::SecureEnclaveKey(SecureEnclaveKey {
host: host.to_string(),
let parsed_key_uri = KeyUri::SecureEnclave(SecureEnclaveKey {
host: host_or_name.to_string(),
usage: key_usage,
private_key: left_part.to_string(),
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"piv" => {
if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
}
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let slot = opt_value_result!(
SlotId::from_str(left_part),
"Invalid slot id: {}",
left_part
);
let parsed_key_uri = KeyUri::YubikeyPiv(YubikeyPivKey {
key_name: host_or_name.to_string(),
algorithm,
slot,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"soft" => {
if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
}
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let hmac_enc_private_key = left_part.to_string();
let parsed_key_uri = KeyUri::YubikeyHmacEncSoft(YubikeyHmacEncSoftKey {
key_name: host_or_name.to_string(),
algorithm,
hmac_enc_private_key,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"external_command" => {
if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
}
let external_command = opt_result!(
percent_encoding::percent_decode_str(host_or_name).decode_utf8(),
"Decode external command failed: {}"
);
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let parameter = left_part.to_string();
let parsed_key_uri = KeyUri::ExternalCommand(ExternalCommandKey {
external_command: external_command.to_string(),
algorithm,
parameter,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
_ => simple_error!("Key uri's module must be se."),
}
}
#[test]
fn test_parse_key_uri_01() {
let se_key_uri =
parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap();
assert_eq!(
"key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)",
se_key_uri.to_string()
);
match se_key_uri {
KeyUri::SecureEnclaveKey(se_key_uri) => {
KeyUri::SecureEnclave(se_key_uri) => {
assert_eq!("hatter-mac-pro", se_key_uri.host);
assert_eq!(KeyUsage::Singing, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
}
_ => {
panic!("Key uri not parsed")
}
}
}
#[test]
fn test_parse_key_uri_02() {
let se_key_uri =
parse_key_uri("key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)")
parse_key_uri("key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)")
.unwrap();
assert_eq!(
"key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)",
se_key_uri.to_string()
);
match se_key_uri {
KeyUri::SecureEnclaveKey(se_key_uri) => {
assert_eq!("hatter-mac-pro", se_key_uri.host);
KeyUri::SecureEnclave(se_key_uri) => {
assert_eq!("hatter-mac-m1", se_key_uri.host);
assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
}
_ => {
panic!("Key uri not parsed")
}
}
}
#[test]
fn test_parse_key_uri_03() {
let se_key_uri = parse_key_uri("key://yubikey-5n:piv/p256::9a").unwrap();
assert_eq!(
"key://yubikey-5n:piv/p256::authentication",
se_key_uri.to_string()
);
match se_key_uri {
KeyUri::YubikeyPiv(piv_key_uri) => {
assert_eq!("yubikey-5n", piv_key_uri.key_name);
assert_eq!(KeyAlgorithmId::EccP256, piv_key_uri.algorithm);
assert_eq!(SlotId::Authentication, piv_key_uri.slot);
}
_ => {
panic!("Key uri not parsed")
}
}
}

View File

@@ -6,58 +6,73 @@ use rust_util::util_clap::{Command, CommandError};
mod argsutil;
mod cmd_chall;
mod cmd_challconfig;
mod cmd_ecverify;
mod cmd_chall_config;
mod cmd_convert_jwk_to_pem;
mod cmd_convert_pem_to_jwk;
mod cmd_ec_verify;
mod cmd_external_ecdh;
mod cmd_external_public_key;
mod cmd_external_sign;
mod cmd_external_spec;
mod cmd_file_sign;
mod cmd_file_verify;
mod cmd_hmac_decrypt;
mod cmd_hmac_encrypt;
mod cmd_hmac_sha1;
mod cmd_hmacencrypt;
mod cmd_hmacdecrypt;
mod cmd_keypair_generate;
mod cmd_keypair_keychain_export;
mod cmd_keypair_keychain_import;
mod cmd_list;
mod cmd_parseecdsasignature;
#[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgp;
mod cmd_pgpageaddress;
mod cmd_pgpcardadmin;
mod cmd_pgpcarddecrypt;
mod cmd_pgpcardlist;
mod cmd_pgp_age_address;
mod cmd_pgp_card_admin;
mod cmd_pgp_card_decrypt;
mod cmd_pgp_card_list;
#[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgpcardmake;
mod cmd_pgpcardsign;
mod cmd_pgp_card_make;
mod cmd_pgp_card_sign;
mod cmd_piv;
mod cmd_pivdecrypt;
mod cmd_pivecdh;
mod cmd_pivecsign;
mod cmd_pivgenerate;
mod cmd_pivmeta;
mod cmd_pivrsasign;
mod cmd_pivsummary;
mod cmd_pivverify;
mod cmd_rsadecrypt;
mod cmd_rsaencrypt;
mod cmd_rsaverify;
mod cmd_piv_decrypt;
mod cmd_piv_ecdh;
mod cmd_piv_ecsign;
mod cmd_piv_generate;
mod cmd_piv_meta;
mod cmd_piv_rsasign;
mod cmd_piv_summary;
mod cmd_piv_verify;
mod cmd_rsa_decrypt;
mod cmd_rsa_encrypt;
mod cmd_rsa_verify;
mod cmd_se;
mod cmd_se_ecdh;
mod cmd_se_ecsign;
mod cmd_se_generate;
mod cmd_se_recover;
mod cmd_signfile;
mod cmd_signjwt;
mod cmd_signjwtsoft;
mod cmd_sshagent;
mod cmd_sshparse;
mod cmd_sshparsesign;
mod cmd_sshpivcert;
mod cmd_sshpivsign;
mod cmd_sshpubkey;
mod cmd_u2fregister;
mod cmd_u2fsign;
mod cmd_verifyfile;
mod cmd_parseecdsasignature;
mod cmd_generatekeypair;
mod digest;
mod cmd_sign_jwt;
mod cmd_sign_jwt_piv;
mod cmd_sign_jwt_se;
mod cmd_sign_jwt_soft;
mod cmd_ssh_agent;
mod cmd_ssh_agent_gpg;
mod cmd_ssh_parse;
mod cmd_ssh_parse_sign;
mod cmd_ssh_piv_cert;
mod cmd_ssh_piv_sign;
mod cmd_ssh_pub_key;
mod cmd_u2f_register;
mod cmd_u2f_sign;
mod cmdutil;
mod digestutil;
mod ecdhutil;
mod ecdsautil;
mod fido;
mod ecutil;
mod fidoutil;
mod hmacutil;
mod keychain;
mod keyutil;
mod pbeutil;
mod pgpcardutil;
mod pinutil;
mod pivutil;
@@ -67,6 +82,14 @@ mod seutil;
mod signfile;
mod sshutil;
mod util;
mod yubikeyutil;
mod cmd_yubikey;
mod mlkemutil;
use zeroizing_alloc::ZeroAlloc;
#[global_allocator]
static ALLOC: ZeroAlloc<std::alloc::System> = ZeroAlloc(std::alloc::System);
pub struct DefaultCommandImpl;
@@ -97,50 +120,62 @@ fn inner_main() -> CommandError {
Box::new(cmd_list::CommandImpl),
Box::new(cmd_chall::CommandImpl),
Box::new(cmd_hmac_sha1::CommandImpl),
Box::new(cmd_hmacencrypt::CommandImpl),
Box::new(cmd_hmacdecrypt::CommandImpl),
Box::new(cmd_challconfig::CommandImpl),
Box::new(cmd_rsaencrypt::CommandImpl),
Box::new(cmd_rsadecrypt::CommandImpl),
Box::new(cmd_rsaverify::CommandImpl),
Box::new(cmd_hmac_encrypt::CommandImpl),
Box::new(cmd_hmac_decrypt::CommandImpl),
Box::new(cmd_chall_config::CommandImpl),
Box::new(cmd_rsa_encrypt::CommandImpl),
Box::new(cmd_rsa_decrypt::CommandImpl),
Box::new(cmd_rsa_verify::CommandImpl),
#[cfg(feature = "with-sequoia-openpgp")]
Box::new(cmd_pgp::CommandImpl),
Box::new(cmd_pgpcardadmin::CommandImpl),
Box::new(cmd_pgpcardlist::CommandImpl),
Box::new(cmd_pgpcardsign::CommandImpl),
Box::new(cmd_pgpcarddecrypt::CommandImpl),
Box::new(cmd_pgp_card_admin::CommandImpl),
Box::new(cmd_pgp_card_list::CommandImpl),
Box::new(cmd_pgp_card_sign::CommandImpl),
Box::new(cmd_pgp_card_decrypt::CommandImpl),
#[cfg(feature = "with-sequoia-openpgp")]
Box::new(cmd_pgpcardmake::CommandImpl),
Box::new(cmd_pgp_card_make::CommandImpl),
Box::new(cmd_piv::CommandImpl),
Box::new(cmd_pivsummary::CommandImpl),
Box::new(cmd_pivmeta::CommandImpl),
Box::new(cmd_pivverify::CommandImpl),
Box::new(cmd_pivrsasign::CommandImpl),
Box::new(cmd_pivecdh::CommandImpl),
Box::new(cmd_pivecsign::CommandImpl),
Box::new(cmd_pivdecrypt::CommandImpl),
Box::new(cmd_pivgenerate::CommandImpl),
Box::new(cmd_u2fregister::CommandImpl),
Box::new(cmd_u2fsign::CommandImpl),
Box::new(cmd_sshagent::CommandImpl),
Box::new(cmd_sshparsesign::CommandImpl),
Box::new(cmd_sshpivsign::CommandImpl),
Box::new(cmd_sshpivcert::CommandImpl),
Box::new(cmd_sshpubkey::CommandImpl),
Box::new(cmd_sshparse::CommandImpl),
Box::new(cmd_pgpageaddress::CommandImpl),
Box::new(cmd_signjwt::CommandImpl),
Box::new(cmd_signjwtsoft::CommandImpl),
Box::new(cmd_signfile::CommandImpl),
Box::new(cmd_verifyfile::CommandImpl),
Box::new(cmd_piv_summary::CommandImpl),
Box::new(cmd_piv_meta::CommandImpl),
Box::new(cmd_piv_verify::CommandImpl),
Box::new(cmd_piv_rsasign::CommandImpl),
Box::new(cmd_piv_ecdh::CommandImpl),
Box::new(cmd_piv_ecsign::CommandImpl),
Box::new(cmd_piv_decrypt::CommandImpl),
Box::new(cmd_piv_generate::CommandImpl),
Box::new(cmd_u2f_register::CommandImpl),
Box::new(cmd_u2f_sign::CommandImpl),
Box::new(cmd_ssh_agent::CommandImpl),
Box::new(cmd_ssh_agent_gpg::CommandImpl),
Box::new(cmd_ssh_parse_sign::CommandImpl),
Box::new(cmd_ssh_piv_sign::CommandImpl),
Box::new(cmd_ssh_piv_cert::CommandImpl),
Box::new(cmd_ssh_pub_key::CommandImpl),
Box::new(cmd_ssh_parse::CommandImpl),
Box::new(cmd_pgp_age_address::CommandImpl),
Box::new(cmd_sign_jwt_piv::CommandImpl),
Box::new(cmd_sign_jwt_soft::CommandImpl),
Box::new(cmd_sign_jwt_se::CommandImpl),
Box::new(cmd_sign_jwt::CommandImpl),
Box::new(cmd_file_sign::CommandImpl),
Box::new(cmd_file_verify::CommandImpl),
Box::new(cmd_se::CommandImpl),
Box::new(cmd_se_generate::CommandImpl),
Box::new(cmd_se_recover::CommandImpl),
Box::new(cmd_se_ecsign::CommandImpl),
Box::new(cmd_se_ecdh::CommandImpl),
Box::new(cmd_ecverify::CommandImpl),
Box::new(cmd_ec_verify::CommandImpl),
Box::new(cmd_parseecdsasignature::CommandImpl),
Box::new(cmd_generatekeypair::CommandImpl),
Box::new(cmd_keypair_generate::CommandImpl),
Box::new(cmd_keypair_keychain_import::CommandImpl),
Box::new(cmd_keypair_keychain_export::CommandImpl),
Box::new(cmd_convert_pem_to_jwk::CommandImpl),
Box::new(cmd_convert_jwk_to_pem::CommandImpl),
Box::new(cmd_external_spec::CommandImpl),
Box::new(cmd_external_public_key::CommandImpl),
Box::new(cmd_external_sign::CommandImpl),
Box::new(cmd_external_ecdh::CommandImpl),
Box::new(cmd_yubikey::CommandImpl),
];
#[allow(clippy::vec_init_then_push)]

241
src/mlkemutil.rs Normal file
View File

@@ -0,0 +1,241 @@
use crate::util::{base64_encode, to_pem};
use ml_kem::kem::{Decapsulate, Encapsulate};
use ml_kem::{EncodedSizeUser, KemCore, MlKem1024, MlKem512, MlKem768};
use rust_util::XResult;
use std::convert::TryInto;
use crate::pivutil::ToStr;
#[derive(Debug, Clone, Copy)]
pub enum MlKemLen {
Len512,
Len768,
Len1024,
}
impl ToStr for MlKemLen {
fn to_str(&self) -> &str {
match self {
MlKemLen::Len512 => "mlkem512",
MlKemLen::Len768 => "mlkem768",
MlKemLen::Len1024 => "mlkem1024",
}
}
}
pub fn generate_mlkem_keypair(len: usize) -> XResult<(String, String, String, Vec<u8>, String)> {
let (dk_private, ek_public) = match len {
512 => generate_ml_kem_512(),
768 => generate_ml_kem_768(),
1024 => generate_ml_kem_1024(),
_ => return simple_error!("Invalid ML-KEM={}", len),
};
let secret_key_der_base64 = base64_encode(&dk_private);
let secret_key_pem = to_pem(&format!("ML-KEM-{} PRIVATE KEY", len), &dk_private);
let public_key_pem = to_pem(&format!("ML-KEM-{} PUBLIC KEY", len), &ek_public);
let public_key_der = ek_public;
let jwk_ec_key = "".to_string();
Ok((
secret_key_der_base64,
secret_key_pem,
public_key_pem,
public_key_der,
jwk_ec_key,
))
}
pub fn try_parse_decapsulate_key_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<(MlKemLen, Vec<u8>)> {
if let Ok(shared_secret) =
parse_decapsulate_key_512_private_then_decapsulate(key_bytes, ciphertext_bytes)
{
return Ok((MlKemLen::Len512, shared_secret.to_vec()));
}
if let Ok(shared_secret) =
parse_decapsulate_key_768_private_then_decapsulate(key_bytes, ciphertext_bytes)
{
return Ok((MlKemLen::Len768, shared_secret.to_vec()));
}
if let Ok(shared_secret) =
parse_decapsulate_key_1024_private_then_decapsulate(key_bytes, ciphertext_bytes)
{
return Ok((MlKemLen::Len1024, shared_secret.to_vec()));
}
simple_error!("Invalid decapsulation key, only allow MK-KEM-512, ML-KEM-768, ML-KEM-1024")
}
pub fn try_parse_decapsulate_key_private_get_encapsulate(
key_bytes: &[u8],
) -> XResult<(MlKemLen, Vec<u8>)> {
if let Ok(encapsulate_key) = parse_decapsulate_key_512_private_get_encapsulate(key_bytes) {
return Ok((MlKemLen::Len512, encapsulate_key));
}
if let Ok(encapsulate_key) = parse_decapsulate_key_768_private_get_encapsulate(key_bytes) {
return Ok((MlKemLen::Len768, encapsulate_key));
}
if let Ok(encapsulate_key) = parse_decapsulate_key_1024_private_get_encapsulate(key_bytes) {
return Ok((MlKemLen::Len1024, encapsulate_key));
}
simple_error!("Invalid decapsulation key, only allow MK-KEM-512, ML-KEM-768, ML-KEM-1024")
}
pub fn generate_ml_kem_512() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let (dk_private, ek_public) = <MlKem512 as KemCore>::generate(&mut rng);
(
dk_private.as_bytes().0.to_vec(),
ek_public.as_bytes().0.to_vec(),
)
}
pub fn generate_ml_kem_768() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let (dk_private, ek_public) = <MlKem768 as KemCore>::generate(&mut rng);
(
dk_private.as_bytes().0.to_vec(),
ek_public.as_bytes().0.to_vec(),
)
}
pub fn generate_ml_kem_1024() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let (dk_private, ek_public) = <MlKem1024 as KemCore>::generate(&mut rng);
(
dk_private.as_bytes().0.to_vec(),
ek_public.as_bytes().0.to_vec(),
)
}
pub fn try_parse_encapsulation_key_public_then_encapsulate(bytes: &[u8]) -> XResult<(MlKemLen, Vec<u8>, Vec<u8>)> {
if let Ok((ciphertext, shared_key)) = parse_encapsulation_key_512_public_then_encapsulate(bytes) {
return Ok((MlKemLen::Len512, ciphertext, shared_key));
}
if let Ok((ciphertext, shared_key)) = parse_encapsulation_key_768_public_then_encapsulate(bytes) {
return Ok((MlKemLen::Len768, ciphertext, shared_key));
}
if let Ok((ciphertext, shared_key)) = parse_encapsulation_key_1024_public_then_encapsulate(bytes) {
return Ok((MlKemLen::Len1024, ciphertext, shared_key));
}
simple_error!("Invalid encapsulation key, only allow MK-KEM-512, ML-KEM-768, ML-KEM-1024")
}
pub fn parse_encapsulation_key_512_public_then_encapsulate(
bytes: &[u8],
) -> XResult<(Vec<u8>, Vec<u8>)> {
let ek = <MlKem512 as KemCore>::EncapsulationKey::from_bytes(&opt_result!(
bytes.try_into(),
"Parse encapsulation key 512 failed: {}"
));
let (ciphertext, shared_key) = opt_result!(
ek.encapsulate(&mut rand::thread_rng()),
"Encapsulation key 512 encapsulate failed: {:?}"
);
Ok((ciphertext.0.to_vec(), shared_key.0.to_vec()))
}
pub fn parse_encapsulation_key_768_public_then_encapsulate(
bytes: &[u8],
) -> XResult<(Vec<u8>, Vec<u8>)> {
let ek = <MlKem768 as KemCore>::EncapsulationKey::from_bytes(&opt_result!(
bytes.try_into(),
"Parse encapsulation key 768 failed: {}"
));
let (ciphertext, shared_key) = opt_result!(
ek.encapsulate(&mut rand::thread_rng()),
"Encapsulation key 768 encapsulate failed: {:?}"
);
Ok((ciphertext.0.to_vec(), shared_key.0.to_vec()))
}
pub fn parse_encapsulation_key_1024_public_then_encapsulate(
bytes: &[u8],
) -> XResult<(Vec<u8>, Vec<u8>)> {
let ek = <MlKem1024 as KemCore>::EncapsulationKey::from_bytes(&opt_result!(
bytes.try_into(),
"Parse encapsulation key 1024 failed: {}"
));
let (ciphertext, shared_key) = opt_result!(
ek.encapsulate(&mut rand::thread_rng()),
"Encapsulation key 1024 encapsulate failed: {:?}"
);
Ok((ciphertext.0.to_vec(), shared_key.0.to_vec()))
}
pub fn parse_decapsulate_key_512_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<Vec<u8>> {
let dk = <MlKem512 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 512 failed: {}"
));
let shared_key = opt_result!(
dk.decapsulate(opt_result!(
ciphertext_bytes.try_into(),
"Parse encoded ciphertext 512 failed: {}"
)),
"Decapsulation key 512 decapsulate failed: {:?}"
);
Ok(shared_key.0.to_vec())
}
pub fn parse_decapsulate_key_768_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<Vec<u8>> {
let dk = <MlKem768 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 768 failed: {}"
));
let shared_key = opt_result!(
dk.decapsulate(opt_result!(
ciphertext_bytes.try_into(),
"Parse encoded ciphertext 768 failed: {}"
)),
"Decapsulation key 768 decapsulate failed: {:?}"
);
Ok(shared_key.0.to_vec())
}
pub fn parse_decapsulate_key_1024_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<Vec<u8>> {
let dk = <MlKem1024 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 1024 failed: {}"
));
let shared_key = opt_result!(
dk.decapsulate(opt_result!(
ciphertext_bytes.try_into(),
"Parse encoded ciphertext 1024 failed: {}"
)),
"Decapsulation key 1024 decapsulate failed: {:?}"
);
Ok(shared_key.0.to_vec())
}
pub fn parse_decapsulate_key_512_private_get_encapsulate(key_bytes: &[u8]) -> XResult<Vec<u8>> {
let dk = <MlKem512 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 512 failed: {}"
));
Ok(dk.encapsulation_key().as_bytes().0.to_vec())
}
pub fn parse_decapsulate_key_768_private_get_encapsulate(key_bytes: &[u8]) -> XResult<Vec<u8>> {
let dk = <MlKem768 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 768 failed: {}"
));
Ok(dk.encapsulation_key().as_bytes().0.to_vec())
}
pub fn parse_decapsulate_key_1024_private_get_encapsulate(key_bytes: &[u8]) -> XResult<Vec<u8>> {
let dk = <MlKem1024 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 1024 failed: {}"
));
Ok(dk.encapsulation_key().as_bytes().0.to_vec())
}

119
src/pbeutil.rs Normal file
View File

@@ -0,0 +1,119 @@
use crate::digestutil::{copy_sha256, sha256_bytes};
use crate::pinutil;
use crate::util::{base64_decode, base64_encode, base64_encode_url_safe_no_pad};
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use rand::random;
use rust_util::XResult;
use zeroize::Zeroize;
const PBE_ENC_PREFIX: &str = "pbe_enc:";
pub fn simple_pbe_encrypt_with_prompt_from_string(iteration: u32, plaintext: &str, passowrd: &mut Option<String>, password_double_check: bool) -> XResult<String> {
simple_pbe_encrypt_with_prompt(iteration, plaintext.as_bytes(), passowrd, password_double_check)
}
pub fn simple_pbe_decrypt_with_prompt_to_string(pin_opt: &mut Option<String>, ciphertext: &str) -> XResult<String> {
let plaintext = simple_pbe_decrypt_with_prompt(pin_opt, ciphertext)?;
Ok(String::from_utf8(plaintext)?)
}
pub fn simple_pbe_encrypt_with_prompt(iteration: u32, plaintext: &[u8], password_opt: &mut Option<String>, password_double_check: bool) -> XResult<String> {
let mut pin = match password_opt {
None => {
let pin1 = opt_value_result!(pinutil::get_pin(None), "Simple PBE password required");
if password_double_check {
let mut pin2 = opt_value_result!(pinutil::get_pin(None), "Simple PBE password required");
if pin1 != pin2 {
return simple_error!("Two PINs mismatch");
}
pin2.zeroize();
}
*password_opt = Some(pin1.clone());
pin1
}
Some(pin) => pin.clone(),
};
let encrypt_result = simple_pbe_encrypt(&pin, iteration, plaintext);
pin.zeroize();
encrypt_result
}
pub fn simple_pbe_decrypt_with_prompt(pin_opt: &mut Option<String>, ciphertext: &str) -> XResult<Vec<u8>> {
let mut pin = opt_value_result!(pinutil::get_pin(pin_opt.clone().as_deref()), "Simple PBE password required");
pin_opt.zeroize();
*pin_opt = Some(pin.clone());
let decrypt_result = simple_pbe_decrypt(&pin, ciphertext);
pin.zeroize();
decrypt_result
}
pub fn simple_pbe_encrypt(password: &str, iteration: u32, plaintext: &[u8]) -> XResult<String> {
let pbe_salt: [u8; 16] = random();
let key = simple_pbe_kdf(password, &pbe_salt, iteration)?;
let aes_gcm_nonce: [u8; 16] = random();
let mut encryptor = Aes256GcmStreamEncryptor::new(key, &aes_gcm_nonce);
let mut ciphertext = encryptor.update(plaintext);
let (final_part, tag) = encryptor.finalize();
ciphertext.extend_from_slice(&final_part);
ciphertext.extend_from_slice(&tag);
Ok(format!(
"{}{}:{}:{}:{}",
PBE_ENC_PREFIX,
iteration,
base64_encode_url_safe_no_pad(pbe_salt),
base64_encode_url_safe_no_pad(aes_gcm_nonce),
base64_encode(&ciphertext)
))
}
pub fn simple_pbe_decrypt(password: &str, ciphertext: &str) -> XResult<Vec<u8>> {
if !is_simple_pbe_encrypted(ciphertext) {
return simple_error!("Invalid ciphertext: {}", ciphertext);
}
let parts = ciphertext.split(":").collect::<Vec<_>>();
let iteration: u32 = parts[1].parse()?;
let pbe_salt = crate::hmacutil::try_decode_hmac_val(parts[2])?;
let aes_gcm_nonce = crate::hmacutil::try_decode_hmac_val(parts[3])?;
let ciphertext = base64_decode(parts[4])?;
let key = simple_pbe_kdf(password, &pbe_salt, iteration)?;
let mut decryptor = Aes256GcmStreamDecryptor::new(key, &aes_gcm_nonce);
let mut plaintext = decryptor.update(&ciphertext);
let final_part = decryptor.finalize()?;
plaintext.extend_from_slice(&final_part);
Ok(plaintext)
}
pub fn is_simple_pbe_encrypted(ciphertext: &str) -> bool {
ciphertext.starts_with(PBE_ENC_PREFIX)
}
fn simple_pbe_kdf(password: &str, pbe_salt: &[u8], iteration: u32) -> XResult<[u8; 32]> {
let mut init_data = password.as_bytes().to_vec();
init_data.extend_from_slice(pbe_salt);
let mut loop_hash = sha256_bytes(&init_data);
for i in 0..iteration {
let i_to_bytes = i.to_be_bytes();
loop_hash[..4].copy_from_slice(&i_to_bytes);
loop_hash = sha256_bytes(&loop_hash);
}
let key = copy_sha256(&sha256_bytes(&loop_hash))?;
Ok(key)
}
#[test]
fn test_simple_pbe_kdf() {
assert_eq!("cea7eb6424132aa4d6b1880c3cb570df17c41d766826e144088fd16b334c80c5",
hex::encode(simple_pbe_kdf("hello_world", b"salt", 1).unwrap()));
assert_eq!("e3ed4a0a2f451180cfa4a0f8f3e1181d8863fc192d161e4c1480e3612135ca27",
hex::encode(simple_pbe_kdf("hello_world", b"salt", 2).unwrap()));
assert_eq!("e552900ad3f14a96629c8056bd7bc4b2431250ef4a47e78856626f45807cd87e",
hex::encode(simple_pbe_kdf("hello_world 2", b"salt", 2).unwrap()));
assert_eq!("37e71484f00033c99db444c77553364b31614d5145e38aad5bd5caa6676d40f9",
hex::encode(simple_pbe_kdf("hello_world", b"salt 2", 2).unwrap()));
}

View File

@@ -1,4 +1,5 @@
use clap::ArgMatches;
use jwt::AlgorithmType;
use rust_util::XResult;
use spki::{ObjectIdentifier, SubjectPublicKeyInfoOwned};
use spki::der::{Decode, Encode};
@@ -56,6 +57,32 @@ pub trait ToStr {
fn to_str(&self) -> &str;
}
pub trait FromStr {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized;
}
impl ToStr for AlgorithmType {
fn to_str(&self) -> &str {
match self {
AlgorithmType::Hs256 => "HS256",
AlgorithmType::Hs384 => "HS384",
AlgorithmType::Hs512 => "HS512",
AlgorithmType::Rs256 => "RS256",
AlgorithmType::Rs384 => "RS384",
AlgorithmType::Rs512 => "RS512",
AlgorithmType::Es256 => "ES256",
AlgorithmType::Es384 => "ES384",
AlgorithmType::Es512 => "ES512",
AlgorithmType::Ps256 => "PS256",
AlgorithmType::Ps384 => "PS384",
AlgorithmType::Ps512 => "PS512",
AlgorithmType::None => "NONE",
}
}
}
impl ToStr for PinPolicy {
fn to_str(&self) -> &str {
match self {
@@ -78,6 +105,21 @@ impl ToStr for TouchPolicy {
}
}
impl FromStr for AlgorithmId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
match s {
"rsa1024" => Some(AlgorithmId::Rsa1024),
"rsa2048" => Some(AlgorithmId::Rsa2048),
"p256" => Some(AlgorithmId::EccP256),
"p384" => Some(AlgorithmId::EccP384),
_ => None,
}
}
}
impl ToStr for AlgorithmId {
fn to_str(&self) -> &str {
match self {
@@ -169,6 +211,7 @@ pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
"9c" | "sign" | "signature" => SlotId::Signature,
"9d" | "keym" | "keymanagement" => SlotId::KeyManagement,
"9e" | "card" | "cardauthentication" => SlotId::CardAuthentication,
"f9" | "attest" | "attestation" => SlotId::Attestation,
"r1" | "82" => SlotId::Retired(RetiredSlotId::R1),
"r2" | "83" => SlotId::Retired(RetiredSlotId::R2),
"r3" | "84" => SlotId::Retired(RetiredSlotId::R3),
@@ -193,6 +236,54 @@ pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
})
}
impl FromStr for SlotId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
get_slot_id(s).ok()
}
}
impl ToStr for SlotId {
fn to_str(&self) -> &str {
match self {
SlotId::Authentication => "authentication",
SlotId::Signature => "signature",
SlotId::KeyManagement => "keymanagement",
SlotId::CardAuthentication => "cardauthentication",
SlotId::Retired(retried) => match retried {
RetiredSlotId::R1 => "r1",
RetiredSlotId::R2 => "r2",
RetiredSlotId::R3 => "r3",
RetiredSlotId::R4 => "r4",
RetiredSlotId::R5 => "r5",
RetiredSlotId::R6 => "r6",
RetiredSlotId::R7 => "r7",
RetiredSlotId::R8 => "r8",
RetiredSlotId::R9 => "r9",
RetiredSlotId::R10 => "r10",
RetiredSlotId::R11 => "r11",
RetiredSlotId::R12 => "r12",
RetiredSlotId::R13 => "r13",
RetiredSlotId::R14 => "r14",
RetiredSlotId::R15 => "r15",
RetiredSlotId::R16 => "r16",
RetiredSlotId::R17 => "r17",
RetiredSlotId::R18 => "r18",
RetiredSlotId::R19 => "r19",
RetiredSlotId::R20 => "r20",
}
SlotId::Attestation => "attestation",
SlotId::Management(management) => match management {
ManagementSlotId::Pin => "pin",
ManagementSlotId::Puk => "puk",
ManagementSlotId::Management => "management",
}
}
}
}
pub fn check_read_pin(yk: &mut YubiKey, slot_id: SlotId, sub_arg_matches: &ArgMatches) -> Option<String> {
if never_use_pin(yk, slot_id) {
None

View File

@@ -7,7 +7,7 @@ use rust_util::XResult;
use sequoia_openpgp::crypto::mpi::PublicKey;
use x509_parser::x509::AlgorithmIdentifier;
use crate::digest::sha256_bytes;
use crate::digestutil::sha256_bytes;
#[derive(Clone, Copy, Debug)]
pub enum PkiAlgorithm {

View File

@@ -1,8 +1,65 @@
use std::collections::HashMap;
use ecdsa::elliptic_curve::rand_core::OsRng;
use openssl::bn::{BigNum, BigNumContext};
use openssl::pkey::PKey;
use openssl::rsa::{Padding, Rsa};
use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
use rust_util::{util_msg, XResult};
use rust_util::util_msg::MessageType;
use spki::DecodePublicKey;
use rsa::pkcs1::DecodeRsaPublicKey;
use rsa::traits::PublicKeyParts;
use spki::EncodePublicKey;
use rsa::pkcs1::LineEnding;
use rsa::pkcs8::EncodePrivateKey;
use sha2::{Sha256, Sha384, Sha512};
use crate::digestutil;
use crate::util::{base64_decode, base64_encode};
pub enum RsaSignAlgorithm {
Rs256,
Rs384,
Rs512,
}
impl RsaSignAlgorithm {
pub fn from_str(alg: &str) -> Option<RsaSignAlgorithm> {
match alg {
"RS256" => Some(RsaSignAlgorithm::Rs256),
"RS384" => Some(RsaSignAlgorithm::Rs384),
"RS512" => Some(RsaSignAlgorithm::Rs512),
_ => None
}
}
}
pub fn sign(rsa_private_key: &RsaPrivateKey, rsa_sign_algorithm: RsaSignAlgorithm, message: &[u8], is_raw: bool) -> XResult<Vec<u8>> {
match rsa_sign_algorithm {
RsaSignAlgorithm::Rs256 => {
let raw_in = iff!(is_raw, digestutil::sha256_bytes(message), message.to_vec());
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha256>(), &raw_in)?)
}
RsaSignAlgorithm::Rs384 => {
let raw_in = iff!(is_raw, digestutil::sha384_bytes(message), message.to_vec());
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha384>(), &raw_in)?)
}
RsaSignAlgorithm::Rs512 => {
let raw_in = iff!(is_raw, digestutil::sha512_bytes(message), message.to_vec());
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha512>(), &raw_in)?)
}
}
}
pub fn generate_rsa_keypair(bit_size: usize) -> XResult<(String, String, String, Vec<u8>, String)> {
let rsa_private_key = opt_result!(RsaPrivateKey::new(&mut OsRng, bit_size), "Generate RSA private key failed: {}");
let rsa_public_key = rsa_private_key.to_public_key();
let secret_key_der_base64 = base64_encode(rsa_private_key.to_pkcs8_der()?.as_bytes());
let secret_key_pem = rsa_private_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
let public_key_pem = rsa_public_key.to_public_key_pem(LineEnding::LF)?;
let public_key_der = rsa_public_key.to_public_key_der()?.to_vec();
let jwk_ec_key = rsa_public_key_to_jwk(&rsa_public_key)?;
Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key))
}
#[derive(Debug)]
pub struct RsaCrt {
@@ -151,3 +208,43 @@ fn pkcs1_padding_for_sign(bs: &[u8], bit_len: usize) -> XResult<Vec<u8>> {
output.extend_from_slice(bs);
Ok(output)
}
pub fn convert_rsa_to_jwk(public_key: &str) -> XResult<String> {
let rsa_public_key = try_parse_rsa(public_key)?;
rsa_public_key_to_jwk(&rsa_public_key)
}
pub fn rsa_public_key_to_jwk(rsa_public_key: &RsaPublicKey) -> XResult<String> {
let e_bytes = rsa_public_key.e().to_bytes_be();
let n_bytes = rsa_public_key.n().to_bytes_be();
let mut jwk = HashMap::new();
jwk.insert("kty", "RSA".to_string());
jwk.insert("n", base64_encode(&n_bytes));
jwk.insert("e", base64_encode(&e_bytes));
Ok(serde_json::to_string(&jwk).unwrap())
}
pub fn try_parse_rsa(public_key: &str) -> XResult<RsaPublicKey> {
debugging!("Try parse RSA public key PEM.");
// parse RSA public key PEM not works? why?
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_pem(public_key) {
return Ok(rsa_public_key);
}
debugging!("Try parse RSA PKCS#1 public key PEM.");
if let Ok(rsa_public_key) = RsaPublicKey::from_pkcs1_pem(public_key) {
return Ok(rsa_public_key);
}
if let Ok(public_key_der) = base64_decode(public_key) {
debugging!("Try parse RSA public key DER.");
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_der(&public_key_der) {
return Ok(rsa_public_key);
}
debugging!("Try parse RSA PKCS#1 public key DER.");
if let Ok(rsa_public_key) = RsaPublicKey::from_pkcs1_der(&public_key_der) {
return Ok(rsa_public_key);
}
}
simple_error!("Invalid RSA public key.")
}

View File

@@ -1,22 +1,49 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use crate::util::{base64_decode, base64_encode};
use rust_util::XResult;
use swift_secure_enclave_tool_rs::KeyPurpose;
use se_tool::KeyPurpose;
use swift_secure_enclave_tool_rs as se_tool;
use swift_secure_enclave_tool_rs::{ControlFlag, DigestType, KeyMlKem};
pub fn is_support_se() -> bool {
swift_secure_enclave_tool_rs::is_secure_enclave_supported().unwrap_or(false)
se_tool::is_secure_enclave_supported().unwrap_or_else(|e| {
failure!("Invoke command swift-secure-enclave-tool failed: {}", e);
false
})
}
pub fn generate_secure_enclave_p256_keypair(sign: bool) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let key_material = if sign {
swift_secure_enclave_tool_rs::generate_ecdsa_keypair(KeyPurpose::Signing, true)?
pub fn check_se_supported() -> XResult<()> {
if !is_support_se() {
simple_error!("Secure Enclave is NOT supported.")
} else {
swift_secure_enclave_tool_rs::generate_ecdsa_keypair(KeyPurpose::KeyAgreement, true)?
Ok(())
}
}
pub fn generate_secure_enclave_p256_keypair(
sign: bool,
control_flag: ControlFlag,
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let key_material = if sign {
se_tool::generate_keypair(KeyPurpose::Signing, control_flag)?
} else {
se_tool::generate_keypair(KeyPurpose::KeyAgreement, control_flag)?
};
Ok((
key_material.public_key_point,
key_material.public_key_der,
STANDARD.encode(&key_material.private_key_representation),
base64_encode(&key_material.private_key_representation),
))
}
pub fn generate_secure_enclave_mlkem_keypair(
key_ml_kem: KeyMlKem,
control_flag: ControlFlag,
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let key_material = se_tool::generate_mlkem_keypair(key_ml_kem, control_flag)?;
Ok((
key_material.public_key_point,
key_material.public_key_der,
base64_encode(&key_material.private_key_representation),
))
}
@@ -24,22 +51,16 @@ pub fn recover_secure_enclave_p256_public_key(
private_key: &str,
sign: bool,
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let private_key_representation = STANDARD.decode(private_key)?;
let private_key_representation = base64_decode(private_key)?;
let key_material = if sign {
swift_secure_enclave_tool_rs::recover_ecdsa_keypair(
KeyPurpose::Signing,
&private_key_representation,
)
se_tool::recover_keypair(KeyPurpose::Signing, &private_key_representation)
} else {
swift_secure_enclave_tool_rs::recover_ecdsa_keypair(
KeyPurpose::KeyAgreement,
&private_key_representation,
)
se_tool::recover_keypair(KeyPurpose::KeyAgreement, &private_key_representation)
}?;
Ok((
key_material.public_key_point,
key_material.public_key_der,
STANDARD.encode(&key_material.private_key_representation),
base64_encode(&key_material.private_key_representation),
))
}
@@ -47,17 +68,14 @@ pub fn secure_enclave_p256_dh(
private_key: &str,
ephemeral_public_key_bytes: &[u8],
) -> XResult<Vec<u8>> {
let private_key_representation = STANDARD.decode(private_key)?;
let shared_secret = swift_secure_enclave_tool_rs::private_key_ecdh(
&private_key_representation,
ephemeral_public_key_bytes,
)?;
let private_key_representation = base64_decode(private_key)?;
let shared_secret =
se_tool::private_key_ecdh(&private_key_representation, ephemeral_public_key_bytes)?;
Ok(shared_secret)
}
pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8]) -> XResult<Vec<u8>> {
let private_key_representation = STANDARD.decode(private_key)?;
let signature =
swift_secure_enclave_tool_rs::private_key_ecdsa_sign(&private_key_representation, content)?;
pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8], digest_type: DigestType) -> XResult<Vec<u8>> {
let private_key_representation = base64_decode(private_key)?;
let signature = se_tool::private_key_sign_digested(&private_key_representation, content, digest_type)?;
Ok(signature)
}

View File

@@ -1,9 +1,10 @@
use std::fs;
use std::io::Read;
use base64::{DecodeError, Engine};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::{DecodeError, Engine};
use rust_util::XResult;
use serde::Serialize;
pub fn base64_encode<T: AsRef<[u8]>>(input: T) -> String {
STANDARD.encode(input)
@@ -21,6 +22,21 @@ pub fn base64_uri_decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeErro
URL_SAFE_NO_PAD.decode(input)
}
pub fn to_pem(header: &str, bytes: &[u8]) -> String {
let mut buf = String::new();
buf.push_str(&format!("-----BEGIN {}-----\n", header));
let bas64ed = base64_encode(bytes);
let len = bas64ed.len();
for (i, c) in bas64ed.chars().enumerate() {
buf.push(c);
if i > 0 && i < len && i % 64 == 0 {
buf.push('\n');
}
}
buf.push_str(&format!("\n-----END {}-----\n", header));
buf
}
pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
match hex::decode(input) {
Ok(v) => Ok(v),
@@ -30,7 +46,7 @@ pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
Ok(v) => Ok(v),
Err(e) => simple_error!("decode hex or base64 error: {}", e),
},
}
},
}
}
@@ -45,6 +61,17 @@ pub fn read_file_or_stdin(file: &str) -> XResult<Vec<u8>> {
if file == "-" {
read_stdin()
} else {
Ok(opt_result!(fs::read(file), "Read file: {} failed: {}", file))
Ok(opt_result!(
fs::read(file),
"Read file: {} failed: {}",
file
))
}
}
pub fn print_pretty_json<T>(value: &T)
where
T: ?Sized + Serialize,
{
println!("{}", serde_json::to_string_pretty(value).unwrap());
}

58
src/yubikeyutil.rs Normal file
View File

@@ -0,0 +1,58 @@
use crate::pivutil::slot_equals;
use clap::ArgMatches;
use rust_util::XResult;
use yubikey::piv::SlotId;
use yubikey::{Key, Serial, YubiKey};
pub fn open_yubikey_with_args(sub_arg_matches: &ArgMatches) -> XResult<YubiKey> {
let serial_opt = sub_arg_matches.value_of("serial");
open_yubikey_with_serial(&serial_opt)
}
pub fn open_yubikey_with_serial(serial_opt: &Option<&str>) -> XResult<YubiKey> {
match serial_opt {
None => open_yubikey(),
Some(serial) => {
let serial_no: u32 = opt_result!(serial.parse(), "{}");
Ok(opt_result!(
YubiKey::open_by_serial(Serial(serial_no)),
"YubiKey with serial: {} not found: {}",
serial
))
}
}
}
pub fn open_yubikey() -> XResult<YubiKey> {
Ok(opt_result!(YubiKey::open(), "YubiKey not found: {}"))
}
pub fn open_and_find_key(slot_id: &SlotId, sub_arg_matches: &ArgMatches) -> XResult<Option<Key>> {
let mut yk = open_yubikey_with_args(sub_arg_matches)?;
find_key(&mut yk, slot_id)
}
pub fn find_key(yk: &mut YubiKey, slot_id: &SlotId) -> XResult<Option<Key>> {
match Key::list(yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => return Ok(filter_key(keys, slot_id)),
}
Ok(None)
}
pub fn find_key_or_error(yk: &mut YubiKey, slot_id: &SlotId) -> XResult<Option<Key>> {
match Key::list(yk) {
Err(e) => simple_error!("List keys failed: {}", e),
Ok(keys) => Ok(filter_key(keys, slot_id)),
}
}
fn filter_key(keys: Vec<Key>, slot_id: &SlotId) -> Option<Key> {
for k in keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(slot_id, &slot_str) {
return Some(k);
}
}
None
}