Compare commits

..

102 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
4a48e932d4 Merge pull request 'feat: v1.11.0, remove swiftc dependency' (#10) from use-swift-se-tool into master
Reviewed-on: #10
2025-03-24 00:38:11 +08:00
8b6056db34 feat: v1.11.0, remove swiftc dependency 2025-03-24 00:05:30 +08:00
111 changed files with 5563 additions and 4804 deletions

2312
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,16 @@
[package] [package]
name = "card-cli" name = "card-cli"
version = "1.10.21" version = "1.13.21"
authors = ["Hatter Jiang <jht5945@gmail.com>"] authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018" 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] [features]
default = ["with-sequoia-openpgp", "with-secure-enclave"] default = ["with-sequoia-openpgp"]
with-sequoia-openpgp = ["sequoia-openpgp", "openpgp-card-sequoia"] with-sequoia-openpgp = ["sequoia-openpgp", "openpgp-card-sequoia"]
with-secure-enclave = ["swift-rs"]
[dependencies] [dependencies]
authenticator = "0.3" authenticator = "0.3"
@@ -17,15 +20,14 @@ digest = "0.10"
sha1 = "0.10" sha1 = "0.10"
sha2 = "0.10" sha2 = "0.10"
rand = "0.8" rand = "0.8"
base64 = "0.21" base64 = "0.22"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
hex = "0.4" hex = "0.4"
u2f = "0.2"
openpgp-card = "0.3" openpgp-card = "0.3"
openpgp-card-pcsc = "0.3" openpgp-card-pcsc = "0.3"
openpgp-card-sequoia = { version = "0.1", optional = true } openpgp-card-sequoia = { version = "0.2", optional = true }
sequoia-openpgp = { version = "1.0", optional = true } sequoia-openpgp = { version = "2.0", optional = true }
chrono = "0.4" chrono = "0.4"
simpledateformat = "0.1" simpledateformat = "0.1"
ring = "0.17" ring = "0.17"
@@ -34,31 +36,41 @@ pem = "3.0"
yubikey = { version = "0.8", features = ["untested"] } yubikey = { version = "0.8", features = ["untested"] }
yubico_manager = "0.9" yubico_manager = "0.9"
x509 = "0.2" x509 = "0.2"
x509-parser = { version = "0.15", features = ["verify"] } x509-parser = { version = "0.17", features = ["verify"] }
ssh-agent = { version = "0.2", features = ["agent"] } ssh-agent = { version = "0.2", features = ["agent"] }
p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa"] } p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa"] } p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
p521 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
spki = { version = "0.7", features = ["pem"] } spki = { version = "0.7", features = ["pem"] }
tabled = "0.14" tabled = "0.20"
env_logger = "0.10" env_logger = "0.11"
bech32 = "0.9" bech32 = "0.9"
ecdsa = { version = "0.16", features = ["verifying", "spki", "pem", "der"] } ecdsa = { version = "0.16", features = ["verifying", "spki", "pem", "der"] }
jwt = "0.16" jwt = "0.16"
reqwest = { version = "0.11", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] }
pinentry = "0.5" pinentry = "0.6"
rpassword = "7.3" rpassword = "7.3"
secrecy = "0.8" secrecy = "0.10"
der-parser = "9.0" der-parser = "10.0"
sshcerts = "0.13" sshcerts-hatter-fork = "0.14.1"
swift-rs = { version = "1.0.7", optional = true } regex = "1.11"
regex = "1.4.6" aes-gcm-stream = "0.2"
aes-gcm-stream = "0.2.4" 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" #lazy_static = "1.4.0"
#ssh-key = "0.4.0"
#ctap-hid-fido2 = "2.1.3" #ctap-hid-fido2 = "2.1.3"
[build-dependencies] #[patch.crates-io]
swift-rs = { version = "1.0.7", features = ["build"], optional = true } #ml-kem = { path = "externals/ml-kem" }
[patch.crates-io]
u2f = { git = "https://github.com/jht5945/u2f-rs.git" }

View File

@@ -247,6 +247,10 @@ SSH to server:
ssh -i id_user root@example.com 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> <br><br>

View File

@@ -1,9 +0,0 @@
fn main() {
// Ensure this matches the versions set in your `Package.swift` file.
#[cfg(feature = "with-secure-enclave")]
swift_rs::SwiftLinker::new("11")
.with_ios("11")
.with_package("swift-lib", "./swift-lib/")
.link();
}

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 @just --list
# publish
publish:
cargo publish --registry crates-io
# install card-cli # install card-cli
install: install:
cargo install --path . cargo install --path .

View File

@@ -5,14 +5,22 @@ use std::io::Read;
use clap::ArgMatches; use clap::ArgMatches;
use rust_util::XResult; 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>> { 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) 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>> { 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); let file_opt = file_opt.as_ref().map(String::as_str);
if let Some(file) = sub_arg_matches.value_of("file").or(file_opt) { 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); 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 f = opt_result!(File::open(file), "Open file: {} failed: {}", file);
let mut content = vec![]; let mut content = vec![];
opt_result!(f.read_to_end(&mut content), "Read file: {} failed: {}", file); 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") { } 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") { } else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") {
Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}")) Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}"))
} else { } else {

View File

@@ -1,8 +1,7 @@
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::hmacutil; use crate::{cmdutil, hmacutil};
pub struct CommandImpl; 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("sha256").short("2").long("sha256").help("Output SHA256"))
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA384")) .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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?; let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
let hmac_result = hmacutil::compute_yubikey_hmac(&challenge_bytes)?; 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 clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use crate::ecdsautil::EcdsaAlgorithm; use crate::ecdsautil::EcdsaAlgorithm;
use crate::{argsutil, ecdsautil}; use crate::{argsutil, cmdutil, ecdsautil, util};
pub struct CommandImpl; 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("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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let hash_bytes = argsutil::get_sha256_digest_or_hash(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") { 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)); 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(_) => { Ok(_) => {
success!("Verify ECDSA succeed."); success!("Verify ECDSA succeed.");
if json_output { if json_output {
@@ -72,7 +70,7 @@ impl Command for CommandImpl {
} }
if json_output { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) 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::{Key, YubiKey};
use yubikey::piv::{sign_data, SlotId}; use yubikey::piv::{sign_data, SlotId};
use crate::{argsutil, pinutil, pivutil}; use crate::{argsutil, cmdutil, pinutil, pivutil};
use crate::digest::sha256_bytes; 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::signfile::{CERTIFICATES_SEARCH_URL, HASH_ALGORITHM_SHA256, SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA, SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile, SimpleSignFileSignature};
use crate::util::base64_encode; use crate::util::base64_encode;
@@ -36,14 +36,13 @@ pub struct CommandImpl;
// all hex is in lower case default // all hex is in lower case default
// file ext: *.simple-sig // file ext: *.simple-sig
impl Command for CommandImpl { impl Command for CommandImpl {
fn name(&self) -> &str { "sign-file" } fn name(&self) -> &str { "file-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV sign(with SHA256) subcommand") 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(cmdutil::build_slot_arg())
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN")) .arg(cmdutil::build_pin_arg())
.arg(Arg::with_name("slot").short("s").long("slot") .arg(cmdutil::build_no_pin_arg())
.takes_value(true).required(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).required(true).help("Input file")) .arg(Arg::with_name("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("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")) .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 x509_parser::time::ASN1Time;
use crate::argsutil; use crate::argsutil;
use crate::digest::sha256_bytes; use crate::digestutil::sha256_bytes;
use crate::signfile::{SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile}; use crate::signfile::{SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile};
use crate::util::base64_decode; use crate::util::base64_decode;
pub struct CommandImpl; pub struct CommandImpl;
impl Command for CommandImpl { impl Command for CommandImpl {
fn name(&self) -> &str { "verify-file" } fn name(&self) -> &str { "file-verify" }
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV verify(with SHA256) subcommand") SubCommand::with_name(self.name()).about("PIV verify(with SHA256) subcommand")

View File

@@ -1,62 +0,0 @@
use crate::ecdsautil;
use crate::keyutil::{parse_key_uri, KeyUri};
use clap::{App, Arg, 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;
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 clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::{util_msg, XResult}; use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use crate::hmacutil; use crate::{cmdutil, hmacutil};
pub struct CommandImpl; 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("sha256").short("2").long("sha256").help("Output SHA256"))
.arg(Arg::with_name("sha384").short("3").long("sha384").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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let variable = sub_arg_matches.is_present("variable"); let variable = sub_arg_matches.is_present("variable");
let secret_bytes = get_secret_bytes(sub_arg_matches)?; 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 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_clap::{Command, CommandError};
use rust_util::util_msg; use serde_json::Value;
use yubikey::YubiKey; use crate::{cmdutil, util, yubikeyutil};
pub struct CommandImpl; pub struct CommandImpl;
@@ -12,34 +12,37 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("YubiKey list") 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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if json_output { if json_output {
let mut json = BTreeMap::<&'_ str, String>::new(); let mut json = BTreeMap::<&'_ str, Value>::new();
json.insert("name", yk.name().to_string()); json.insert("name", yk.name().into());
json.insert("version", yk.version().to_string()); json.insert("version", yk.version().to_string().into());
json.insert("serial", yk.serial().0.to_string()); json.insert("serial", yk.serial().0.into());
if let Ok(pin_retries) = yk.get_pin_retries() { 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() { 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() { 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() { 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 { } else {
success!("Name: {}", yk.name()); success!("Name: {}", yk.name());
success!("Version: {}", yk.version()); success!("Version: {}", yk.version());

View File

@@ -3,13 +3,10 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; 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::ecdsautil::parse_ecdsa_r_and_s;
use crate::util::try_decode; use crate::util::try_decode;
const SEPARATOR: &str = ".";
pub struct CommandImpl; pub struct CommandImpl;
impl Command for CommandImpl { impl Command for CommandImpl {
@@ -27,14 +24,11 @@ impl Command for CommandImpl {
.takes_value(true) .takes_value(true)
.help("ECDSA signature"), .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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output {
util_msg::set_logger_std_out(false);
}
let mut json = BTreeMap::<&'_ str, String>::new(); let mut json = BTreeMap::<&'_ str, String>::new();
@@ -57,7 +51,7 @@ impl Command for CommandImpl {
} }
if json_output { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) Ok(None)
} }

View File

@@ -1,5 +1,3 @@
use std::ops::Deref;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; 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("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("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("verbose").long("verbose").help("Verbose 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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -151,7 +149,7 @@ impl Command for CommandImpl {
debugging!("Found PKESK: {:?}", pkesk); debugging!("Found PKESK: {:?}", pkesk);
match pkesk { match pkesk {
PKESK::V3(pkesk3) => { 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), unknown => warning!("Unknown PKESK: {:?}", unknown),
} }
@@ -162,19 +160,21 @@ impl Command for CommandImpl {
Packet::SEIP(seip) => { Packet::SEIP(seip) => {
debugging!("Found SEIP: {:?}", seip); debugging!("Found SEIP: {:?}", seip);
match 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::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()), 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)] #[allow(deprecated)]
Packet::MDC(mdc) => { Packet::MDC(mdc) => {
information!("Found MDC: {:?}", mdc); information!("Found MDC: {:?}", mdc);
} }
Packet::AED(aed) => {
information!("Found AED: {:?}", aed);
}
Packet::Unknown(unknown) => { Packet::Unknown(unknown) => {
warning!("Found unknown: {:?}", unknown); warning!("Found unknown: {:?}", unknown);
} }

View File

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

View File

@@ -2,10 +2,10 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::crypto_data::Cryptogram; use openpgp_card::crypto_data::Cryptogram;
use rust_util::{util_msg, XResult}; use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError}; 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}; use crate::util::{base64_encode, read_stdin, try_decode};
#[derive(Debug, Clone, Copy)] #[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("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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin")); 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); let pin_opt = pinutil::get_pin(pin_opt);
@@ -89,7 +88,7 @@ impl Command for CommandImpl {
json.insert("text_utf8", text); json.insert("text_utf8", text);
} }
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) Ok(None)
} }

View File

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

View File

@@ -1,15 +1,15 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs::File; use std::fs::File;
use std::io::{ErrorKind, Read}; use std::io::{ErrorKind, Read};
use std::ops::Deref;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use digest::Digest; use digest::Digest;
use openpgp_card::crypto_data::Hash; use openpgp_card::crypto_data::Hash;
use rust_util::{util_msg, XResult}; use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use sha2::{Sha256, Sha384, Sha512}; use sha2::{Sha256, Sha384, Sha512};
use crate::{pgpcardutil, pinutil}; use crate::{cmdutil, pgpcardutil, pinutil, util};
use crate::util::base64_encode; use crate::util::base64_encode;
const BUFF_SIZE: usize = 512 * 1024; 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-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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin")); 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); let pin_opt = pinutil::get_pin(pin_opt);
@@ -115,7 +114,7 @@ impl Command for CommandImpl {
if let Some(sha256) = sha256 { if let Some(sha256) = sha256 {
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}"); 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: {}"); opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!"); success!("User sign pin verify success!");
let sig = match algo { let sig = match algo {
@@ -134,7 +133,7 @@ impl Command for CommandImpl {
} }
if let Some(sha384) = sha384 { if let Some(sha384) = sha384 {
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}"); 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: {}"); opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!"); success!("User sign pin verify success!");
let sig = match algo { let sig = match algo {
@@ -153,7 +152,7 @@ impl Command for CommandImpl {
} }
if let Some(sha512) = sha512 { if let Some(sha512) = sha512 {
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}"); 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: {}"); opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!"); success!("User sign pin verify success!");
let sig = match algo { let sig = match algo {
@@ -172,7 +171,7 @@ impl Command for CommandImpl {
} }
if json_output { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) Ok(None)
@@ -190,7 +189,7 @@ where
debugging!("File: {}, length: {}", file_name, file_len); debugging!("File: {}, length: {}", file_name, file_len);
loop { loop {
let len = match f.read(&mut buf) { 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, Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return simple_error!("Calc file digest failed: {}", e), 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 x509_parser::parse_x509_certificate;
use yubikey::{Certificate, YubiKey}; use yubikey::{Certificate, YubiKey};
use yubikey::piv::SlotId; use yubikey::piv::SlotId;
use crate::{cmdutil, yubikeyutil};
use crate::pivutil::get_algorithm_id; use crate::pivutil::get_algorithm_id;
use crate::pkiutil::{bytes_to_pem, get_pki_algorithm}; 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") SubCommand::with_name(self.name()).about("PIV subcommand")
.arg(Arg::with_name("detail").long("detail").help("Detail output")) .arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("show-config").long("show-config").help("Show config 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")) // .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 detail_output = sub_arg_matches.is_present("detail");
let show_config = sub_arg_matches.is_present("show-config"); 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()); success!("Name: {}", yk.name());
information!("Version: {}", yk.version()); information!("Version: {}", yk.version());
information!("Serial: {}", yk.serial()); information!("Serial: {}", yk.serial());

View File

@@ -2,11 +2,9 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::piv::AlgorithmId; 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}; use crate::util::{read_stdin, try_decode};
pub struct CommandImpl; pub struct CommandImpl;
@@ -16,17 +14,17 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV decrypt(RSA) subcommand") 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(cmdutil::build_slot_arg())
.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("no-pin").long("no-pin").help("No PIN")) .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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { 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 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"); 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 { if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); 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("decrypted_data_hex", hex::encode(decrypted_data_bytes));
json.insert("clear_data_hex", hex::encode(clear_data)); json.insert("clear_data_hex", hex::encode(clear_data));
json.insert("clear_data", String::from_utf8_lossy(clear_data).to_string()); 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) Ok(None)
} }

View File

@@ -4,11 +4,10 @@ use std::fs;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use yubikey::PinPolicy;
use yubikey::{PinPolicy, YubiKey};
use yubikey::piv::{AlgorithmId, decrypt_data, metadata}; 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; use crate::pivutil::get_algorithm_id;
pub struct CommandImpl; pub struct CommandImpl;
@@ -18,9 +17,9 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV ECDH subcommand") 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(cmdutil::build_slot_arg())
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN")) .arg(cmdutil::build_pin_arg())
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ...")) .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-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("public-384").long("public-384").help("Public key (P-384)"))
.arg(Arg::with_name("private").long("private").help("Private key(PIV)")) .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").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-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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let public256 = sub_arg_matches.is_present("public-256"); let public256 = sub_arg_matches.is_present("public-256");
let public384 = sub_arg_matches.is_present("public-384"); 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 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 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)?; let slot_id = pivutil::get_slot_id(slot)?;
debugging!("Slot id: {}", slot_id); debugging!("Slot id: {}", slot_id);
if let Ok(meta) = metadata(&mut yk, slot_id) { if let Ok(meta) = metadata(&mut yk, slot_id) {
@@ -128,7 +127,7 @@ impl Command for CommandImpl {
} }
if json_output { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) Ok(None)
} }

View File

@@ -2,13 +2,12 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use x509_parser::nom::AsBytes; use x509_parser::nom::AsBytes;
use yubikey::piv::{metadata, sign_data, AlgorithmId, ManagementAlgorithmId}; use yubikey::piv::{metadata, sign_data, AlgorithmId, ManagementAlgorithmId};
use yubikey::YubiKey;
use crate::util::base64_encode; use crate::util::base64_encode;
use crate::{argsutil, pivutil}; use crate::{argsutil, cmdutil, pivutil, util, yubikeyutil};
use crate::digestutil::DigestAlgorithm;
pub struct CommandImpl; pub struct CommandImpl;
@@ -16,32 +15,32 @@ impl Command for CommandImpl {
fn name(&self) -> &str { "piv-ecsign" } fn name(&self) -> &str { "piv-ecsign" }
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV EC sign(with SHA256) subcommand") SubCommand::with_name(self.name()).about("PIV EC sign(with SHA256/SHA384) subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN")) .arg(cmdutil::build_slot_arg())
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN")) .arg(cmdutil::build_pin_arg())
.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_no_pin_arg())
.arg(Arg::with_name("algorithm").short("a").long("algorithm").takes_value(true).help("Algorithm, p256 or p384")) .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("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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let mut json = BTreeMap::<&'_ str, String>::new(); 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 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, digest_algorithm) = match sub_arg_matches.value_of("algorithm") {
let (algorithm, algorithm_str) = match sub_arg_matches.value_of("algorithm") { None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256", DigestAlgorithm::Sha256),
None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256"), Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha384", DigestAlgorithm::Sha384),
Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha256"),
Some(unknown_algorithm) => return simple_error!("Unknown algorithm {}, e.g. p256 or p384", unknown_algorithm), 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 slot_id = pivutil::get_slot_id(slot)?;
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches); 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 { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) Ok(None)
} }

View File

@@ -1,10 +1,9 @@
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use yubikey::{PinPolicy, piv, TouchPolicy};
use yubikey::{PinPolicy, piv, TouchPolicy, YubiKey};
use yubikey::piv::{AlgorithmId, SlotId}; use yubikey::piv::{AlgorithmId, SlotId};
use crate::pinutil; use crate::{cmdutil, pinutil, yubikeyutil};
pub struct CommandImpl; pub struct CommandImpl;
@@ -13,15 +12,13 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV generate subcommand") 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("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 { 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"); warning!("This feature is not works");
let pin_opt = sub_arg_matches.value_of("pin"); let pin_opt = sub_arg_matches.value_of("pin");
let pin_opt = pinutil::get_pin(pin_opt); let pin_opt = pinutil::get_pin(pin_opt);
@@ -32,7 +29,7 @@ impl Command for CommandImpl {
failure_and_exit!("--force must be assigned"); 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: {}"); 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, 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); success!("Generate key success: {:?}", public_key_info);
Ok(None) Ok(None)
} }
} }

View File

@@ -1,15 +1,16 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, ArgMatches, SubCommand};
use p256::pkcs8::der::Encode; use p256::pkcs8::der::Encode;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use rust_util::util_msg;
use rust_util::util_msg::MessageType; use rust_util::util_msg::MessageType;
use x509_parser::parse_x509_certificate; use x509_parser::parse_x509_certificate;
use yubikey::{Key, YubiKey}; use yubikey::Key;
use yubikey::piv::{AlgorithmId, metadata}; 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::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::pkiutil::bytes_to_pem; use crate::pkiutil::bytes_to_pem;
use crate::sshutil::SshVecWriter; use crate::sshutil::SshVecWriter;
@@ -22,19 +23,19 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV meta subcommand") 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(cmdutil::build_slot_arg())
.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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let mut json = BTreeMap::<&'_ str, String>::new(); 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 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)?; let slot_id = pivutil::get_slot_id(slot)?;
json.insert("slot", pivutil::to_slot_hex(&slot_id)); 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(format!("nistp{}", ec_bit_len).as_bytes());
ssh_public_key.write_string(pk_point_hex); ssh_public_key.write_string(pk_point_hex);
let ssh_public_key_str = format!( 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()); 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(); 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 }); 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 { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) Ok(None)
} }

View File

@@ -2,11 +2,10 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use yubikey::piv;
use yubikey::{piv, YubiKey};
use yubikey::piv::{AlgorithmId, SlotId}; use yubikey::piv::{AlgorithmId, SlotId};
use crate::{pinutil, pivutil, rsautil}; use crate::{cmdutil, pinutil, pivutil, rsautil, util, yubikeyutil};
use crate::util::base64_encode; use crate::util::base64_encode;
pub struct CommandImpl; pub struct CommandImpl;
@@ -16,22 +15,22 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV RSA sign(with SHA256) subcommand") 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(cmdutil::build_slot_arg())
.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("no-pin").long("no-pin").help("No PIN")) .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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let pin_opt = pinutil::read_pin(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 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 { if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); 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("hash_hex", hex::encode(&sha256));
json.insert("sign_hex", hex::encode(sign_bytes)); json.insert("sign_hex", hex::encode(sign_bytes));
json.insert("sign_base64", base64_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 { } else {
success!("Signature HEX: {}", hex::encode(sign_bytes)); success!("Signature HEX: {}", hex::encode(sign_bytes));
success!("Signature base64: {}", base64_encode(sign_bytes)); success!("Signature base64: {}", base64_encode(sign_bytes));

View File

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

View File

@@ -2,14 +2,11 @@ use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::rsa::{Padding, Rsa}; use openssl::rsa::{Padding, Rsa};
use rust_util::{util_msg, XResult};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use yubikey::{Key, YubiKey}; use yubikey::piv::AlgorithmId;
use yubikey::piv::{AlgorithmId, SlotId};
use crate::{argsutil, ecdsautil, pivutil}; use crate::{argsutil, cmdutil, ecdsautil, pivutil, util, yubikeyutil};
use crate::ecdsautil::EcdsaAlgorithm; use crate::ecdsautil::EcdsaAlgorithm;
use crate::pivutil::slot_equals;
pub struct CommandImpl; pub struct CommandImpl;
@@ -18,17 +15,17 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV verify subcommand") 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("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("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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let hash_bytes = argsutil::get_sha256_digest_or_hash(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") { 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)?; let slot_id = pivutil::get_slot_id(slot)?;
json.insert("slot", pivutil::to_slot_hex(&slot_id)); 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 certificate = key.certificate();
let tbs_certificate = &certificate.cert.tbs_certificate; let tbs_certificate = &certificate.cert.tbs_certificate;
if let Ok(algorithm_id) = pivutil::get_algorithm_id_by_certificate(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); 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(_) => { Ok(_) => {
success!("Verify ECDSA succeed."); success!("Verify ECDSA succeed.");
if json_output { if json_output {
@@ -100,22 +97,8 @@ impl Command for CommandImpl {
} }
if json_output { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) 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_clap::{Command, CommandError};
use rust_util::util_msg; use rust_util::util_msg;
use rust_util::util_msg::MessageType; use rust_util::util_msg::MessageType;
use crate::{cmdutil, util};
use crate::util::{read_stdin, try_decode}; use crate::util::{read_stdin, try_decode};
pub struct CommandImpl; 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("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(Arg::with_name("padding").long("padding").takes_value(true) .arg(Arg::with_name("padding").long("padding").takes_value(true)
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding")) .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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let pri_key_in = opt_value_result!(sub_arg_matches.value_of("pri-key-in"), "Require private key in"); 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); 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 = crate::rsautil::parse_padding(padding_opt)?;
let padding_str = crate::rsautil::padding_to_string(padding); 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!(Rsa::private_key_from_pem(&pri_key_bytes), "Parse RSA failed: {}");
let keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey 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 HEX: {}", hex::encode(&data));
success!("Message: {}", String::from_utf8_lossy(&data)); success!("Message: {}", String::from_utf8_lossy(&data));
if json_output { if json_output {
let mut json = BTreeMap::new();
json.insert("data", hex::encode(&data)); json.insert("data", hex::encode(&data));
json.insert("padding", padding_str.to_string()); json.insert("padding", padding_str.to_string());
json.insert("encrypted", encrypted_hex); json.insert("encrypted", encrypted_hex);
}
if json_output { util::print_pretty_json(&json);
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} }
Ok(None) Ok(None)

View File

@@ -6,9 +6,8 @@ use openssl::encrypt::Encrypter;
use openssl::pkey::PKey; use openssl::pkey::PKey;
use openssl::rsa::Rsa; use openssl::rsa::Rsa;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use crate::{cmdutil, util};
use crate::digestutil::sha256_bytes;
use crate::digest::sha256_bytes;
pub struct CommandImpl; 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("data-hex").long("data-hex").takes_value(true).help("Data in HEX"))
.arg(Arg::with_name("padding").long("padding").takes_value(true) .arg(Arg::with_name("padding").long("padding").takes_value(true)
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding")) .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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let pub_key_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in"); 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); 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 = crate::rsautil::parse_padding(padding_opt)?;
let padding_str = crate::rsautil::padding_to_string(padding); 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 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_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)); 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); information!("Public key fingerprint: {}", pub_key_fingerprint);
success!("Encrypted message: {}", encrypted_hex); success!("Encrypted message: {}", encrypted_hex);
if json_output { if json_output {
let mut json = BTreeMap::new();
json.insert("data", hex::encode(&data)); json.insert("data", hex::encode(&data));
json.insert("public_key_fingerprint", pub_key_fingerprint); json.insert("public_key_fingerprint", pub_key_fingerprint);
json.insert("padding", padding_str.to_string()); json.insert("padding", padding_str.to_string());
json.insert("encrypted", encrypted_hex); json.insert("encrypted", encrypted_hex);
}
if json_output { util::print_pretty_json(&json);
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} }
Ok(None) 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::MessageType;
use rust_util::{util_msg, XResult}; use rust_util::{util_msg, XResult};
use crate::digest::sha256_bytes; use crate::digestutil::sha256_bytes;
pub struct CommandImpl; pub struct CommandImpl;

View File

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

View File

@@ -1,12 +1,13 @@
use crate::keyutil::{parse_key_uri, KeyUri}; use crate::keyutil::parse_key_uri;
use crate::seutil; use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, ArgMatches, SubCommand};
use p256::elliptic_curve::sec1::FromEncodedPoint; use p256::elliptic_curve::sec1::FromEncodedPoint;
use p256::{EncodedPoint, PublicKey}; use p256::{EncodedPoint, PublicKey};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use spki::EncodePublicKey; use spki::EncodePublicKey;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use rust_util::XResult;
use crate::util::base64_decode;
pub struct CommandImpl; pub struct CommandImpl;
@@ -18,61 +19,27 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()) SubCommand::with_name(self.name())
.about("Secure Enclave ECDH subcommand") .about("Secure Enclave ECDH subcommand")
.arg( .arg(cmdutil::build_key_uri_arg())
Arg::with_name("key") .arg(cmdutil::build_epk_arg())
.long("key") .arg(cmdutil::build_json_arg())
.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"))
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() { let json_output = cmdutil::check_json_output(sub_arg_matches);
return simple_error!("Secure Enclave is NOT supported.");
} seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap(); let key = sub_arg_matches.value_of("key").unwrap();
let epk = sub_arg_matches.value_of("epk").unwrap(); let epk = sub_arg_matches.value_of("epk").unwrap();
let json_output = sub_arg_matches.is_present("json"); let key_uri = parse_key_uri(key)?;
if json_output { let se_key_uri = key_uri.as_secure_enclave_key()?;
util_msg::set_logger_std_out(false);
}
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
debugging!("Secure enclave key URI: {:?}", se_key_uri); debugging!("Secure enclave key URI: {:?}", se_key_uri);
let ephemeral_public_key_der_bytes; let ephemeral_public_key_der_bytes = parse_epk(epk)?;
if epk.starts_with("04") {
let ephemeral_public_key_point_bytes = opt_result!(
hex::decode(epk),
"Decode public key point from hex failed: {}"
);
let encoded_point = opt_result!(
EncodedPoint::from_bytes(ephemeral_public_key_point_bytes),
"Parse public key point failed: {}"
);
let public_key_opt = PublicKey::from_encoded_point(&encoded_point);
if public_key_opt.is_none().into() {
return simple_error!("Parse public key failed.");
}
let public_key = public_key_opt.unwrap();
ephemeral_public_key_der_bytes = public_key.to_public_key_der()?.as_bytes().to_vec();
} else {
ephemeral_public_key_der_bytes =
opt_result!(hex::decode(epk), "Decode public key from hex failed: {}");
}
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let dh = seutil::secure_enclave_p256_dh( let dh = seutil::secure_enclave_p256_dh(
&se_key_uri.private_key, &private_key,
&ephemeral_public_key_der_bytes, &ephemeral_public_key_der_bytes,
)?; )?;
let dh_hex = hex::encode(&dh); let dh_hex = hex::encode(&dh);
@@ -81,7 +48,7 @@ impl Command for CommandImpl {
let mut json = BTreeMap::<&'_ str, String>::new(); let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("shared_secret_hex", dh_hex); json.insert("shared_secret_hex", dh_hex);
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} else { } else {
information!("Shared secret: {}", dh_hex); information!("Shared secret: {}", dh_hex);
} }
@@ -89,3 +56,30 @@ impl Command for CommandImpl {
Ok(None) 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: {}"
);
let encoded_point = opt_result!(
EncodedPoint::from_bytes(ephemeral_public_key_point_bytes),
"Parse public key point failed: {}"
);
let public_key_opt = PublicKey::from_encoded_point(&encoded_point);
if public_key_opt.is_none().into() {
return simple_error!("Parse public key failed.");
}
let public_key = public_key_opt.unwrap();
Ok(public_key.to_public_key_der()?.as_bytes().to_vec())
} else {
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)
}
}
}
}

View File

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

View File

@@ -1,10 +1,13 @@
use crate::cmd_hmac_encrypt;
use crate::pkiutil::bytes_to_pem; use crate::pkiutil::bytes_to_pem;
use crate::seutil;
use crate::util::base64_encode; use crate::util::base64_encode;
use crate::{cmdutil, seutil, util};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use p256::PublicKey;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use spki::DecodePublicKey;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::{ControlFlag, KeyMlKem};
pub struct CommandImpl; pub struct CommandImpl;
@@ -30,50 +33,105 @@ impl Command for CommandImpl {
.takes_value(true) .takes_value(true)
.help("Host name"), .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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() { let json_output = cmdutil::check_json_output(sub_arg_matches);
return simple_error!("Secure Enclave is NOT supported.");
} seutil::check_se_supported()?;
let ty = sub_arg_matches.value_of("type").unwrap(); let ty = sub_arg_matches.value_of("type").unwrap();
let host = sub_arg_matches.value_of("host").unwrap_or("macbook"); 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 { let sign = match ty {
"signing" | "ecsign" | "sign" => true, "signing" | "ecsign" | "sign" => true,
"key_agreement" | "ecdh" | "dh" => false, "key_agreement" | "ecdh" | "dh" => false,
_ => return simple_error!("Invalid type: {}", ty), _ => 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) = let private_key = cmd_hmac_encrypt::do_encrypt(&private_key, &mut None, sub_arg_matches)?;
seutil::generate_secure_enclave_p256_keypair(sign)?; let key_uri = format!(
"key://{}:se/{}:{}:{}",
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:{}:{}",
host, host,
algorithm,
iff!(sign, "signing", "key_agreement"), iff!(sign, "signing", "key_agreement"),
private_key, 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) 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::cmd_se_generate::print_se_key;
use crate::pkiutil::bytes_to_pem; use crate::keyutil::{parse_key_uri, KeyUsage};
use crate::seutil; use crate::{cmd_hmac_decrypt, cmdutil, seutil};
use crate::util::base64_encode; use clap::{App, ArgMatches, SubCommand};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
pub struct CommandImpl; pub struct CommandImpl;
@@ -17,50 +14,27 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()) SubCommand::with_name(self.name())
.about("Secure Enclave recover subcommand") .about("Secure Enclave recover subcommand")
.arg( .arg(cmdutil::build_key_uri_arg())
Arg::with_name("key") .arg(cmdutil::build_json_arg())
.long("key")
.required(true)
.takes_value(true)
.help("Key uri"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
if !seutil::is_support_se() { let json_output = cmdutil::check_json_output(sub_arg_matches);
return simple_error!("Secure Enclave is NOT supported.");
} seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap(); let key = sub_arg_matches.value_of("key").unwrap();
let key_uri = parse_key_uri(key)?;
let json_output = sub_arg_matches.is_present("json"); let se_key_uri = key_uri.as_secure_enclave_key()?;
if json_output {
util_msg::set_logger_std_out(false);
}
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
debugging!("Secure enclave key URI: {:?}", se_key_uri); 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) = let (public_key_point, public_key_der, _private_key) =
seutil::recover_secure_enclave_p256_public_key( seutil::recover_secure_enclave_p256_public_key(
&se_key_uri.private_key, &private_key,
se_key_uri.usage == KeyUsage::Singing, se_key_uri.usage == KeyUsage::Singing,
)?; )?;
let public_key_point_hex = hex::encode(&public_key_point); print_se_key(json_output, &public_key_point, &public_key_der, key);
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);
}
Ok(None) 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,183 +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 yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::{cmd_signjwt, digest, ecdsautil, hmacutil, pivutil, 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::message::{self, Message};
use ssh_agent::proto::public_key::PublicKey; 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::pinutil;
use crate::sshutil::{generate_ssh_string, with_sign}; use crate::sshutil::{generate_ssh_string, with_sign};
@@ -139,10 +139,10 @@ impl Agent for SshAgent {
pub struct CommandImpl; pub struct CommandImpl;
impl Command for 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> { 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("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").long("pgp").help("Use PGP"))
.arg(Arg::with_name("pgp-sign").long("pgp-sign").help("Use PGP sign")) .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 clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use sshcerts::ssh::PublicKeyKind; use sshcerts_hatter_fork::ssh::PublicKeyKind;
use sshcerts::{Certificate, PrivateKey, PublicKey}; use sshcerts_hatter_fork::{Certificate, PrivateKey, PublicKey};
use std::fs; use std::fs;
pub struct CommandImpl; pub struct CommandImpl;

View File

@@ -3,19 +3,19 @@ use ecdsa::elliptic_curve::pkcs8::der::Encode;
use rand::random; use rand::random;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_time, XResult}; use rust_util::{util_time, XResult};
use sshcerts::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner}; use sshcerts_hatter_fork::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner};
use sshcerts::utils::format_signature_for_ssh; use sshcerts_hatter_fork::utils::format_signature_for_ssh;
use sshcerts::x509::extract_ssh_pubkey_from_x509_certificate; use sshcerts_hatter_fork::x509::extract_ssh_pubkey_from_x509_certificate;
use sshcerts::{CertType, Certificate, PublicKey}; use sshcerts_hatter_fork::{CertType, Certificate, PublicKey};
use std::fs; use std::fs;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::SystemTime; use std::time::SystemTime;
use yubikey::piv::{sign_data, AlgorithmId, SlotId}; use yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Key, YubiKey}; 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::pivutil::slot_equals;
use crate::{pinutil, pivutil, util}; use crate::{cmdutil, pinutil, pivutil, util};
pub struct CommandImpl; pub struct CommandImpl;
@@ -27,9 +27,9 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH PIV sign cert subcommand") 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(cmdutil::build_slot_arg())
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN")) .arg(cmdutil::build_pin_arg())
.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_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("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("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")) .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::{Key, YubiKey};
use yubikey::piv::{AlgorithmId, sign_data}; 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::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::sshutil::SshVecWriter; use crate::sshutil::SshVecWriter;
@@ -18,9 +18,9 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH piv sign subcommand") 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(cmdutil::build_slot_arg())
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN")) .arg(cmdutil::build_pin_arg())
.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_no_pin_arg())
.arg(Arg::with_name("namespace").short("n").long("namespace").takes_value(true).help("Namespace")) .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")) .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(namespace.as_bytes());
sign_message.write_string("".as_bytes()); sign_message.write_string("".as_bytes());
sign_message.write_string("sha512".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)); debugging!("Data digest: {} (sha512)", hex::encode(&data_digest));
sign_message.write_string(&data_digest); sign_message.write_string(&data_digest);
debugging!("Singed message: {}", hex::encode(&sign_message)); debugging!("Singed message: {}", hex::encode(&sign_message));
let tobe_signed_data = if ec_bit_len == 256 { let tobe_signed_data = if ec_bit_len == 256 {
crate::digest::sha256_bytes(&sign_message) crate::digestutil::sha256_bytes(&sign_message)
} else { } else {
crate::digest::sha384_bytes(&sign_message) crate::digestutil::sha384_bytes(&sign_message)
}; };
debugging!("Digest of signed message: {}", hex::encode(&tobe_signed_data)); 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::pkey::PKey;
use openssl::sign::Verifier; use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use x509_parser::certificate::X509Certificate; use x509_parser::certificate::X509Certificate;
use x509_parser::prelude::FromDer; use x509_parser::prelude::FromDer;
use crate::digest; use crate::{cmdutil, digestutil, util};
use crate::fido; use crate::fidoutil;
use crate::fido::{U2fRegistrationData, U2fV2Challenge}; use crate::fidoutil::{U2fRegistrationData, U2fV2Challenge};
use crate::util::base64_encode; use crate::util::base64_encode;
pub struct CommandImpl; 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("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").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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { util_msg::set_logger_std_out(false); }
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() { let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64, 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 = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
let u2fv2_challenge_str = u2fv2_challenge.to_json(); let u2fv2_challenge_str = u2fv2_challenge.to_json();
let app_id_hash = digest::sha256(app_id); let app_id_hash = digestutil::sha256(app_id);
let challenge_hash = digest::sha256(&u2fv2_challenge_str); let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
let flags = RegisterFlags::empty(); 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 (register_tx, register_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| { 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); + u2f_registration_data.attestation_cert.as_ref().map(|c| c.len()).unwrap_or(0);
let sign = &register_result.0[sign_prefix_len..]; let sign = &register_result.0[sign_prefix_len..];
let mut json = BTreeMap::new();
if json_output { if json_output {
let mut json = BTreeMap::new();
// println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap()); // println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
if let Some(device_name) = u2f_registration_data.device_name { if let Some(device_name) = u2f_registration_data.device_name {
json.insert("device_name", 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("app_id_hash", hex::encode(&app_id_hash));
json.insert("challenge", u2fv2_challenge_str); json.insert("challenge", u2fv2_challenge_str);
json.insert("challenge_hash", hex::encode(&challenge_hash)); json.insert("challenge_hash", hex::encode(&challenge_hash));
util::print_pretty_json(&json);
} else { } else {
success!("Device info: {}", u2f_registration_data.device_info); success!("Device info: {}", u2f_registration_data.device_info);
information!("Register challenge: {}", u2fv2_challenge_str); information!("Register challenge: {}", u2fv2_challenge_str);
@@ -149,9 +149,6 @@ impl Command for CommandImpl {
warning!("Cannot find attestation cert!"); warning!("Cannot find attestation cert!");
} }
} }
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None) Ok(None)
} }
} }

View File

@@ -13,9 +13,9 @@ use openssl::pkey::PKey;
use openssl::sign::Verifier; use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use crate::digest; use crate::{cmdutil, digestutil, util};
use crate::fido; use crate::fidoutil;
use crate::fido::U2fV2Challenge; use crate::fidoutil::U2fV2Challenge;
use crate::util::base64_encode; use crate::util::base64_encode;
pub struct CommandImpl; 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").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("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("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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() { let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64, 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 = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
let u2fv2_challenge_str = u2fv2_challenge.to_json(); let u2fv2_challenge_str = u2fv2_challenge.to_json();
let app_id_hash = digest::sha256(app_id); let app_id_hash = digestutil::sha256(app_id);
let challenge_hash = digest::sha256(&u2fv2_challenge_str); 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); information!("App id: {}, Start sign...", app_id);
debugging!("Wait timeout: {} ms", timeout_ms); debugging!("Wait timeout: {} ms", timeout_ms);
@@ -163,7 +162,7 @@ impl Command for CommandImpl {
} }
} }
if json_output { if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap()); util::print_pretty_json(&json);
} }
Ok(None) 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 sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512}; 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> { pub fn sha256(input: &str) -> Vec<u8> {
sha256_bytes(input.as_bytes()) sha256_bytes(input.as_bytes())
} }

View File

@@ -2,8 +2,7 @@
macro_rules! piv_ecdh { macro_rules! piv_ecdh {
($p_algo: tt, $public_key_pem_opt: expr, $sub_arg_matches: expr, $json: expr, $json_output: expr) => ({ ($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::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use $p_algo::ecdh::EphemeralSecret; use $p_algo::{EncodedPoint, PublicKey, ecdh::EphemeralSecret};
use $p_algo::{EncodedPoint, PublicKey};
let public_key; let public_key;
if let Some(public_key_pem) = $public_key_pem_opt { if let Some(public_key_pem) = $public_key_pem_opt {
public_key = opt_result!(public_key_pem.parse::<PublicKey>(), "Parse public key failed: {}"); 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; pub(crate) use piv_ecdh;

View File

@@ -5,6 +5,7 @@ use p256::NistP256;
use p256::ecdsa::signature::hazmat::PrehashVerifier; use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p384::NistP384; use p384::NistP384;
use p256::pkcs8::EncodePrivateKey; use p256::pkcs8::EncodePrivateKey;
use p521::NistP521;
use rust_util::XResult; use rust_util::XResult;
use spki::EncodePublicKey; use spki::EncodePublicKey;
use crate::util::{base64_encode, try_decode}; use crate::util::{base64_encode, try_decode};
@@ -13,6 +14,12 @@ use crate::util::{base64_encode, try_decode};
pub enum EcdsaAlgorithm { pub enum EcdsaAlgorithm {
P256, P256,
P384, P384,
P521,
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum EcdsaSignType {
Der, Rs,
} }
pub fn parse_ecdsa_to_rs(signature_der: &[u8]) -> XResult<Vec<u8>> { pub fn parse_ecdsa_to_rs(signature_der: &[u8]) -> XResult<Vec<u8>> {
@@ -22,30 +29,22 @@ pub fn parse_ecdsa_to_rs(signature_der: &[u8]) -> XResult<Vec<u8>> {
} }
pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)> { pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)> {
let mut vec_r: Vec<u8> = Vec::new(); let vec_r: Vec<u8>;
let mut vec_s: Vec<u8> = Vec::new(); let vec_s: Vec<u8>;
let (_, parsed_signature) = opt_result!(der_parser::parse_der(signature_der), "Parse signature failed: {}"); let (_, parsed_signature) = opt_result!(der_parser::parse_der(signature_der), "Parse signature failed: {}");
match parsed_signature.content { match parsed_signature.content {
BerObjectContent::Sequence(seq) => { BerObjectContent::Sequence(seq) => {
match &seq[0].content { match &seq[0].content {
BerObjectContent::Integer(r) => { BerObjectContent::Integer(r) => {
debugging!("Signature r: {}", hex::encode(r)); debugging!("Signature r: {}", hex::encode(r));
if r.len() == ((256 / 8) + 1) || r.len() == ((384 / 8) + 1) { vec_r = trim_ecdsa_point_coord(r);
vec_r = r[1..].to_vec();
} else {
vec_r = r.to_vec();
}
} }
_ => return simple_error!("Parse signature failed: [0]not integer"), _ => return simple_error!("Parse signature failed: [0]not integer"),
} }
match &seq[1].content { match &seq[1].content {
BerObjectContent::Integer(s) => { BerObjectContent::Integer(s) => {
debugging!("Signature s: {}", hex::encode(s)); debugging!("Signature s: {}", hex::encode(s));
if s.len() == ((256 / 8) + 1) || s.len() == ((384 / 8) + 1) { vec_s = trim_ecdsa_point_coord(s);
vec_s = s[1..].to_vec();
} else {
vec_s = s.to_vec();
}
} }
_ => return simple_error!("Parse signature failed: [1]not integer"), _ => return simple_error!("Parse signature failed: [1]not integer"),
} }
@@ -55,27 +54,112 @@ pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)>
Ok((vec_r, vec_s)) Ok((vec_r, vec_s))
} }
pub fn generate_p256_keypair() -> XResult<(String, String, String)> { const P256_LEN: usize = 32;
let secret_key = p256::SecretKey::random(&mut rand::thread_rng()); const P384_LEN: usize = 48;
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes()); const P521_LEN: usize = 66;
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)?; fn trim_ecdsa_point_coord(p: &[u8]) -> Vec<u8> {
Ok((secret_key_der_base64, secret_key_pem, public_key_pem)) 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 {
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes()); ($algo: tt) => ({
let secret_key_pem = secret_key.to_pkcs8_pem(LineEnding::LF)?.to_string(); use $algo::SecretKey;
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 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)?;
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 { macro_rules! parse_ecdsa_private_key {
($algo: tt, $parse_ecdsa_private_key: tt) => ({ ($algo: tt, $parse_ecdsa_private_key: tt) => ({
use $algo::ecdsa::{SigningKey}; use $algo::{SecretKey, pkcs8::DecodePrivateKey};
use $algo::pkcs8::DecodePrivateKey;
use $algo::SecretKey;
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) { let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
Ok(secret_key) => secret_key, Ok(secret_key) => secret_key,
@@ -99,26 +183,36 @@ pub fn parse_p384_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key!(p384, private_key_pkcs8) parse_ecdsa_private_key!(p384, private_key_pkcs8)
} }
pub fn sign_p256_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult<Vec<u8>> { pub fn parse_p521_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
use p256::ecdsa::{SigningKey, Signature}; parse_ecdsa_private_key!(p521, private_key_pkcs8)
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 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)?; macro_rules! sign_ecdsa_rs_or_der {
let signature: Signature = signing_key.sign_prehash(pre_hash)?; ($algo: tt, $private_key_d: tt, $pre_hash: tt, $is_rs: tt) => ({
use $algo::ecdsa::{SigningKey, Signature, signature::hazmat::PrehashSigner};
Ok(signature.to_bytes().to_vec()) 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 { macro_rules! ecdsa_verify_signature {
($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({ ($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({
use ecdsa::Signature; use ecdsa::Signature;
@@ -134,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 { match algo {
EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature), EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature),
EcdsaAlgorithm::P384 => ecdsa_verify_signature!(NistP384, 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(()) 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 std::time::SystemTime;
use authenticator::{RegisterResult, StatusUpdate}; use authenticator::{RegisterResult, StatusUpdate};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rand::Rng; use rand::Rng;
use rust_util::XResult; use rust_util::XResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::pkiutil::bytes_to_pem; 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct U2FDeviceInfo { pub struct U2FDeviceInfo {
@@ -103,7 +101,7 @@ impl U2fV2Challenge {
None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix), None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix),
Some(challenge_hex) => { Some(challenge_hex) => {
let challenge_bytes = opt_result!(hex::decode(challenge_hex), "Decode challenge hex failed: {}"); 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) U2fV2Challenge::new(challenge, app_id)
} }
}) })
@@ -122,7 +120,7 @@ impl U2fV2Challenge {
rand_bytes[..8].clone_from_slice(&timestamp_be_bytes[..8]); 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) 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::hmacmode::HmacKey;
use yubico_manager::sec::hmac_sha1; use yubico_manager::sec::hmac_sha1;
use yubico_manager::Yubico; use yubico_manager::Yubico;
use crate::digest::{copy_sha256, sha256, sha256_bytes}; use crate::digestutil::{copy_sha256, sha256_bytes};
use crate::util::{base64_decode, base64_encode}; 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:"; 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> { 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 aes_gcm_nonce: [u8; 16] = random();
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?; let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
@@ -33,8 +34,8 @@ pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
Ok(format!("{}{}:{}:{}", Ok(format!("{}{}:{}:{}",
HMAC_ENC_PREFIX, HMAC_ENC_PREFIX,
hex::encode(&hmac_nonce), base64_encode_url_safe_no_pad(hmac_nonce),
hex::encode(&aes_gcm_nonce), base64_encode_url_safe_no_pad(aes_gcm_nonce),
base64_encode(&ciphertext) base64_encode(&ciphertext)
)) ))
} }
@@ -43,14 +44,6 @@ pub fn is_hmac_encrypted(ciphertext: &str) -> bool {
ciphertext.starts_with(HMAC_ENC_PREFIX) 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> { pub fn hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
let plaintext = hmac_decrypt(ciphertext)?; let plaintext = hmac_decrypt(ciphertext)?;
Ok(String::from_utf8(plaintext)?) Ok(String::from_utf8(plaintext)?)
@@ -61,8 +54,8 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
return simple_error!("Invalid ciphertext: {}", ciphertext); return simple_error!("Invalid ciphertext: {}", ciphertext);
} }
let parts = ciphertext.split(":").collect::<Vec<_>>(); let parts = ciphertext.split(":").collect::<Vec<_>>();
let hmac_nonce = hex::decode(parts[1])?; let hmac_nonce = try_decode_hmac_val(parts[1])?;
let aes_gcm_nonce = hex::decode(parts[2])?; let aes_gcm_nonce = try_decode_hmac_val(parts[2])?;
let ciphertext = base64_decode(parts[3])?; let ciphertext = base64_decode(parts[3])?;
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?; let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
@@ -76,6 +69,16 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
Ok(plaintext) 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>> { pub fn compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
let mut yubi = Yubico::new(); let mut yubi = Yubico::new();
let device = match yubi.find_yubikey() { let device = match yubi.find_yubikey() {
@@ -92,7 +95,7 @@ pub fn compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
.set_mode(Mode::Sha1) .set_mode(Mode::Sha1)
.set_slot(Slot::Slot2); .set_slot(Slot::Slot2);
let hmac_result = opt_result!(yubi.challenge_response_hmac(&challenge_bytes, config), "Challenge HMAC failed: {}"); let hmac_result = opt_result!(yubi.challenge_response_hmac(challenge_bytes, config), "Challenge HMAC failed: {}");
Ok(hmac_result.deref().to_vec()) Ok(hmac_result.deref().to_vec())
} }
@@ -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 sha512_output = sub_arg_matches.is_present("sha512");
let hex_string = hex::encode(result); let hex_string = hex::encode(result);
let hex_sha1 = iff!(sha1_output, Some(crate::digest::sha1_bytes(result)), None); let hex_sha1 = iff!(sha1_output, Some(crate::digestutil::sha1_bytes(result)), None);
let hex_sha256 = iff!(sha256_output, Some(crate::digest::sha256_bytes(result)), None); let hex_sha256 = iff!(sha256_output, Some(crate::digestutil::sha256_bytes(result)), None);
let hex_sha384 = iff!(sha384_output, Some(crate::digest::sha384_bytes(result)), None); let hex_sha384 = iff!(sha384_output, Some(crate::digestutil::sha384_bytes(result)), None);
let hex_sha512 = iff!(sha512_output, Some(crate::digest::sha512_bytes(result)), None); let hex_sha512 = iff!(sha512_output, Some(crate::digestutil::sha512_bytes(result)), None);
if json_output { if json_output {
let mut json = BTreeMap::<&'_ str, String>::new(); 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_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))); 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 { } else {
success!("Challenge HEX: {}", hex::encode(challenge_bytes)); success!("Challenge HEX: {}", hex::encode(challenge_bytes));
success!("Response HEX: {}", hex_string); 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 regex::Regex;
use rust_util::XResult; use rust_util::XResult;
use yubikey::piv::{AlgorithmId, SlotId};
// reference: https://git.hatter.ink/hatter/card-cli/issues/6 // reference: https://git.hatter.ink/hatter/card-cli/issues/6
#[derive(Debug)] #[derive(Debug)]
pub enum KeyUri { pub enum KeyUri {
SecureEnclaveKey(SecureEnclaveKey), SecureEnclave(SecureEnclaveKey),
YubikeyPiv(YubikeyPivKey),
YubikeyHmacEncSoft(YubikeyHmacEncSoftKey),
ExternalCommand(ExternalCommandKey),
} }
// #[derive(Debug, PartialEq, Eq)] impl KeyUri {
// pub enum KeyModule { pub fn as_secure_enclave_key(&self) -> XResult<&SecureEnclaveKey> {
// SecureEnclave, match self {
// OpenPgpCard, KeyUri::SecureEnclave(key) => Ok(key),
// PersonalIdentityVerification, _ => simple_error!("Not a secure enclave key."),
// } }
// }
// impl KeyModule {
// pub fn from(module: &str) -> Option<Self> { pub fn get_preferred_algorithm_type(&self) -> AlgorithmType {
// match module { let algorithm_id = match &self {
// "se" => Some(Self::SecureEnclave), KeyUri::SecureEnclave(_) => return AlgorithmType::Es256,
// "pgp" => Some(Self::OpenPgpCard), KeyUri::YubikeyPiv(key) => key.algorithm,
// "piv" => Some(Self::PersonalIdentityVerification), KeyUri::YubikeyHmacEncSoft(key) => key.algorithm,
// _ => None, 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)] #[derive(Debug, PartialEq, Eq)]
pub enum KeyUsage { 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)] #[derive(Debug)]
pub struct SecureEnclaveKey { pub struct SecureEnclaveKey {
pub host: String, pub host: String,
@@ -51,64 +266,180 @@ pub struct SecureEnclaveKey {
pub private_key: String, 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> { 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) { let captures = match regex.captures(key_uri) {
None => return simple_error!("Invalid key uri: {}", key_uri), None => return simple_error!("Invalid key uri: {}", key_uri),
Some(captures) => captures, 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 module = captures.get(2).unwrap().as_str();
let algorithm = captures.get(3).unwrap().as_str(); let algorithm = captures.get(3).unwrap().as_str();
let usage = captures.get(4).unwrap().as_str(); let usage = captures.get(4).unwrap().as_str();
let left_part = captures.get(5).unwrap().as_str(); let left_part = captures.get(5).unwrap().as_str();
if "se" != module { match module {
return simple_error!("Key uri's module must be se."); "se" => {
} if "p256" != algorithm {
if "p256" != algorithm { return simple_error!("Key uri's algorithm must be p256.");
return simple_error!("Key uri's algorithm must be p256."); }
} let key_usage = match KeyUsage::from(usage) {
let key_usage = match KeyUsage::from(usage) { None | Some(KeyUsage::Any) => {
None | Some(KeyUsage::Any) => { return simple_error!("Key uri's usage must be signing or key_agreement.")
return simple_error!("Key uri's usage must be signing or key_agreement.") }
Some(key_usage) => key_usage,
};
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)
} }
Some(key_usage) => key_usage, "piv" => {
}; if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
let parsed_key_uri = KeyUri::SecureEnclaveKey(SecureEnclaveKey { }
host: host.to_string(), let algorithm = opt_value_result!(
usage: key_usage, KeyAlgorithmId::from_str(algorithm),
private_key: left_part.to_string(), "Invalid algorithm id: {}",
}); algorithm
);
debugging!("Parsed key uri: {:?}", parsed_key_uri); let slot = opt_value_result!(
Ok(parsed_key_uri) 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] #[test]
fn test_parse_key_uri_01() { fn test_parse_key_uri_01() {
let se_key_uri = let se_key_uri =
parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap(); 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 { 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!("hatter-mac-pro", se_key_uri.host);
assert_eq!(KeyUsage::Singing, se_key_uri.usage); assert_eq!(KeyUsage::Singing, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key); assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
} }
_ => {
panic!("Key uri not parsed")
}
} }
} }
#[test] #[test]
fn test_parse_key_uri_02() { fn test_parse_key_uri_02() {
let se_key_uri = 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(); .unwrap();
assert_eq!(
"key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)",
se_key_uri.to_string()
);
match se_key_uri { 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!("hatter-mac-m1", se_key_uri.host);
assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage); assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key); 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,73 +6,90 @@ use rust_util::util_clap::{Command, CommandError};
mod argsutil; mod argsutil;
mod cmd_chall; mod cmd_chall;
mod cmd_challconfig; mod cmd_chall_config;
mod cmd_ecverify; 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_hmac_sha1;
mod cmd_hmacencrypt; mod cmd_keypair_generate;
mod cmd_hmacdecrypt; mod cmd_keypair_keychain_export;
mod cmd_keypair_keychain_import;
mod cmd_list; mod cmd_list;
mod cmd_parseecdsasignature;
#[cfg(feature = "with-sequoia-openpgp")] #[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgp; mod cmd_pgp;
mod cmd_pgpageaddress; mod cmd_pgp_age_address;
mod cmd_pgpcardadmin; mod cmd_pgp_card_admin;
mod cmd_pgpcarddecrypt; mod cmd_pgp_card_decrypt;
mod cmd_pgpcardlist; mod cmd_pgp_card_list;
#[cfg(feature = "with-sequoia-openpgp")] #[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgpcardmake; mod cmd_pgp_card_make;
mod cmd_pgpcardsign; mod cmd_pgp_card_sign;
mod cmd_piv; mod cmd_piv;
mod cmd_pivdecrypt; mod cmd_piv_decrypt;
mod cmd_pivecdh; mod cmd_piv_ecdh;
mod cmd_pivecsign; mod cmd_piv_ecsign;
mod cmd_pivgenerate; mod cmd_piv_generate;
mod cmd_pivmeta; mod cmd_piv_meta;
mod cmd_pivrsasign; mod cmd_piv_rsasign;
mod cmd_pivsummary; mod cmd_piv_summary;
mod cmd_pivverify; mod cmd_piv_verify;
mod cmd_rsadecrypt; mod cmd_rsa_decrypt;
mod cmd_rsaencrypt; mod cmd_rsa_encrypt;
mod cmd_rsaverify; mod cmd_rsa_verify;
#[cfg(feature = "with-secure-enclave")]
mod cmd_se; mod cmd_se;
#[cfg(feature = "with-secure-enclave")]
mod cmd_se_ecdh; mod cmd_se_ecdh;
#[cfg(feature = "with-secure-enclave")]
mod cmd_se_ecsign; mod cmd_se_ecsign;
#[cfg(feature = "with-secure-enclave")]
mod cmd_se_generate; mod cmd_se_generate;
#[cfg(feature = "with-secure-enclave")]
mod cmd_se_recover; mod cmd_se_recover;
mod cmd_signfile; mod cmd_sign_jwt;
mod cmd_signjwt; mod cmd_sign_jwt_piv;
mod cmd_signjwtsoft; mod cmd_sign_jwt_se;
mod cmd_sshagent; mod cmd_sign_jwt_soft;
mod cmd_sshparse; mod cmd_ssh_agent;
mod cmd_sshparsesign; mod cmd_ssh_agent_gpg;
mod cmd_sshpivcert; mod cmd_ssh_parse;
mod cmd_sshpivsign; mod cmd_ssh_parse_sign;
mod cmd_sshpubkey; mod cmd_ssh_piv_cert;
mod cmd_u2fregister; mod cmd_ssh_piv_sign;
mod cmd_u2fsign; mod cmd_ssh_pub_key;
mod cmd_verifyfile; mod cmd_u2f_register;
mod cmd_parseecdsasignature; mod cmd_u2f_sign;
mod cmd_generatekeypair; mod cmdutil;
mod digest; mod digestutil;
mod ecdhutil; mod ecdhutil;
mod ecdsautil; mod ecdsautil;
mod fido; mod ecutil;
mod fidoutil;
mod hmacutil; mod hmacutil;
mod keychain;
mod keyutil; mod keyutil;
mod pbeutil;
mod pgpcardutil; mod pgpcardutil;
mod pinutil; mod pinutil;
mod pivutil; mod pivutil;
mod pkiutil; mod pkiutil;
mod rsautil; mod rsautil;
#[cfg(feature = "with-secure-enclave")]
mod seutil; mod seutil;
mod signfile; mod signfile;
mod sshutil; mod sshutil;
mod util; 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; pub struct DefaultCommandImpl;
@@ -103,55 +120,62 @@ fn inner_main() -> CommandError {
Box::new(cmd_list::CommandImpl), Box::new(cmd_list::CommandImpl),
Box::new(cmd_chall::CommandImpl), Box::new(cmd_chall::CommandImpl),
Box::new(cmd_hmac_sha1::CommandImpl), Box::new(cmd_hmac_sha1::CommandImpl),
Box::new(cmd_hmacencrypt::CommandImpl), Box::new(cmd_hmac_encrypt::CommandImpl),
Box::new(cmd_hmacdecrypt::CommandImpl), Box::new(cmd_hmac_decrypt::CommandImpl),
Box::new(cmd_challconfig::CommandImpl), Box::new(cmd_chall_config::CommandImpl),
Box::new(cmd_rsaencrypt::CommandImpl), Box::new(cmd_rsa_encrypt::CommandImpl),
Box::new(cmd_rsadecrypt::CommandImpl), Box::new(cmd_rsa_decrypt::CommandImpl),
Box::new(cmd_rsaverify::CommandImpl), Box::new(cmd_rsa_verify::CommandImpl),
#[cfg(feature = "with-sequoia-openpgp")] #[cfg(feature = "with-sequoia-openpgp")]
Box::new(cmd_pgp::CommandImpl), Box::new(cmd_pgp::CommandImpl),
Box::new(cmd_pgpcardadmin::CommandImpl), Box::new(cmd_pgp_card_admin::CommandImpl),
Box::new(cmd_pgpcardlist::CommandImpl), Box::new(cmd_pgp_card_list::CommandImpl),
Box::new(cmd_pgpcardsign::CommandImpl), Box::new(cmd_pgp_card_sign::CommandImpl),
Box::new(cmd_pgpcarddecrypt::CommandImpl), Box::new(cmd_pgp_card_decrypt::CommandImpl),
#[cfg(feature = "with-sequoia-openpgp")] #[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_piv::CommandImpl),
Box::new(cmd_pivsummary::CommandImpl), Box::new(cmd_piv_summary::CommandImpl),
Box::new(cmd_pivmeta::CommandImpl), Box::new(cmd_piv_meta::CommandImpl),
Box::new(cmd_pivverify::CommandImpl), Box::new(cmd_piv_verify::CommandImpl),
Box::new(cmd_pivrsasign::CommandImpl), Box::new(cmd_piv_rsasign::CommandImpl),
Box::new(cmd_pivecdh::CommandImpl), Box::new(cmd_piv_ecdh::CommandImpl),
Box::new(cmd_pivecsign::CommandImpl), Box::new(cmd_piv_ecsign::CommandImpl),
Box::new(cmd_pivdecrypt::CommandImpl), Box::new(cmd_piv_decrypt::CommandImpl),
Box::new(cmd_pivgenerate::CommandImpl), Box::new(cmd_piv_generate::CommandImpl),
Box::new(cmd_u2fregister::CommandImpl), Box::new(cmd_u2f_register::CommandImpl),
Box::new(cmd_u2fsign::CommandImpl), Box::new(cmd_u2f_sign::CommandImpl),
Box::new(cmd_sshagent::CommandImpl), Box::new(cmd_ssh_agent::CommandImpl),
Box::new(cmd_sshparsesign::CommandImpl), Box::new(cmd_ssh_agent_gpg::CommandImpl),
Box::new(cmd_sshpivsign::CommandImpl), Box::new(cmd_ssh_parse_sign::CommandImpl),
Box::new(cmd_sshpivcert::CommandImpl), Box::new(cmd_ssh_piv_sign::CommandImpl),
Box::new(cmd_sshpubkey::CommandImpl), Box::new(cmd_ssh_piv_cert::CommandImpl),
Box::new(cmd_sshparse::CommandImpl), Box::new(cmd_ssh_pub_key::CommandImpl),
Box::new(cmd_pgpageaddress::CommandImpl), Box::new(cmd_ssh_parse::CommandImpl),
Box::new(cmd_signjwt::CommandImpl), Box::new(cmd_pgp_age_address::CommandImpl),
Box::new(cmd_signjwtsoft::CommandImpl), Box::new(cmd_sign_jwt_piv::CommandImpl),
Box::new(cmd_signfile::CommandImpl), Box::new(cmd_sign_jwt_soft::CommandImpl),
Box::new(cmd_verifyfile::CommandImpl), Box::new(cmd_sign_jwt_se::CommandImpl),
#[cfg(feature = "with-secure-enclave")] 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::CommandImpl),
#[cfg(feature = "with-secure-enclave")]
Box::new(cmd_se_generate::CommandImpl), Box::new(cmd_se_generate::CommandImpl),
#[cfg(feature = "with-secure-enclave")]
Box::new(cmd_se_recover::CommandImpl), Box::new(cmd_se_recover::CommandImpl),
#[cfg(feature = "with-secure-enclave")]
Box::new(cmd_se_ecsign::CommandImpl), Box::new(cmd_se_ecsign::CommandImpl),
#[cfg(feature = "with-secure-enclave")]
Box::new(cmd_se_ecdh::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_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)] #[allow(clippy::vec_init_then_push)]
@@ -159,8 +183,6 @@ fn inner_main() -> CommandError {
let mut features: Vec<&str> = vec![]; let mut features: Vec<&str> = vec![];
#[cfg(feature = "with-sequoia-openpgp")] #[cfg(feature = "with-sequoia-openpgp")]
features.push("sequoia-openpgp"); features.push("sequoia-openpgp");
#[cfg(feature = "with-secure-enclave")]
features.push("secure-enclave");
features features
}; };
let about = format!( let about = format!(

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 clap::ArgMatches;
use jwt::AlgorithmType;
use rust_util::XResult; use rust_util::XResult;
use spki::{ObjectIdentifier, SubjectPublicKeyInfoOwned}; use spki::{ObjectIdentifier, SubjectPublicKeyInfoOwned};
use spki::der::{Decode, Encode}; use spki::der::{Decode, Encode};
@@ -56,6 +57,32 @@ pub trait ToStr {
fn to_str(&self) -> &str; 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 { impl ToStr for PinPolicy {
fn to_str(&self) -> &str { fn to_str(&self) -> &str {
match self { 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 { impl ToStr for AlgorithmId {
fn to_str(&self) -> &str { fn to_str(&self) -> &str {
match self { match self {
@@ -169,6 +211,7 @@ pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
"9c" | "sign" | "signature" => SlotId::Signature, "9c" | "sign" | "signature" => SlotId::Signature,
"9d" | "keym" | "keymanagement" => SlotId::KeyManagement, "9d" | "keym" | "keymanagement" => SlotId::KeyManagement,
"9e" | "card" | "cardauthentication" => SlotId::CardAuthentication, "9e" | "card" | "cardauthentication" => SlotId::CardAuthentication,
"f9" | "attest" | "attestation" => SlotId::Attestation,
"r1" | "82" => SlotId::Retired(RetiredSlotId::R1), "r1" | "82" => SlotId::Retired(RetiredSlotId::R1),
"r2" | "83" => SlotId::Retired(RetiredSlotId::R2), "r2" | "83" => SlotId::Retired(RetiredSlotId::R2),
"r3" | "84" => SlotId::Retired(RetiredSlotId::R3), "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> { pub fn check_read_pin(yk: &mut YubiKey, slot_id: SlotId, sub_arg_matches: &ArgMatches) -> Option<String> {
if never_use_pin(yk, slot_id) { if never_use_pin(yk, slot_id) {
None None

View File

@@ -7,7 +7,7 @@ use rust_util::XResult;
use sequoia_openpgp::crypto::mpi::PublicKey; use sequoia_openpgp::crypto::mpi::PublicKey;
use x509_parser::x509::AlgorithmIdentifier; use x509_parser::x509::AlgorithmIdentifier;
use crate::digest::sha256_bytes; use crate::digestutil::sha256_bytes;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum PkiAlgorithm { 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::bn::{BigNum, BigNumContext};
use openssl::pkey::PKey; use openssl::pkey::PKey;
use openssl::rsa::{Padding, Rsa}; use openssl::rsa::{Padding, Rsa};
use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
use rust_util::{util_msg, XResult}; use rust_util::{util_msg, XResult};
use rust_util::util_msg::MessageType; 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)] #[derive(Debug)]
pub struct RsaCrt { 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); output.extend_from_slice(bs);
Ok(output) 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,113 +1,81 @@
use crate::util::{base64_decode, base64_encode}; use crate::util::{base64_decode, base64_encode};
use rust_util::XResult; use rust_util::XResult;
use swift_rs::swift; use se_tool::KeyPurpose;
use swift_rs::{Bool, SRString}; use swift_secure_enclave_tool_rs as se_tool;
use swift_secure_enclave_tool_rs::{ControlFlag, DigestType, KeyMlKem};
swift!(fn is_support_secure_enclave() -> Bool);
swift!(fn generate_secure_enclave_p256_ecdh_keypair() -> SRString);
swift!(fn generate_secure_enclave_p256_ecsign_keypair() -> SRString);
swift!(fn compute_secure_enclave_p256_ecdh(private_key_base64: SRString, ephemera_public_key_base64: SRString) -> SRString);
swift!(fn compute_secure_enclave_p256_ecsign(private_key_base64: SRString, content: SRString) -> SRString);
swift!(fn recover_secure_enclave_p256_ecsign_public_key(private_key_base64: SRString) -> SRString);
swift!(fn recover_secure_enclave_p256_ecdh_public_key(private_key_base64: SRString) -> SRString);
pub fn is_support_se() -> bool { pub fn is_support_se() -> bool {
unsafe { is_support_secure_enclave() } 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)> { pub fn check_se_supported() -> XResult<()> {
let p256_keypair_result = if sign { if !is_support_se() {
unsafe { generate_secure_enclave_p256_ecsign_keypair() } simple_error!("Secure Enclave is NOT supported.")
} else { } else {
unsafe { generate_secure_enclave_p256_ecdh_keypair() } 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)?
}; };
parse_p256_keypair_result(p256_keypair_result.as_str()) Ok((
key_material.public_key_point,
key_material.public_key_der,
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),
))
} }
pub fn recover_secure_enclave_p256_public_key( pub fn recover_secure_enclave_p256_public_key(
private_key: &str, private_key: &str,
sign: bool, sign: bool,
) -> XResult<(Vec<u8>, Vec<u8>, String)> { ) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let p256_keypair_result = if sign { let private_key_representation = base64_decode(private_key)?;
unsafe { recover_secure_enclave_p256_ecsign_public_key(SRString::from(private_key)) } let key_material = if sign {
se_tool::recover_keypair(KeyPurpose::Signing, &private_key_representation)
} else { } else {
unsafe { recover_secure_enclave_p256_ecdh_public_key(SRString::from(private_key)) } se_tool::recover_keypair(KeyPurpose::KeyAgreement, &private_key_representation)
}; }?;
parse_p256_keypair_result(p256_keypair_result.as_str()) Ok((
key_material.public_key_point,
key_material.public_key_der,
base64_encode(&key_material.private_key_representation),
))
} }
pub fn secure_enclave_p256_dh( pub fn secure_enclave_p256_dh(
private_key: &str, private_key: &str,
ephemeral_public_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8],
) -> XResult<Vec<u8>> { ) -> XResult<Vec<u8>> {
let dh_result = unsafe { let private_key_representation = base64_decode(private_key)?;
compute_secure_enclave_p256_ecdh( let shared_secret =
SRString::from(private_key), se_tool::private_key_ecdh(&private_key_representation, ephemeral_public_key_bytes)?;
SRString::from(base64_encode(ephemeral_public_key_bytes).as_str()), Ok(shared_secret)
)
};
let dh_result_str = dh_result.as_str();
debugging!("DH result: {}", &dh_result_str);
if !dh_result_str.starts_with("ok:SharedSecret:") {
return simple_error!("ECDH P256 in secure enclave failed: {}", dh_result_str);
}
let shared_secret_hex = dh_result_str
.chars()
.skip("ok:SharedSecret:".len())
.collect::<String>();
let shared_secret_hex = shared_secret_hex.trim();
Ok(opt_result!(
hex::decode(shared_secret_hex),
"Decrypt shared secret hex: {}, failed: {}",
shared_secret_hex
))
} }
pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8]) -> XResult<Vec<u8>> { pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8], digest_type: DigestType) -> XResult<Vec<u8>> {
let signature_result = unsafe { let private_key_representation = base64_decode(private_key)?;
compute_secure_enclave_p256_ecsign( let signature = se_tool::private_key_sign_digested(&private_key_representation, content, digest_type)?;
SRString::from(private_key), Ok(signature)
SRString::from(base64_encode(content).as_str()), }
)
};
let signature_result_str = signature_result.as_str();
debugging!("Signature result: {}", &signature_result_str);
if !signature_result_str.starts_with("ok:") {
return simple_error!(
"Sign P256 in secure enclave failed: {}",
signature_result_str
);
}
let signature = signature_result_str.chars().skip(3).collect::<String>();
debugging!("Signature: {}", &signature);
Ok(base64_decode(&signature)?)
}
fn parse_p256_keypair_result(p256_keypair_result_str: &str) -> XResult<(Vec<u8>, Vec<u8>, String)> {
if !p256_keypair_result_str.starts_with("ok:") {
return simple_error!(
"Generate P256 in secure enclave failed: {}",
p256_keypair_result_str
);
}
let public_key_and_private_key = p256_keypair_result_str.chars().skip(3).collect::<String>();
let public_key_and_private_keys = public_key_and_private_key.split(',').collect::<Vec<_>>();
if public_key_and_private_keys.len() != 3 {
return simple_error!(
"Generate P256 in secure enclave result is bad: {}",
public_key_and_private_key
);
}
let public_key_point = opt_result!(
base64_decode(public_key_and_private_keys[0]),
"Public key point is not base64 encoded: {}"
);
let public_key_der = opt_result!(
base64_decode(public_key_and_private_keys[1]),
"Public key der is not base64 encoded: {}"
);
let private_key = public_key_and_private_keys[2].to_string();
Ok((public_key_point, public_key_der, private_key))
}

View File

@@ -1,9 +1,10 @@
use std::fs; use std::fs;
use std::io::Read; use std::io::Read;
use base64::{DecodeError, Engine};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::{DecodeError, Engine};
use rust_util::XResult; use rust_util::XResult;
use serde::Serialize;
pub fn base64_encode<T: AsRef<[u8]>>(input: T) -> String { pub fn base64_encode<T: AsRef<[u8]>>(input: T) -> String {
STANDARD.encode(input) 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) 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>> { pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
match hex::decode(input) { match hex::decode(input) {
Ok(v) => Ok(v), Ok(v) => Ok(v),
@@ -30,7 +46,7 @@ pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
Ok(v) => Ok(v), Ok(v) => Ok(v),
Err(e) => simple_error!("decode hex or base64 error: {}", e), 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 == "-" { if file == "-" {
read_stdin() read_stdin()
} else { } 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
}

View File

@@ -1,30 +0,0 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "swift-lib",
platforms: [
.macOS(.v11), // macOS Catalina. Earliest version that is officially supported by Apple.
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "swift-lib",
type: .static,
targets: ["swift-lib"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(name: "SwiftRs", path: "../swift-rs")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "swift-lib",
dependencies: [.product(name: "SwiftRs", package: "SwiftRs")],
path: "src")
]
)

View File

@@ -1,154 +0,0 @@
import SwiftRs
import CryptoKit
import LocalAuthentication
// reference:
// https://zenn.dev/iceman/scraps/380f69137c7ea2
// https://www.andyibanez.com/posts/cryptokit-secure-enclave/
@_cdecl("is_support_secure_enclave")
func isSupportSecureEnclave() -> Bool {
return SecureEnclave.isAvailable
}
@_cdecl("generate_secure_enclave_p256_ecdh_keypair")
func generateSecureEnclaveP256KeyPairEcdh() -> SRString {
return generateSecureEnclaveP256KeyPair(sign: false);
}
@_cdecl("generate_secure_enclave_p256_ecsign_keypair")
func generateSecureEnclaveP256KeyPairEcsign() -> SRString {
return generateSecureEnclaveP256KeyPair(sign: true);
}
func generateSecureEnclaveP256KeyPair(sign: Bool) -> SRString {
var error: Unmanaged<CFError>? = nil;
guard let accessCtrl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
&error
) else {
return SRString("err:\(error.debugDescription)")
}
do {
if (sign) {
let privateKeyReference = try SecureEnclave.P256.Signing.PrivateKey.init(
accessControl: accessCtrl
);
let publicKeyBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString()
let publicKeyPem = privateKeyReference.publicKey.derRepresentation.base64EncodedString()
let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString()
return SRString("ok:\(publicKeyBase64),\(publicKeyPem),\(dataRepresentationBase64)")
} else {
let privateKeyReference = try SecureEnclave.P256.KeyAgreement.PrivateKey.init(
accessControl: accessCtrl
);
let publicKeyBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString()
let publicKeyPem = privateKeyReference.publicKey.derRepresentation.base64EncodedString()
let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString()
return SRString("ok:\(publicKeyBase64),\(publicKeyPem),\(dataRepresentationBase64)")
}
} catch {
return SRString("err:\(error)")
}
}
@_cdecl("recover_secure_enclave_p256_ecsign_public_key")
func recoverSecureEnclaveP256PublicKeyEcsign(privateKeyDataRepresentation: SRString) -> SRString {
return recoverSecureEnclaveP256PublicKey(privateKeyDataRepresentation: privateKeyDataRepresentation, sign: true);
}
@_cdecl("recover_secure_enclave_p256_ecdh_public_key")
func recoverSecureEnclaveP256PublicKeyEcdh(privateKeyDataRepresentation: SRString) -> SRString {
return recoverSecureEnclaveP256PublicKey(privateKeyDataRepresentation: privateKeyDataRepresentation, sign: false);
}
func recoverSecureEnclaveP256PublicKey(privateKeyDataRepresentation: SRString, sign: Bool) -> SRString {
guard let privateKeyDataRepresentation = Data(
base64Encoded: privateKeyDataRepresentation.toString()
) else {
return SRString("err:private key base64 decode failed")
}
do {
let context = LAContext();
if (sign) {
let privateKeyReference = try SecureEnclave.P256.Signing.PrivateKey(
dataRepresentation: privateKeyDataRepresentation,
authenticationContext: context
)
let publicKeyBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString()
let publicKeyPem = privateKeyReference.publicKey.derRepresentation.base64EncodedString()
let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString()
return SRString("ok:\(publicKeyBase64),\(publicKeyPem),\(dataRepresentationBase64)")
} else {
let privateKeyReference = try SecureEnclave.P256.KeyAgreement.PrivateKey(
dataRepresentation: privateKeyDataRepresentation,
authenticationContext: context
)
let publicKeyBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString()
let publicKeyPem = privateKeyReference.publicKey.derRepresentation.base64EncodedString()
let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString()
return SRString("ok:\(publicKeyBase64),\(publicKeyPem),\(dataRepresentationBase64)")
}
} catch {
return SRString("err:\(error)")
}
}
@_cdecl("compute_secure_enclave_p256_ecdh")
func computeSecureEnclaveP256Ecdh(privateKeyDataRepresentation: SRString, ephemeraPublicKey: SRString) -> SRString {
guard let privateKeyDataRepresentation = Data(
base64Encoded: privateKeyDataRepresentation.toString()
) else {
return SRString("err:private key base64 decode failed")
}
guard let ephemeralPublicKeyRepresentation = Data(
base64Encoded: ephemeraPublicKey.toString()
) else {
return SRString("err:ephemeral public key base64 decode failed")
}
do {
let context = LAContext();
let p = try SecureEnclave.P256.KeyAgreement.PrivateKey(
dataRepresentation: privateKeyDataRepresentation,
authenticationContext: context
)
let ephemeralPublicKey = try P256.KeyAgreement.PublicKey.init(derRepresentation: ephemeralPublicKeyRepresentation)
let sharedSecret = try p.sharedSecretFromKeyAgreement(
with: ephemeralPublicKey)
return SRString("ok:\(sharedSecret.description)")
} catch {
return SRString("err:\(error)")
}
}
@_cdecl("compute_secure_enclave_p256_ecsign")
func computeSecureEnclaveP256Ecsign(privateKeyDataRepresentation: SRString, content: SRString) -> SRString {
guard let privateKeyDataRepresentation = Data(
base64Encoded: privateKeyDataRepresentation.toString()
) else {
return SRString("err:private key base64 decode failed")
}
guard let contentData = Data(
base64Encoded: content.toString()
) else {
return SRString("err:content base64 decode failed")
}
do {
let context = LAContext();
let p = try SecureEnclave.P256.Signing.PrivateKey(
dataRepresentation: privateKeyDataRepresentation,
authenticationContext: context
)
let digest = SHA256.hash(data: contentData)
let signature = try p.signature(for: digest)
return SRString("ok:\(signature.derRepresentation.base64EncodedString())")
} catch {
return SRString("err:\(error)")
}
}

417
swift-rs/Cargo.lock generated
View File

@@ -1,417 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pin-project-lite"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "serde_json"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serial_test"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2"
dependencies = [
"dashmap",
"futures",
"lazy_static",
"log",
"parking_lot",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "swift-rs-hatter-fork"
version = "1.0.6"
dependencies = [
"base64",
"serde",
"serde_json",
"serial_test",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@@ -1,34 +0,0 @@
[package]
name = "swift-rs"
version = "1.0.7"
description = "Call Swift from Rust with ease!"
authors = ["The swift-rs contributors"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/Brendonovich/swift-rs"
edition = "2021"
exclude=["/src-swift", "*.swift"]
build = "src-rs/test-build.rs"
# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features
[package.metadata."docs.rs"]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lib]
path = "src-rs/lib.rs"
[dependencies]
base64 = "0.22"
serde = { version = "1.0", features = ["derive"], optional = true}
serde_json = { version = "1.0", optional = true }
[build-dependencies]
serde = { version = "1.0", features = ["derive"]}
serde_json = { version = "1.0" }
[dev-dependencies]
serial_test = "0.10"
[features]
default = []
build = ["serde", "serde_json"]

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 The swift-rs developers
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,19 +0,0 @@
Copyright (c) 2023 The swift-rs Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,30 +0,0 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SwiftRs",
platforms: [
.macOS(.v10_13),
.iOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SwiftRs",
targets: ["SwiftRs"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SwiftRs",
dependencies: [],
path: "src-swift")
]
)

View File

@@ -1,483 +0,0 @@
# swift-rs
![Crates.io](https://img.shields.io/crates/v/swift-rs?color=blue&style=flat-square)
![docs.rs](https://img.shields.io/docsrs/swift-rs?color=blue&style=flat-square)
Call Swift functions from Rust with ease!
## Setup
Add `swift-rs` to your project's `dependencies` and `build-dependencies`:
```toml
[dependencies]
swift-rs = "1.0.5"
[build-dependencies]
swift-rs = { version = "1.0.5", features = ["build"] }
```
Next, some setup work must be done:
1. Ensure your swift code is organized into a Swift Package.
This can be done in XCode by selecting File -> New -> Project -> Multiplatform -> Swift Package and importing your existing code.
2. Add `SwiftRs` as a dependency to your Swift package and make the build type `.static`.
```swift
let package = Package(
dependencies: [
.package(url: "https://github.com/Brendonovich/swift-rs", from: "1.0.5")
],
products: [
.library(
type: .static,
),
],
targets: [
.target(
// Must specify swift-rs as a dependency of your target
dependencies: [
.product(
name: "SwiftRs",
package: "swift-rs"
)
],
)
]
)
```
3. Create a `build.rs` file in your project's root folder, if you don't have one already.
4. Use `SwiftLinker` in your `build.rs` file to link both the Swift runtime and your Swift package.
The package name should be the same as is specified in your `Package.swift` file,
and the path should point to your Swift project's root folder relative to your crate's root folder.
```rust
use swift_rs::SwiftLinker;
fn build() {
// swift-rs has a minimum of macOS 10.13
// Ensure the same minimum supported macOS version is specified as in your `Package.swift` file.
SwiftLinker::new("10.13")
// Only if you are also targetting iOS
// Ensure the same minimum supported iOS version is specified as in your `Package.swift` file
.with_ios("11")
.with_package(PACKAGE_NAME, PACKAGE_PATH)
.link();
// Other build steps
}
```
With those steps completed, you should be ready to start using Swift code from Rust!
If you experience the error `dyld[16008]: Library not loaded: @rpath/libswiftCore.dylib`
when using `swift-rs` with [Tauri](https://tauri.app) ensure you have set your
[Tauri minimum system version](https://tauri.app/v1/guides/building/macos#setting-a-minimum-system-version)
to `10.15` or higher in your `tauri.config.json`.
## Calling basic functions
To allow calling a Swift function from Rust, it must follow some rules:
1. It must be global
2. It must be annotated with `@_cdecl`, so that it is callable from C
3. It must only use types that can be represented in Objective-C,
so only classes that derive `NSObject`, as well as scalars such as Int and Bool.
This excludes strings, arrays, generics (though all of these can be sent with workarounds)
and structs (which are strictly forbidden).
For this example we will use a function that simply squares a number:
```swift
public func squareNumber(number: Int) -> Int {
return number * number
}
```
So far, this function meets requirements 1 and 3: it is global and public, and only uses the Int type, which is Objective-C compatible.
However, it is not annotated with `@_cdecl`.
To fix this, we must call `@_cdecl` before the function's declaration and specify the name that the function is exposed to Rust with as its only argument.
To keep with Rust's naming conventions, we will export this function in snake case as `square_number`.
```swift
@_cdecl("square_number")
public func squareNumber(number: Int) -> Int {
return number * number
}
```
Now that `squareNumber` is properly exposed to Rust, we can start interfacing with it.
This can be done using the `swift!` macro, with the `Int` type helping to provide a similar function signature:
```rust
use swift_rs::swift;
swift!(fn square_number(number: Int) -> Int);
```
Lastly, you can call the function from regular Rust functions.
Note that <b>all</b> calls to a Swift function are unsafe,
and require wrapping in an `unsafe {}` block or `unsafe fn`.
```rust
fn main() {
let input: Int = 4;
let output = unsafe { square_number(input) };
println!("Input: {}, Squared: {}", input, output);
// Prints "Input: 4, Squared: 16"
}
```
Check [the documentation](TODO) for all available helper types.
## Returning objects from Swift
Let's say that we want our `squareNumber` function to return not only the result, but also the original input.
A standard way to do this in Swift would be with a struct:
```swift
struct SquareNumberResult {
var input: Int
var output: Int
}
```
We are not allowed to do this, though, since structs cannot be represented in Objective-C.
Instead, we must use a class that extends `NSObject`:
```swift
class SquareNumberResult: NSObject {
var input: Int
var output: Int
init(_ input: Int, _ output: Int) {
self.input = input;
self.output = output
}
}
```
<sub><sup>Yes, this class could contain the squaring logic too, but that is irrelevant for this example
An instance of this class can then be returned from `squareNumber`:
```swift
@_cdecl("square_number")
public func squareNumber(input: Int) -> SquareNumberResult {
let output = input * input
return SquareNumberResult(input, output)
}
```
As you can see, returning an `NSObject` from Swift isn't too difficult.
The same can't be said for the Rust implementation, though.
`squareNumber` doesn't actually return a struct containing `input` and `output`,
but instead a pointer to a `SquareNumberResult` stored somewhere in memory.
Additionally, this value contains more data than just `input` and `output`:
Since it is an `NSObject`, it contains extra data that must be accounted for when using it in Rust.
This may sound daunting, but it's not actually a problem thanks to `SRObject<T>`.
This type manages the pointer internally, and takes a generic argument for a struct that we can access the data through.
Let's see how we'd implement `SquareNumberResult` in Rust:
```rust
use swift_rs::{swift, Int, SRObject};
// Any struct that is used in a C function must be annotated
// with this, and since our Swift function is exposed as a
// C function with @_cdecl, this is necessary here
#[repr(C)]
// Struct matches the class declaration in Swift
struct SquareNumberResult {
input: Int,
output: Int
}
// SRObject abstracts away the underlying pointer and will automatically deref to
// &SquareNumberResult through the Deref trait
swift!(fn square_number(input: Int) -> SRObject<SquareNumberResult>);
```
Then, using the new return value is just like using `SquareNumberResult` directly:
```rust
fn main() {
let input = 4;
let result = unsafe { square_number(input) };
let result_input = result.input; // 4
let result_output = result.output; // 16
}
```
Creating objects in Rust and then passing them to Swift is not supported.
## Optionals
`swift-rs` also supports Swift's `nil` type, but only for functions that return optional `NSObject`s.
Functions returning optional primitives cannot be represented in Objective C, and thus are not supported.
Let's say we have a function returning an optional `SRString`:
```swift
@_cdecl("optional_string")
func optionalString(returnNil: Bool) -> SRString? {
if (returnNil) return nil
else return SRString("lorem ipsum")
}
```
Thanks to Rust's [null pointer optimisation](https://doc.rust-lang.org/std/option/index.html#representation),
the optional nature of `SRString?` can be represented by wrapping `SRString` in Rust's `Option<T>` type!
```rust
use swift_rs::{swift, Bool, SRString};
swift!(optional_string(return_nil: Bool) -> Option<SRString>)
```
Null pointers are actually the reason why a function that returns an optional primitive cannot be represented in C.
If this were to be supported, how could a `nil` be differentiated from a number? It can't!
## Complex types
So far we have only looked at using primitive types and structs/classes,
but this leaves out some of the most important data structures: arrays (`SRArray<T>`) and strings (`SRString`).
These types must be treated with caution, however, and are not as flexible as their native Swift & Rust counterparts.
### Strings
Strings can be passed between Rust and Swift through `SRString`, which can be created from native strings in either language.
**As an argument**
```swift
import SwiftRs
@_cdecl("swift_print")
public func swiftPrint(value: SRString) {
// .to_string() converts the SRString to a Swift String
print(value.to_string())
}
```
```rust
use swift_rs::{swift, SRString, SwiftRef};
swift!(fn swift_print(value: &SRString));
fn main() {
// SRString can be created by simply calling .into() on any string reference.
// This will allocate memory in Swift and copy the string
let value: SRString = "lorem ipsum".into();
unsafe { swift_print(&value) }; // Will print "lorem ipsum" to the console
}
```
**As a return value**
```swift
import SwiftRs
@_cdecl("get_string")
public func getString() -> SRString {
let value = "lorem ipsum"
// SRString can be created from a regular String
return SRString(value)
}
```
```rust
use swift_rs::{swift, SRString};
swift!(fn get_string() -> SRString);
fn main() {
let value_srstring = unsafe { get_string() };
// SRString can be converted to an &str using as_str()...
let value_str: &str = value_srstring.as_str();
// or though the Deref trait
let value_str: &str = &*value_srstring;
// SRString also implements Display
println!("{}", value_srstring); // Will print "lorem ipsum" to the console
}
```
### Arrays
**Primitive Arrays**
Representing arrays properly is tricky, since we cannot use generics as Swift arguments or return values according to rule 3.
Instead, `swift-rs` provides a generic `SRArray<T>` that can be embedded inside another class that extends `NSObject` that is not generic,
but is restricted to a single element type.
```swift
import SwiftRs
// Argument/Return values can contain generic types, but cannot be generic themselves.
// This includes extending generic types.
class IntArray: NSObject {
var data: SRArray<Int>
init(_ data: [Int]) {
self.data = SRArray(data)
}
}
@_cdecl("get_numbers")
public func getNumbers() -> IntArray {
let numbers = [1, 2, 3, 4]
return IntArray(numbers)
}
```
```rust
use swift_rs::{Int, SRArray, SRObject};
#[repr(C)]
struct IntArray {
data: SRArray<Int>
}
// Since IntArray extends NSObject in its Swift implementation,
// it must be wrapped in SRObject on the Rust side
swift!(fn get_numbers() -> SRObject<IntArray>);
fn main() {
let numbers = unsafe { get_numbers() };
// SRArray can be accessed as a slice via as_slice
let numbers_slice: &[Int] = numbers.data.as_slice();
assert_eq!(numbers_slice, &[1, 2, 3, 4]);
}
```
To simplify things on the rust side, we can actually do away with the `IntArray` struct.
Since `IntArray` only has one field, its memory layout is identical to that of `SRArray<usize>`,
so our Rust implementation can be simplified at the cost of equivalence with our Swift code:
```rust
// We still need to wrap the array in SRObject since
// the wrapper class in Swift is an NSObject
swift!(fn get_numbers() -> SRObject<SRArray<Int>>);
```
**NSObject Arrays**
What if we want to return an `NSObject` array? There are two options on the Swift side:
1. Continue using `SRArray` and a custom wrapper type, or
2. Use `SRObjectArray`, a wrapper type provided by `swift-rs` that accepts any `NSObject` as its elements.
This can be easier than continuing to create wrapper types, but sacrifices some type safety.
There is also `SRObjectArray<T>` for Rust, which is compatible with any single-element Swift wrapper type (and of course `SRObjectArray` in Swift),
and automatically wraps its elements in `SRObject<T>`, so there's very little reason to not use it unless you _really_ like custom wrapper types.
Using `SRObjectArray` in both Swift and Rust with a basic custom class/struct can be done like this:
```swift
import SwiftRs
class IntTuple: NSObject {
var item1: Int
var item2: Int
init(_ item1: Int, _ item2: Int) {
self.item1 = item1
self.item2 = item2
}
}
@_cdecl("get_tuples")
public func getTuples() -> SRObjectArray {
let tuple1 = IntTuple(0,1),
tuple2 = IntTuple(2,3),
tuple3 = IntTuple(4,5)
let tupleArray: [IntTuple] = [
tuple1,
tuple2,
tuple3
]
// Type safety is only lost when the Swift array is converted to an SRObjectArray
return SRObjectArray(tupleArray)
}
```
```rust
use swift_rs::{swift, Int, SRObjectArray};
#[repr(C)]
struct IntTuple {
item1: Int,
item2: Int
}
// No need to wrap IntTuple in SRObject<T> since
// SRObjectArray<T> does it automatically
swift!(fn get_tuples() -> SRObjectArray<IntTuple>);
fn main() {
let tuples = unsafe { get_tuples() };
for tuple in tuples.as_slice() {
// Will print each tuple's contents to the console
println!("Item 1: {}, Item 2: {}", tuple.item1, tuple.item2);
}
}
```
Complex types can contain whatever combination of primitives and `SRObject<T>` you like, just remember to follow the 3 rules!
## Bonuses
### SRData
A wrapper type for `SRArray<T>` designed for storing `u8`s - essentially just a byte buffer.
### Tighter Memory Control with `autoreleasepool!`
If you've come to Swift from an Objective-C background, you likely know the utility of `@autoreleasepool` blocks.
`swift-rs` has your back on this too, just wrap your block of code with a `autoreleasepool!`, and that block of code now executes with its own autorelease pool!
```rust
use swift_rs::autoreleasepool;
for _ in 0..10000 {
autoreleasepool!({
// do some memory intensive thing here
});
}
```
## Limitations
Currently, the only types that can be created from Rust are number types, boolean, `SRString`, and `SRData`.
This is because those types are easy to allocate memory for, either on the stack or on the heap via calling out to swift,
whereas other types are not. This may be implemented in the future, though.
Mutating values across Swift and Rust is not currently an aim for this library, it is purely for providing arguments and returning values.
Besides, this would go against Rust's programming model, potentially allowing for multiple shared references to a value instead of interior mutability via something like a Mutex.
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms or
conditions.

View File

@@ -1,26 +0,0 @@
/// Run code with its own autorelease pool. Semantically, this is identical
/// to [`@autoreleasepool`](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html)
/// in Objective-C
///
///
/// ```no_run
/// use swift_rs::autoreleasepool;
///
/// autoreleasepool!({
/// // do something memory intensive stuff
/// })
/// ```
#[macro_export]
macro_rules! autoreleasepool {
( $expr:expr ) => {{
extern "C" {
fn objc_autoreleasePoolPush() -> *mut std::ffi::c_void;
fn objc_autoreleasePoolPop(context: *mut std::ffi::c_void);
}
let pool = unsafe { objc_autoreleasePoolPush() };
let r = { $expr };
unsafe { objc_autoreleasePoolPop(pool) };
r
}};
}

View File

@@ -1,326 +0,0 @@
#![allow(dead_code)]
use std::{env, fmt::Display, path::Path, path::PathBuf, process::Command};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SwiftTarget {
triple: String,
unversioned_triple: String,
module_triple: String,
//pub swift_runtime_compatibility_version: String,
#[serde(rename = "librariesRequireRPath")]
libraries_require_rpath: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SwiftPaths {
runtime_library_paths: Vec<String>,
runtime_library_import_paths: Vec<String>,
runtime_resource_path: String,
}
#[derive(Deserialize)]
struct SwiftEnv {
target: SwiftTarget,
paths: SwiftPaths,
}
impl SwiftEnv {
fn new(minimum_macos_version: &str, minimum_ios_version: Option<&str>) -> Self {
let rust_target = RustTarget::from_env();
let target = rust_target.swift_target_triple(minimum_macos_version, minimum_ios_version);
let swift_target_info_str = Command::new("swift")
.args(["-target", &target, "-print-target-info"])
.output()
.unwrap()
.stdout;
serde_json::from_slice(&swift_target_info_str).unwrap()
}
}
#[allow(clippy::upper_case_acronyms)]
enum RustTargetOS {
MacOS,
IOS,
}
impl RustTargetOS {
fn from_env() -> Self {
match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() {
"macos" => RustTargetOS::MacOS,
"ios" => RustTargetOS::IOS,
_ => panic!("unexpected target operating system"),
}
}
fn to_swift(&self) -> &'static str {
match self {
Self::MacOS => "macosx",
Self::IOS => "ios",
}
}
}
impl Display for RustTargetOS {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MacOS => write!(f, "macos"),
Self::IOS => write!(f, "ios"),
}
}
}
#[allow(clippy::upper_case_acronyms)]
enum SwiftSDK {
MacOS,
IOS,
IOSSimulator,
}
impl SwiftSDK {
fn from_os(os: &RustTargetOS) -> Self {
let target = env::var("TARGET").unwrap();
let simulator = target.ends_with("ios-sim")
|| (target.starts_with("x86_64") && target.ends_with("ios"));
match os {
RustTargetOS::MacOS => Self::MacOS,
RustTargetOS::IOS if simulator => Self::IOSSimulator,
RustTargetOS::IOS => Self::IOS,
}
}
fn clang_lib_extension(&self) -> &'static str {
match self {
Self::MacOS => "osx",
Self::IOS => "ios",
Self::IOSSimulator => "iossim",
}
}
}
impl Display for SwiftSDK {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MacOS => write!(f, "macosx"),
Self::IOSSimulator => write!(f, "iphonesimulator"),
Self::IOS => write!(f, "iphoneos"),
}
}
}
struct RustTarget {
arch: String,
os: RustTargetOS,
sdk: SwiftSDK,
}
impl RustTarget {
fn from_env() -> Self {
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let os = RustTargetOS::from_env();
let sdk = SwiftSDK::from_os(&os);
Self { arch, os, sdk }
}
fn swift_target_triple(
&self,
minimum_macos_version: &str,
minimum_ios_version: Option<&str>,
) -> String {
let unversioned = self.unversioned_swift_target_triple();
format!(
"{unversioned}{}{}",
match (&self.os, minimum_ios_version) {
(RustTargetOS::MacOS, _) => minimum_macos_version,
(RustTargetOS::IOS, Some(version)) => version,
_ => "",
},
// simulator suffix
matches!(self.sdk, SwiftSDK::IOSSimulator)
.then(|| "-simulator".to_string())
.unwrap_or_default()
)
}
fn unversioned_swift_target_triple(&self) -> String {
format!(
"{}-apple-{}",
match self.arch.as_str() {
"aarch64" => "arm64",
a => a,
},
self.os.to_swift(),
)
}
}
struct SwiftPackage {
name: String,
path: PathBuf,
}
/// Builder for linking the Swift runtime and custom packages.
#[cfg(feature = "build")]
pub struct SwiftLinker {
packages: Vec<SwiftPackage>,
macos_min_version: String,
ios_min_version: Option<String>,
}
impl SwiftLinker {
/// Creates a new [`SwiftLinker`] with a minimum macOS verison.
///
/// Minimum macOS version must be at least 10.13.
pub fn new(macos_min_version: &str) -> Self {
Self {
packages: vec![],
macos_min_version: macos_min_version.to_string(),
ios_min_version: None,
}
}
/// Instructs the [`SwiftLinker`] to also compile for iOS
/// using the specified minimum iOS version.
///
/// Minimum iOS version must be at least 11.
pub fn with_ios(mut self, min_version: &str) -> Self {
self.ios_min_version = Some(min_version.to_string());
self
}
/// Adds a package to be linked against.
/// `name` should match the `name` field in your `Package.swift`,
/// and `path` should point to the root of your Swift package relative
/// to your crate's root.
pub fn with_package(mut self, name: &str, path: impl AsRef<Path>) -> Self {
self.packages.extend([SwiftPackage {
name: name.to_string(),
path: path.as_ref().into(),
}]);
self
}
/// Links the Swift runtime, then builds and links the provided packages.
/// This does not (yet) automatically rebuild your Swift files when they are modified,
/// you'll need to modify/save your `build.rs` file for that.
pub fn link(self) {
let swift_env = SwiftEnv::new(&self.macos_min_version, self.ios_min_version.as_deref());
#[allow(clippy::uninlined_format_args)]
for path in swift_env.paths.runtime_library_paths {
println!("cargo:rustc-link-search=native={path}");
}
let debug = env::var("DEBUG").unwrap() == "true";
let configuration = if debug { "debug" } else { "release" };
let rust_target = RustTarget::from_env();
link_clang_rt(&rust_target);
for package in self.packages {
let package_path =
Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(&package.path);
let out_path = Path::new(&env::var("OUT_DIR").unwrap())
.join("swift-rs")
.join(&package.name);
let sdk_path_output = Command::new("xcrun")
.args(["--sdk", &rust_target.sdk.to_string(), "--show-sdk-path"])
.output()
.unwrap();
if !sdk_path_output.status.success() {
panic!(
"Failed to get SDK path with `xcrun --sdk {} --show-sdk-path`",
rust_target.sdk
);
}
let sdk_path = String::from_utf8_lossy(&sdk_path_output.stdout);
let mut command = Command::new("swift");
command.current_dir(&package.path);
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
arch => arch,
};
command
// Build the package (duh)
.args(["build"])
// SDK path for regular compilation (idk)
.args(["--sdk", sdk_path.trim()])
// Release/Debug configuration
.args(["-c", configuration])
.args(["--arch", arch])
// Where the artifacts will be generated to
.args(["--build-path", &out_path.display().to_string()])
// Override SDK path for each swiftc instance.
// Necessary for iOS compilation.
.args(["-Xswiftc", "-sdk"])
.args(["-Xswiftc", sdk_path.trim()])
// Override target triple for each swiftc instance.
// Necessary for iOS compilation.
.args(["-Xswiftc", "-target"])
.args([
"-Xswiftc",
&rust_target.swift_target_triple(
&self.macos_min_version,
self.ios_min_version.as_deref(),
),
]);
if !command.status().unwrap().success() {
panic!("Failed to compile swift package {}", package.name);
}
let search_path = out_path
// swift build uses this output folder no matter what is the target
.join(format!(
"{}-apple-macosx",
arch
))
.join(configuration);
println!("cargo:rerun-if-changed={}", package_path.display());
println!("cargo:rustc-link-search=native={}", search_path.display());
println!("cargo:rustc-link-lib=static={}", package.name);
}
}
}
fn link_clang_rt(rust_target: &RustTarget) {
println!(
"cargo:rustc-link-lib=clang_rt.{}",
rust_target.sdk.clang_lib_extension()
);
println!("cargo:rustc-link-search={}", clang_link_search_path());
}
fn clang_link_search_path() -> String {
let output = std::process::Command::new(
std::env::var("SWIFT_RS_CLANG").unwrap_or_else(|_| "/usr/bin/clang".to_string()),
)
.arg("--print-search-dirs")
.output()
.unwrap();
if !output.status.success() {
panic!("Can't get search paths from clang");
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("libraries: =") {
let path = line.split('=').nth(1).unwrap();
return format!("{}/lib/darwin", path);
}
}
panic!("clang is missing search paths");
}

View File

@@ -1,90 +0,0 @@
/// This retain-balancing algorithm is cool but likely isn't required.
/// I'm keeping it around in case it's necessary one day.
// #[derive(Clone, Copy, Debug)]
// enum ValueArity {
// Reference,
// Value,
// }
// pub unsafe fn balance_ptrs(args: Vec<(*const c_void, bool)>, ret: Vec<(*const c_void, bool)>) {
// fn collect_references(
// v: Vec<(*const c_void, bool)>,
// ) -> BTreeMap<*const c_void, Vec<ValueArity>> {
// v.into_iter().fold(
// BTreeMap::<_, Vec<ValueArity>>::new(),
// |mut map, (ptr, is_ref)| {
// map.entry(ptr).or_default().push(if is_ref {
// ValueArity::Reference
// } else {
// ValueArity::Value
// });
// map
// },
// )
// }
// let mut args = collect_references(args);
// let mut ret = collect_references(ret);
// let both_counts = args
// .clone()
// .into_iter()
// .flat_map(|(arg, values)| {
// ret.remove(&arg).map(|ret| {
// args.remove(&arg);
// let ret_values = ret
// .iter()
// .filter(|v| matches!(v, ValueArity::Value))
// .count() as isize;
// let arg_references = values
// .iter()
// .filter(|v| matches!(v, ValueArity::Reference))
// .count() as isize;
// let ref_in_value_out_retains = min(ret_values, arg_references);
// (arg, ref_in_value_out_retains)
// })
// })
// .collect::<Vec<_>>();
// let arg_counts = args.into_iter().map(|(ptr, values)| {
// let count = values
// .into_iter()
// .filter(|v| matches!(v, ValueArity::Value))
// .count() as isize;
// (ptr, count)
// });
// let ret_counts = ret
// .into_iter()
// .map(|(ptr, values)| {
// let count = values
// .into_iter()
// .filter(|v| matches!(v, ValueArity::Value))
// .count() as isize;
// (ptr, count)
// })
// .collect::<Vec<_>>();
// both_counts
// .into_iter()
// .chain(arg_counts)
// .chain(ret_counts)
// .for_each(|(ptr, count)| match count {
// 0 => {}
// n if n > 0 => {
// for _ in 0..n {
// retain_object(ptr)
// }
// }
// n => {
// for _ in n..0 {
// release_object(ptr)
// }
// }
// });
// }

View File

@@ -1,20 +0,0 @@
//! Call Swift functions from Rust with ease!
#![cfg_attr(docsrs, feature(doc_cfg))]
mod autorelease;
mod swift;
mod swift_arg;
mod swift_ret;
mod types;
// pub use autorelease::*;
pub use swift::*;
pub use swift_arg::*;
pub use swift_ret::*;
pub use types::*;
#[cfg(feature = "build")]
#[cfg_attr(docsrs, doc(cfg(feature = "build")))]
mod build;
#[cfg(feature = "build")]
pub use build::*;

Some files were not shown because too many files have changed in this diff Show More