Compare commits
102 Commits
ea0b091414
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a50024285 | |||
|
07e1671867
|
|||
|
4537b6ece9
|
|||
|
f96144f450
|
|||
|
1d49b7c1c1
|
|||
|
10c38cda8a
|
|||
|
9f544e3cb7
|
|||
|
3d29fe6a6d
|
|||
|
1d23dba248
|
|||
|
6f556cc2d6
|
|||
|
33a6661c3f
|
|||
|
421f2e2ffe
|
|||
|
3647515321
|
|||
|
6bd4d0ba57
|
|||
|
d272904357
|
|||
|
0bc671be7b
|
|||
|
a698a852fd
|
|||
|
febdf659cd
|
|||
|
62110ed7fb
|
|||
|
149650bf15
|
|||
|
5b3e0bc8cb
|
|||
|
f870c07387
|
|||
|
ecf034376d
|
|||
|
488db38387
|
|||
|
f6b9671872
|
|||
|
87e51cc7e4
|
|||
|
bb8d804505
|
|||
|
d104d4405e
|
|||
|
f5a15ca0ae
|
|||
|
28c2f096f7
|
|||
|
9926dbf09d
|
|||
|
b23f4a3a69
|
|||
|
d42bfd4bcc
|
|||
|
f74820903a
|
|||
|
21b5cc8221
|
|||
|
7fa6aa1146
|
|||
|
b4beaa3a75
|
|||
|
fb026c9f21
|
|||
|
4431bff9e6
|
|||
|
58f665823d
|
|||
|
fdf02bc976
|
|||
|
8e4cf5cec8
|
|||
|
0b9ec436ba
|
|||
|
a1ae0ff4dc
|
|||
|
06d2da4ddf
|
|||
|
0513dd2398
|
|||
|
d6ecdb5ed4
|
|||
|
81f7a6d77e
|
|||
|
63fabc6054
|
|||
|
57c3ec57df
|
|||
|
c0ea3b773d
|
|||
|
67568f8f15
|
|||
|
9435b287c8
|
|||
|
96927e0dab
|
|||
|
8894a2156a
|
|||
|
e52e42d48c
|
|||
|
a3541e7b68
|
|||
|
d7f52530df
|
|||
|
3dae02e090
|
|||
|
86489c5d29
|
|||
|
1773186dbf
|
|||
|
cec27e0f88
|
|||
|
9a749b63eb
|
|||
|
fcb10f5efa
|
|||
|
5329108380
|
|||
|
b8f0be2023
|
|||
|
0ac9300262
|
|||
|
c270c2e369
|
|||
|
3af863762f
|
|||
|
21676451fd
|
|||
|
4dca8e0146
|
|||
|
dc56f2df77
|
|||
|
4dac890200
|
|||
|
e7b20abd6d
|
|||
|
492c434f62
|
|||
|
fe30f538ba
|
|||
|
e6409174b6
|
|||
|
bb02c7c823
|
|||
|
e9388eb164
|
|||
|
a6bff6d31c
|
|||
|
6d3298549e
|
|||
|
417e3f6a49
|
|||
|
3a40d7f0ad
|
|||
|
1be5754ed1
|
|||
|
7ec3a705cf
|
|||
|
e2fa3bba9f
|
|||
|
070161c056
|
|||
|
3fb43403aa
|
|||
|
c2b3a779c8
|
|||
|
755d61fa86
|
|||
|
af20f4c4a0
|
|||
|
1582f76cae
|
|||
|
3848b65ff1
|
|||
|
d4fce3f4fc
|
|||
|
8aeb47c66f
|
|||
|
d04038ccd9
|
|||
|
aee2f8d5d3
|
|||
|
25e80661bb
|
|||
|
c5e35fd941
|
|||
|
468701b3ff
|
|||
| 4a48e932d4 | |||
|
8b6056db34
|
2312
Cargo.lock
generated
2312
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
64
Cargo.toml
64
Cargo.toml
@@ -1,13 +1,16 @@
|
||||
[package]
|
||||
name = "card-cli"
|
||||
version = "1.10.21"
|
||||
version = "1.13.21"
|
||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
description = "FIDO(U2F, WebAuthn), YubiKey, OpenPGP command line tool"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://git.hatter.ink/hatter/card-cli"
|
||||
|
||||
[features]
|
||||
default = ["with-sequoia-openpgp", "with-secure-enclave"]
|
||||
default = ["with-sequoia-openpgp"]
|
||||
with-sequoia-openpgp = ["sequoia-openpgp", "openpgp-card-sequoia"]
|
||||
with-secure-enclave = ["swift-rs"]
|
||||
|
||||
[dependencies]
|
||||
authenticator = "0.3"
|
||||
@@ -17,15 +20,14 @@ digest = "0.10"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
rand = "0.8"
|
||||
base64 = "0.21"
|
||||
base64 = "0.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
hex = "0.4"
|
||||
u2f = "0.2"
|
||||
openpgp-card = "0.3"
|
||||
openpgp-card-pcsc = "0.3"
|
||||
openpgp-card-sequoia = { version = "0.1", optional = true }
|
||||
sequoia-openpgp = { version = "1.0", optional = true }
|
||||
openpgp-card-sequoia = { version = "0.2", optional = true }
|
||||
sequoia-openpgp = { version = "2.0", optional = true }
|
||||
chrono = "0.4"
|
||||
simpledateformat = "0.1"
|
||||
ring = "0.17"
|
||||
@@ -34,31 +36,41 @@ pem = "3.0"
|
||||
yubikey = { version = "0.8", features = ["untested"] }
|
||||
yubico_manager = "0.9"
|
||||
x509 = "0.2"
|
||||
x509-parser = { version = "0.15", features = ["verify"] }
|
||||
x509-parser = { version = "0.17", features = ["verify"] }
|
||||
ssh-agent = { version = "0.2", features = ["agent"] }
|
||||
p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa"] }
|
||||
p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa"] }
|
||||
p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
|
||||
p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
|
||||
p521 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
|
||||
spki = { version = "0.7", features = ["pem"] }
|
||||
tabled = "0.14"
|
||||
env_logger = "0.10"
|
||||
tabled = "0.20"
|
||||
env_logger = "0.11"
|
||||
bech32 = "0.9"
|
||||
ecdsa = { version = "0.16", features = ["verifying", "spki", "pem", "der"] }
|
||||
jwt = "0.16"
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
pinentry = "0.5"
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
pinentry = "0.6"
|
||||
rpassword = "7.3"
|
||||
secrecy = "0.8"
|
||||
der-parser = "9.0"
|
||||
sshcerts = "0.13"
|
||||
swift-rs = { version = "1.0.7", optional = true }
|
||||
regex = "1.4.6"
|
||||
aes-gcm-stream = "0.2.4"
|
||||
secrecy = "0.10"
|
||||
der-parser = "10.0"
|
||||
sshcerts-hatter-fork = "0.14.1"
|
||||
regex = "1.11"
|
||||
aes-gcm-stream = "0.2"
|
||||
swift-secure-enclave-tool-rs = "1.0"
|
||||
u2f-hatter-fork = "0.2"
|
||||
security-framework = { version = "3.0", features = ["OSX_10_15"] }
|
||||
rsa = "0.9"
|
||||
which = "8.0"
|
||||
percent-encoding = "2.3.1"
|
||||
external-command-rs = "0.1.1"
|
||||
ssh-agent-lib = { version = "0.5.1" }
|
||||
ssh-key = { version = "0.6", features = ["ecdsa", "alloc"] }
|
||||
tokio = "1.45.1"
|
||||
ssh-encoding = { version = "0.2.0", features = ["alloc"] }
|
||||
zeroize = "1.8"
|
||||
ml-kem = { version = "0.2.1", features = ["zeroize"] }
|
||||
zeroizing-alloc = "0.1.0"
|
||||
#lazy_static = "1.4.0"
|
||||
#ssh-key = "0.4.0"
|
||||
#ctap-hid-fido2 = "2.1.3"
|
||||
|
||||
[build-dependencies]
|
||||
swift-rs = { version = "1.0.7", features = ["build"], optional = true }
|
||||
|
||||
[patch.crates-io]
|
||||
u2f = { git = "https://github.com/jht5945/u2f-rs.git" }
|
||||
#[patch.crates-io]
|
||||
#ml-kem = { path = "externals/ml-kem" }
|
||||
|
||||
@@ -247,6 +247,10 @@ SSH to server:
|
||||
ssh -i id_user root@example.com
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
> `external_*` subcommands follow <<Cryptography external command specification>><br>
|
||||
> Specification: https://openwebstandard.org/rfc1
|
||||
|
||||
<br><br>
|
||||
|
||||
|
||||
9
build.rs
9
build.rs
@@ -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
26
examples/rsa.rs
Normal 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()));
|
||||
}
|
||||
4
justfile
4
justfile
@@ -1,6 +1,10 @@
|
||||
_:
|
||||
@just --list
|
||||
|
||||
# publish
|
||||
publish:
|
||||
cargo publish --registry crates-io
|
||||
|
||||
# install card-cli
|
||||
install:
|
||||
cargo install --path .
|
||||
|
||||
@@ -5,14 +5,22 @@ use std::io::Read;
|
||||
use clap::ArgMatches;
|
||||
use rust_util::XResult;
|
||||
|
||||
use crate::digest::{sha256, sha256_bytes};
|
||||
use crate::digestutil::DigestAlgorithm;
|
||||
|
||||
|
||||
pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
|
||||
get_sha256_digest_or_hash_with_file_opt(sub_arg_matches, &None)
|
||||
}
|
||||
|
||||
pub fn get_digest_or_hash(sub_arg_matches: &ArgMatches, digest: DigestAlgorithm) -> XResult<Vec<u8>> {
|
||||
get_digest_or_hash_with_file_opt(sub_arg_matches, &None, digest)
|
||||
}
|
||||
|
||||
pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>) -> XResult<Vec<u8>> {
|
||||
get_digest_or_hash_with_file_opt(sub_arg_matches, file_opt, DigestAlgorithm::Sha256)
|
||||
}
|
||||
|
||||
pub fn get_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>, digest: DigestAlgorithm) -> XResult<Vec<u8>> {
|
||||
let file_opt = file_opt.as_ref().map(String::as_str);
|
||||
if let Some(file) = sub_arg_matches.value_of("file").or(file_opt) {
|
||||
let metadata = opt_result!(fs::metadata(file), "Read file: {} metadata filed: {}", file);
|
||||
@@ -28,9 +36,9 @@ pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, fil
|
||||
let mut f = opt_result!(File::open(file), "Open file: {} failed: {}", file);
|
||||
let mut content = vec![];
|
||||
opt_result!(f.read_to_end(&mut content), "Read file: {} failed: {}", file);
|
||||
Ok(sha256_bytes(&content))
|
||||
Ok(digest.digest(&content))
|
||||
} else if let Some(input) = sub_arg_matches.value_of("input") {
|
||||
Ok(sha256(input))
|
||||
Ok(digest.digest_str(input))
|
||||
} else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") {
|
||||
Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}"))
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
|
||||
use crate::hmacutil;
|
||||
use crate::{cmdutil, hmacutil};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -17,12 +16,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256"))
|
||||
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA384"))
|
||||
.arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA512"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
|
||||
let hmac_result = hmacutil::compute_yubikey_hmac(&challenge_bytes)?;
|
||||
|
||||
46
src/cmd_convert_jwk_to_pem.rs
Normal file
46
src/cmd_convert_jwk_to_pem.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
42
src/cmd_convert_pem_to_jwk.rs
Normal file
42
src/cmd_convert_pem_to_jwk.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
|
||||
use crate::ecdsautil::EcdsaAlgorithm;
|
||||
use crate::{argsutil, ecdsautil};
|
||||
use crate::{argsutil, cmdutil, ecdsautil, util};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -19,12 +18,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
|
||||
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
|
||||
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
|
||||
let public_key = if let Some(public_key_hex) = sub_arg_matches.value_of("public-key-hex") {
|
||||
@@ -55,7 +53,7 @@ impl Command for CommandImpl {
|
||||
json.insert("signature_hex", hex::encode(&signature));
|
||||
}
|
||||
|
||||
match ecdsautil::ecdsaverify(ecdsa_algorithm, &public_key, &hash_bytes, &signature) {
|
||||
match ecdsautil::ecdsa_verify(ecdsa_algorithm, &public_key, &hash_bytes, &signature) {
|
||||
Ok(_) => {
|
||||
success!("Verify ECDSA succeed.");
|
||||
if json_output {
|
||||
@@ -72,7 +70,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
142
src/cmd_external_ecdh.rs
Normal file
142
src/cmd_external_ecdh.rs
Normal 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, ¶meter, ephemeral_public_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/cmd_external_public_key.rs
Normal file
121
src/cmd_external_public_key.rs
Normal 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, ¶meter)
|
||||
}
|
||||
}
|
||||
}
|
||||
169
src/cmd_external_sign.rs
Normal file
169
src/cmd_external_sign.rs
Normal 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, ¶meter, 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
40
src/cmd_external_spec.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ use x509_parser::nom::AsBytes;
|
||||
use yubikey::{Key, YubiKey};
|
||||
use yubikey::piv::{sign_data, SlotId};
|
||||
|
||||
use crate::{argsutil, pinutil, pivutil};
|
||||
use crate::digest::sha256_bytes;
|
||||
use crate::{argsutil, cmdutil, pinutil, pivutil};
|
||||
use crate::digestutil::sha256_bytes;
|
||||
use crate::signfile::{CERTIFICATES_SEARCH_URL, HASH_ALGORITHM_SHA256, SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA, SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile, SimpleSignFileSignature};
|
||||
use crate::util::base64_encode;
|
||||
|
||||
@@ -36,14 +36,13 @@ pub struct CommandImpl;
|
||||
// all hex is in lower case default
|
||||
// file ext: *.simple-sig
|
||||
impl Command for CommandImpl {
|
||||
fn name(&self) -> &str { "sign-file" }
|
||||
fn name(&self) -> &str { "file-sign" }
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV sign(with SHA256) subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(Arg::with_name("slot").short("s").long("slot")
|
||||
.takes_value(true).required(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).required(true).help("Input file"))
|
||||
.arg(Arg::with_name("filename").short("n").long("filename").takes_value(true).help("Filename"))
|
||||
.arg(Arg::with_name("sign-file").short("S").long("sign-file").takes_value(false).help("Sign file"))
|
||||
@@ -11,14 +11,14 @@ use x509_parser::public_key::PublicKey;
|
||||
use x509_parser::time::ASN1Time;
|
||||
|
||||
use crate::argsutil;
|
||||
use crate::digest::sha256_bytes;
|
||||
use crate::digestutil::sha256_bytes;
|
||||
use crate::signfile::{SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile};
|
||||
use crate::util::base64_decode;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
impl Command for CommandImpl {
|
||||
fn name(&self) -> &str { "verify-file" }
|
||||
fn name(&self) -> &str { "file-verify" }
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV verify(with SHA256) subcommand")
|
||||
@@ -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
70
src/cmd_hmac_decrypt.rs
Normal 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
81
src/cmd_hmac_encrypt.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::{util_msg, XResult};
|
||||
use rust_util::XResult;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
|
||||
use crate::hmacutil;
|
||||
use crate::{cmdutil, hmacutil};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -19,12 +19,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256"))
|
||||
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA256"))
|
||||
.arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA256"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let variable = sub_arg_matches.is_present("variable");
|
||||
let secret_bytes = get_secret_bytes(sub_arg_matches)?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
169
src/cmd_keypair_generate.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
38
src/cmd_keypair_keychain_export.rs
Normal file
38
src/cmd_keypair_keychain_export.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
42
src/cmd_keypair_keychain_import.rs
Normal file
42
src/cmd_keypair_keychain_import.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use yubikey::YubiKey;
|
||||
use serde_json::Value;
|
||||
use crate::{cmdutil, util, yubikeyutil};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -12,34 +12,37 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("YubiKey list")
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("name", yk.name().to_string());
|
||||
json.insert("version", yk.version().to_string());
|
||||
json.insert("serial", yk.serial().0.to_string());
|
||||
let mut json = BTreeMap::<&'_ str, Value>::new();
|
||||
json.insert("name", yk.name().into());
|
||||
json.insert("version", yk.version().to_string().into());
|
||||
json.insert("serial", yk.serial().0.into());
|
||||
if let Ok(pin_retries) = yk.get_pin_retries() {
|
||||
json.insert("pin_retries", pin_retries.to_string());
|
||||
json.insert("pin_retries", pin_retries.into());
|
||||
}
|
||||
if let Ok(chuid) = yk.chuid() {
|
||||
json.insert("chuid", chuid.to_string());
|
||||
json.insert("chuid", chuid.to_string().into());
|
||||
}
|
||||
if let Ok(ccuid) = yk.cccid() {
|
||||
json.insert("ccuid", ccuid.to_string());
|
||||
json.insert("ccuid", ccuid.to_string().into());
|
||||
}
|
||||
if let Ok(piv_keys) = yk.piv_keys() {
|
||||
json.insert("keys", piv_keys.iter().map(|k| format!("{}", k.slot())).collect::<Vec<_>>().join(", "));
|
||||
let key_list = piv_keys.iter().map(|k| Value::String(format!("{}", k.slot()))).collect::<Vec<_>>();
|
||||
json.insert("key_list", key_list.into());
|
||||
let keys = piv_keys.iter().map(|k| format!("{}", k.slot())).collect::<Vec<_>>().join(", ");
|
||||
json.insert("keys", keys.into());
|
||||
}
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).expect("Convert to JSON failed!"));
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
success!("Name: {}", yk.name());
|
||||
success!("Version: {}", yk.version());
|
||||
|
||||
@@ -3,13 +3,10 @@ use std::collections::BTreeMap;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
|
||||
use crate::{cmdutil, util};
|
||||
use crate::ecdsautil::parse_ecdsa_r_and_s;
|
||||
use crate::util::try_decode;
|
||||
|
||||
const SEPARATOR: &str = ".";
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
impl Command for CommandImpl {
|
||||
@@ -27,14 +24,11 @@ impl Command for CommandImpl {
|
||||
.takes_value(true)
|
||||
.help("ECDSA signature"),
|
||||
)
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output {
|
||||
util_msg::set_logger_std_out(false);
|
||||
}
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
|
||||
@@ -57,7 +51,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
@@ -21,7 +19,7 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("in").short("i").long("in").takes_value(true).help("File input, *.pgp or *.asc"))
|
||||
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
|
||||
.arg(Arg::with_name("verbose").long("verbose").help("Verbose output"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
// .arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
@@ -151,7 +149,7 @@ impl Command for CommandImpl {
|
||||
debugging!("Found PKESK: {:?}", pkesk);
|
||||
match pkesk {
|
||||
PKESK::V3(pkesk3) => {
|
||||
information!("Found public key encrypted session key, key ID: {}, alog: {}", pkesk3.recipient(), pkesk3.pk_algo());
|
||||
information!("Found public key encrypted session key, key ID: {:?}, alog: {}", pkesk3.recipient(), pkesk3.pk_algo());
|
||||
}
|
||||
unknown => warning!("Unknown PKESK: {:?}", unknown),
|
||||
}
|
||||
@@ -162,19 +160,21 @@ impl Command for CommandImpl {
|
||||
Packet::SEIP(seip) => {
|
||||
debugging!("Found SEIP: {:?}", seip);
|
||||
match seip {
|
||||
SEIP::V1(seip1) => match seip1.deref().body() {
|
||||
SEIP::V1(seip1) => match seip1.body() {
|
||||
Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()),
|
||||
Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()),
|
||||
}
|
||||
SEIP::V2(seip2) => match seip2.body() {
|
||||
Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()),
|
||||
Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()),
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
Packet::MDC(mdc) => {
|
||||
information!("Found MDC: {:?}", mdc);
|
||||
}
|
||||
Packet::AED(aed) => {
|
||||
information!("Found AED: {:?}", aed);
|
||||
}
|
||||
Packet::Unknown(unknown) => {
|
||||
warning!("Found unknown: {:?}", unknown);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use bech32::{ToBase32, Variant};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use openpgp_card::algorithm::{Algo, Curve};
|
||||
use openpgp_card::crypto_data::{EccType, PublicKeyMaterial};
|
||||
use openpgp_card::{KeyType, OpenPgp};
|
||||
use openpgp_card_pcsc::PcscBackend;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use std::collections::BTreeMap;
|
||||
use crate::{cmdutil, util};
|
||||
|
||||
const AGE_PUBLIC_KEY_PREFIX: &str = "age";
|
||||
|
||||
@@ -17,12 +17,11 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("OpenPGP Card encryption key to age address")
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
|
||||
|
||||
@@ -67,7 +66,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&cards_output).unwrap());
|
||||
util::print_pretty_json(&cards_output);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -2,10 +2,10 @@ use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use openpgp_card::crypto_data::Cryptogram;
|
||||
use rust_util::{util_msg, XResult};
|
||||
use rust_util::XResult;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
|
||||
use crate::{pgpcardutil, pinutil};
|
||||
use crate::{cmdutil, pgpcardutil, pinutil, util};
|
||||
use crate::util::{base64_encode, read_stdin, try_decode};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -36,12 +36,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("ciphertext").short("c").long("ciphertext").takes_value(true).help("Cipher text (HEX or Base64)"))
|
||||
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
|
||||
.arg(Arg::with_name("algo").long("algo").takes_value(true).help("Algo: RSA, X25519/ECDH"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
|
||||
let pin_opt = pinutil::get_pin(pin_opt);
|
||||
@@ -89,7 +88,7 @@ impl Command for CommandImpl {
|
||||
json.insert("text_utf8", text);
|
||||
}
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -4,8 +4,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use openpgp_card::{KeyType, OpenPgp};
|
||||
use openpgp_card_pcsc::PcscBackend;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
|
||||
use crate::{cmdutil, util};
|
||||
use crate::pkiutil::openpgp_card_public_key_pem as public_key_pem;
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -16,13 +15,13 @@ impl Command for CommandImpl {
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("OpenPGP Card list subcommand")
|
||||
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let detail_output = sub_arg_matches.is_present("detail");
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
|
||||
let mut jsons = vec![];
|
||||
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
|
||||
@@ -136,7 +135,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&jsons).unwrap());
|
||||
util::print_pretty_json(&jsons);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::File;
|
||||
use std::io::{ErrorKind, Read};
|
||||
|
||||
use std::ops::Deref;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use digest::Digest;
|
||||
use openpgp_card::crypto_data::Hash;
|
||||
use rust_util::{util_msg, XResult};
|
||||
use rust_util::XResult;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use sha2::{Sha256, Sha384, Sha512};
|
||||
|
||||
use crate::{pgpcardutil, pinutil};
|
||||
use crate::{cmdutil, pgpcardutil, pinutil, util};
|
||||
use crate::util::base64_encode;
|
||||
|
||||
const BUFF_SIZE: usize = 512 * 1024;
|
||||
@@ -49,12 +49,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("use-sha384").long("use-sha384").help("Use SHA384 for file in"))
|
||||
.arg(Arg::with_name("use-sha512").long("use-sha512").help("Use SHA512 for file in"))
|
||||
.arg(Arg::with_name("algo").long("algo").takes_value(true).help("Algorithm, rsa, ecdsa, eddsa"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
|
||||
let pin_opt = pinutil::get_pin(pin_opt);
|
||||
@@ -115,7 +114,7 @@ impl Command for CommandImpl {
|
||||
|
||||
if let Some(sha256) = sha256 {
|
||||
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}");
|
||||
let sha256_hex = crate::digest::copy_sha256(&sha256_hex)?;
|
||||
let sha256_hex = crate::digestutil::copy_sha256(&sha256_hex)?;
|
||||
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
||||
success!("User sign pin verify success!");
|
||||
let sig = match algo {
|
||||
@@ -134,7 +133,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
if let Some(sha384) = sha384 {
|
||||
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}");
|
||||
let sha384_hex = crate::digest::copy_sha384(&sha384_hex)?;
|
||||
let sha384_hex = crate::digestutil::copy_sha384(&sha384_hex)?;
|
||||
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
||||
success!("User sign pin verify success!");
|
||||
let sig = match algo {
|
||||
@@ -153,7 +152,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
if let Some(sha512) = sha512 {
|
||||
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}");
|
||||
let sha512_hex = crate::digest::copy_sha512(&sha512_hex)?;
|
||||
let sha512_hex = crate::digestutil::copy_sha512(&sha512_hex)?;
|
||||
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
||||
success!("User sign pin verify success!");
|
||||
let sig = match algo {
|
||||
@@ -172,7 +171,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -190,7 +189,7 @@ where
|
||||
debugging!("File: {}, length: {}", file_name, file_len);
|
||||
loop {
|
||||
let len = match f.read(&mut buf) {
|
||||
Ok(0) => return Ok(hasher.finalize().as_slice().to_vec()),
|
||||
Ok(0) => return Ok(hasher.finalize().deref().to_vec()),
|
||||
Ok(len) => len,
|
||||
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => return simple_error!("Calc file digest failed: {}", e),
|
||||
@@ -10,7 +10,7 @@ use spki::der::Encode;
|
||||
use x509_parser::parse_x509_certificate;
|
||||
use yubikey::{Certificate, YubiKey};
|
||||
use yubikey::piv::SlotId;
|
||||
|
||||
use crate::{cmdutil, yubikeyutil};
|
||||
use crate::pivutil::get_algorithm_id;
|
||||
use crate::pkiutil::{bytes_to_pem, get_pki_algorithm};
|
||||
|
||||
@@ -23,6 +23,7 @@ impl Command for CommandImpl {
|
||||
SubCommand::with_name(self.name()).about("PIV subcommand")
|
||||
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
|
||||
.arg(Arg::with_name("show-config").long("show-config").help("Show config output"))
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
// .arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
}
|
||||
|
||||
@@ -30,7 +31,7 @@ impl Command for CommandImpl {
|
||||
let detail_output = sub_arg_matches.is_present("detail");
|
||||
let show_config = sub_arg_matches.is_present("show-config");
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
success!("Name: {}", yk.name());
|
||||
information!("Version: {}", yk.version());
|
||||
information!("Serial: {}", yk.serial());
|
||||
|
||||
@@ -2,11 +2,9 @@ use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use yubikey::piv::AlgorithmId;
|
||||
use yubikey::YubiKey;
|
||||
|
||||
use crate::{pinutil, pivutil};
|
||||
use crate::{cmdutil, pinutil, pivutil, util, yubikeyutil};
|
||||
use crate::util::{read_stdin, try_decode};
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -16,17 +14,17 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV decrypt(RSA) subcommand")
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("ciphertext").long("ciphertext").short("c").takes_value(true).help("Encrypted data (HEX or Base64)"))
|
||||
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
|
||||
|
||||
@@ -40,7 +38,7 @@ impl Command for CommandImpl {
|
||||
return simple_error!("Argument --ciphertext must be assigned");
|
||||
};
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
if let Some(pin) = &pin_opt {
|
||||
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
|
||||
}
|
||||
@@ -75,7 +73,8 @@ impl Command for CommandImpl {
|
||||
json.insert("decrypted_data_hex", hex::encode(decrypted_data_bytes));
|
||||
json.insert("clear_data_hex", hex::encode(clear_data));
|
||||
json.insert("clear_data", String::from_utf8_lossy(clear_data).to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -4,11 +4,10 @@ use std::fs;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rand::rngs::OsRng;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use yubikey::{PinPolicy, YubiKey};
|
||||
use yubikey::PinPolicy;
|
||||
use yubikey::piv::{AlgorithmId, decrypt_data, metadata};
|
||||
|
||||
use crate::{ecdhutil, pinutil, pivutil};
|
||||
use crate::{cmdutil, ecdhutil, pinutil, pivutil, util, yubikeyutil};
|
||||
use crate::pivutil::get_algorithm_id;
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -18,9 +17,9 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV ECDH subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ..."))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("public-256").long("public-256").help("Public key (P-256)"))
|
||||
.arg(Arg::with_name("public-384").long("public-384").help("Public key (P-384)"))
|
||||
.arg(Arg::with_name("private").long("private").help("Private key(PIV)"))
|
||||
@@ -28,12 +27,12 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("public-key").long("public-key").takes_value(true).help("Public key"))
|
||||
.arg(Arg::with_name("public-key-file").long("public-key-file").takes_value(true).help("Public key"))
|
||||
.arg(Arg::with_name("public-key-point-hex").long("public-key-point-hex").takes_value(true).help("Public key point hex"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let public256 = sub_arg_matches.is_present("public-256");
|
||||
let public384 = sub_arg_matches.is_present("public-384");
|
||||
@@ -76,7 +75,7 @@ impl Command for CommandImpl {
|
||||
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ...");
|
||||
let epk = opt_value_result!(sub_arg_matches.value_of("epk"), "--epk must assigned");
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
let slot_id = pivutil::get_slot_id(slot)?;
|
||||
debugging!("Slot id: {}", slot_id);
|
||||
if let Ok(meta) = metadata(&mut yk, slot_id) {
|
||||
@@ -128,7 +127,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -2,13 +2,12 @@ use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use x509_parser::nom::AsBytes;
|
||||
use yubikey::piv::{metadata, sign_data, AlgorithmId, ManagementAlgorithmId};
|
||||
use yubikey::YubiKey;
|
||||
|
||||
use crate::util::base64_encode;
|
||||
use crate::{argsutil, pivutil};
|
||||
use crate::{argsutil, cmdutil, pivutil, util, yubikeyutil};
|
||||
use crate::digestutil::DigestAlgorithm;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -16,32 +15,32 @@ impl Command for CommandImpl {
|
||||
fn name(&self) -> &str { "piv-ecsign" }
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV EC sign(with SHA256) subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
SubCommand::with_name(self.name()).about("PIV EC sign(with SHA256/SHA384) subcommand")
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("algorithm").short("a").long("algorithm").takes_value(true).help("Algorithm, p256 or p384"))
|
||||
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
|
||||
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
|
||||
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
|
||||
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
|
||||
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
|
||||
let (algorithm, algorithm_str) = match sub_arg_matches.value_of("algorithm") {
|
||||
None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256"),
|
||||
Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha256"),
|
||||
let (algorithm, algorithm_str, digest_algorithm) = match sub_arg_matches.value_of("algorithm") {
|
||||
None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256", DigestAlgorithm::Sha256),
|
||||
Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha384", DigestAlgorithm::Sha384),
|
||||
Some(unknown_algorithm) => return simple_error!("Unknown algorithm {}, e.g. p256 or p384", unknown_algorithm),
|
||||
};
|
||||
let hash_bytes = argsutil::get_digest_or_hash(sub_arg_matches, digest_algorithm)?;
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
let slot_id = pivutil::get_slot_id(slot)?;
|
||||
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches);
|
||||
|
||||
@@ -82,7 +81,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use yubikey::{PinPolicy, piv, TouchPolicy, YubiKey};
|
||||
use yubikey::{PinPolicy, piv, TouchPolicy};
|
||||
use yubikey::piv::{AlgorithmId, SlotId};
|
||||
|
||||
use crate::pinutil;
|
||||
use crate::{cmdutil, pinutil, yubikeyutil};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -13,15 +12,13 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV generate subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(Arg::with_name("force").long("force").help("Force generate"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
// .arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
|
||||
warning!("This feature is not works");
|
||||
let pin_opt = sub_arg_matches.value_of("pin");
|
||||
let pin_opt = pinutil::get_pin(pin_opt);
|
||||
@@ -32,7 +29,7 @@ impl Command for CommandImpl {
|
||||
failure_and_exit!("--force must be assigned");
|
||||
}
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
|
||||
|
||||
let public_key_info = opt_result!(piv::generate(&mut yk,SlotId::Signature, AlgorithmId::Rsa2048,
|
||||
@@ -40,7 +37,6 @@ impl Command for CommandImpl {
|
||||
|
||||
success!("Generate key success: {:?}", public_key_info);
|
||||
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use p256::pkcs8::der::Encode;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use rust_util::util_msg::MessageType;
|
||||
use x509_parser::parse_x509_certificate;
|
||||
use yubikey::{Key, YubiKey};
|
||||
use yubikey::Key;
|
||||
use yubikey::piv::{AlgorithmId, metadata};
|
||||
|
||||
use crate::pivutil;
|
||||
use crate::{cmdutil, pivutil, util, yubikeyutil};
|
||||
use crate::keyutil::{KeyAlgorithmId, KeyUri, YubikeyPivKey};
|
||||
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
|
||||
use crate::pkiutil::bytes_to_pem;
|
||||
use crate::sshutil::SshVecWriter;
|
||||
@@ -22,19 +23,19 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV meta subcommand")
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
|
||||
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
|
||||
let slot_id = pivutil::get_slot_id(slot)?;
|
||||
json.insert("slot", pivutil::to_slot_hex(&slot_id));
|
||||
@@ -97,11 +98,18 @@ impl Command for CommandImpl {
|
||||
ssh_public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes());
|
||||
ssh_public_key.write_string(pk_point_hex);
|
||||
let ssh_public_key_str = format!(
|
||||
"ecdsa-sha2-nistp{} {} PIV:{}", ec_bit_len, base64_encode(ssh_public_key), slot_id);
|
||||
"ecdsa-sha2-nistp{} {} YubiKey-PIV-{}", ec_bit_len, base64_encode(ssh_public_key), slot_id);
|
||||
json.insert("ssh_public_key", ssh_public_key_str.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let yubikey_piv_key = YubikeyPivKey {
|
||||
key_name: format!("yubikey{}-{}", yk.version().major, yk.serial().0),
|
||||
algorithm: KeyAlgorithmId::from_algorithm_id(algorithm_id),
|
||||
slot: slot_id,
|
||||
};
|
||||
json.insert("key_uri", KeyUri::YubikeyPiv(yubikey_piv_key).to_string());
|
||||
}
|
||||
let serial_lower = cert.serial_number.to_string().to_lowercase();
|
||||
json.insert("serial", if serial_lower.starts_with("00:") { serial_lower.chars().skip(3).collect() } else { serial_lower });
|
||||
@@ -135,7 +143,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -2,11 +2,10 @@ use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use yubikey::{piv, YubiKey};
|
||||
use yubikey::piv;
|
||||
use yubikey::piv::{AlgorithmId, SlotId};
|
||||
|
||||
use crate::{pinutil, pivutil, rsautil};
|
||||
use crate::{cmdutil, pinutil, pivutil, rsautil, util, yubikeyutil};
|
||||
use crate::util::base64_encode;
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -16,22 +15,22 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV RSA sign(with SHA256) subcommand")
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 HEX"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let pin_opt = pinutil::read_pin(sub_arg_matches);
|
||||
|
||||
let sha256_hex_opt = sub_arg_matches.value_of("sha256").map(|s| s.to_string());
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
if let Some(pin) = &pin_opt {
|
||||
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
|
||||
}
|
||||
@@ -55,7 +54,8 @@ impl Command for CommandImpl {
|
||||
json.insert("hash_hex", hex::encode(&sha256));
|
||||
json.insert("sign_hex", hex::encode(sign_bytes));
|
||||
json.insert("sign_base64", base64_encode(sign_bytes));
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
success!("Signature HEX: {}", hex::encode(sign_bytes));
|
||||
success!("Signature base64: {}", base64_encode(sign_bytes));
|
||||
@@ -1,6 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::{util_msg, XResult};
|
||||
use rust_util::XResult;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Map, Value};
|
||||
use spki::der::Encode;
|
||||
@@ -9,7 +9,7 @@ use tabled::{Table, Tabled};
|
||||
use x509_parser::parse_x509_certificate;
|
||||
use yubikey::piv::{metadata, SlotId};
|
||||
use yubikey::{Certificate, YubiKey};
|
||||
|
||||
use crate::{cmdutil, util, yubikeyutil};
|
||||
use crate::pivutil::{get_algorithm_id_by_certificate, ToStr, ORDERED_SLOTS};
|
||||
|
||||
const NA: &str = "N/A";
|
||||
@@ -35,20 +35,21 @@ impl Command for CommandImpl {
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV subcommand")
|
||||
.arg(Arg::with_name("table").long("table").help("Show table"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(Arg::with_name("all").long("all").help("Show all"))
|
||||
.arg(Arg::with_name("ordered").long("ordered").help("Show ordered"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let show_table = sub_arg_matches.is_present("table");
|
||||
let show_all = sub_arg_matches.is_present("all");
|
||||
let show_ordered = sub_arg_matches.is_present("ordered");
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
|
||||
let mut output = Map::new();
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||
|
||||
success!("Name: {}", yk.name());
|
||||
information!("Version: {}", yk.version());
|
||||
@@ -99,7 +100,7 @@ impl Command for CommandImpl {
|
||||
output.insert("piv_slots".to_string(), Value::Array(piv_slots_values));
|
||||
}
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&output).unwrap());
|
||||
util::print_pretty_json(&output);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -2,14 +2,11 @@ use std::collections::BTreeMap;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use openssl::rsa::{Padding, Rsa};
|
||||
use rust_util::{util_msg, XResult};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use yubikey::{Key, YubiKey};
|
||||
use yubikey::piv::{AlgorithmId, SlotId};
|
||||
use yubikey::piv::AlgorithmId;
|
||||
|
||||
use crate::{argsutil, ecdsautil, pivutil};
|
||||
use crate::{argsutil, cmdutil, ecdsautil, pivutil, util, yubikeyutil};
|
||||
use crate::ecdsautil::EcdsaAlgorithm;
|
||||
use crate::pivutil::slot_equals;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -18,17 +15,17 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("PIV verify subcommand")
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(Arg::with_name("signature-hex").short("t").long("signature-hex").takes_value(true).help("Signature"))
|
||||
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
|
||||
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
|
||||
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
.arg(cmdutil::build_serial_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
|
||||
let signature = if let Some(signature_hex) = sub_arg_matches.value_of("signature-hex") {
|
||||
@@ -43,7 +40,7 @@ impl Command for CommandImpl {
|
||||
|
||||
let slot_id = pivutil::get_slot_id(slot)?;
|
||||
json.insert("slot", pivutil::to_slot_hex(&slot_id));
|
||||
if let Some(key) = find_key(&slot_id)? {
|
||||
if let Some(key) = yubikeyutil::open_and_find_key(&slot_id, sub_arg_matches)? {
|
||||
let certificate = key.certificate();
|
||||
let tbs_certificate = &certificate.cert.tbs_certificate;
|
||||
if let Ok(algorithm_id) = pivutil::get_algorithm_id_by_certificate(certificate) {
|
||||
@@ -61,7 +58,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
let algorithm = iff!(algorithm_id == AlgorithmId::EccP256, EcdsaAlgorithm::P256, EcdsaAlgorithm::P384);
|
||||
match ecdsautil::ecdsaverify(algorithm, pk_point, &hash_bytes, &signature) {
|
||||
match ecdsautil::ecdsa_verify(algorithm, pk_point, &hash_bytes, &signature) {
|
||||
Ok(_) => {
|
||||
success!("Verify ECDSA succeed.");
|
||||
if json_output {
|
||||
@@ -100,22 +97,8 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_key(slot_id: &SlotId) -> XResult<Option<Key>> {
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
match Key::list(&mut yk) {
|
||||
Err(e) => warning!("List keys failed: {}", e),
|
||||
Ok(keys) => for k in keys {
|
||||
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
|
||||
if slot_equals(slot_id, &slot_str) {
|
||||
return Ok(Some(k));
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use openssl::rsa::Rsa;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use rust_util::util_msg::MessageType;
|
||||
|
||||
use crate::{cmdutil, util};
|
||||
use crate::util::{read_stdin, try_decode};
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -25,12 +25,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
|
||||
.arg(Arg::with_name("padding").long("padding").takes_value(true)
|
||||
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let pri_key_in = opt_value_result!(sub_arg_matches.value_of("pri-key-in"), "Require private key in");
|
||||
let pri_key_bytes = opt_result!(std::fs::read(pri_key_in), "Read file: {}, failed: {}", pri_key_in);
|
||||
@@ -39,8 +38,6 @@ impl Command for CommandImpl {
|
||||
let padding = crate::rsautil::parse_padding(padding_opt)?;
|
||||
let padding_str = crate::rsautil::padding_to_string(padding);
|
||||
|
||||
let mut json = BTreeMap::new();
|
||||
|
||||
let keypair = opt_result!(Rsa::private_key_from_pem(&pri_key_bytes), "Parse RSA failed: {}");
|
||||
let keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey failed: {}");
|
||||
|
||||
@@ -81,13 +78,12 @@ impl Command for CommandImpl {
|
||||
success!("Message HEX: {}", hex::encode(&data));
|
||||
success!("Message: {}", String::from_utf8_lossy(&data));
|
||||
if json_output {
|
||||
let mut json = BTreeMap::new();
|
||||
json.insert("data", hex::encode(&data));
|
||||
json.insert("padding", padding_str.to_string());
|
||||
json.insert("encrypted", encrypted_hex);
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -6,9 +6,8 @@ use openssl::encrypt::Encrypter;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::Rsa;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
|
||||
use crate::digest::sha256_bytes;
|
||||
use crate::{cmdutil, util};
|
||||
use crate::digestutil::sha256_bytes;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -23,12 +22,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("data-hex").long("data-hex").takes_value(true).help("Data in HEX"))
|
||||
.arg(Arg::with_name("padding").long("padding").takes_value(true)
|
||||
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let pub_key_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in");
|
||||
let pub_key_bytes = opt_result!(fs::read(pub_key_in), "Read file: {}, failed: {}", pub_key_in);
|
||||
@@ -37,8 +35,6 @@ impl Command for CommandImpl {
|
||||
let padding = crate::rsautil::parse_padding(padding_opt)?;
|
||||
let padding_str = crate::rsautil::padding_to_string(padding);
|
||||
|
||||
let mut json = BTreeMap::new();
|
||||
|
||||
let keypair = opt_result!(Rsa::public_key_from_pem(&pub_key_bytes), "Parse RSA failed: {}");
|
||||
let pub_key_der = opt_result!(keypair.public_key_to_der(), "RSA public key to der failed: {}");
|
||||
let pub_key_fingerprint = hex::encode(sha256_bytes(&pub_key_der));
|
||||
@@ -66,14 +62,13 @@ impl Command for CommandImpl {
|
||||
information!("Public key fingerprint: {}", pub_key_fingerprint);
|
||||
success!("Encrypted message: {}", encrypted_hex);
|
||||
if json_output {
|
||||
let mut json = BTreeMap::new();
|
||||
json.insert("data", hex::encode(&data));
|
||||
json.insert("public_key_fingerprint", pub_key_fingerprint);
|
||||
json.insert("padding", padding_str.to_string());
|
||||
json.insert("encrypted", encrypted_hex);
|
||||
}
|
||||
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -12,7 +12,7 @@ use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg::MessageType;
|
||||
use rust_util::{util_msg, XResult};
|
||||
|
||||
use crate::digest::sha256_bytes;
|
||||
use crate::digestutil::sha256_bytes;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::seutil;
|
||||
use crate::{cmdutil, seutil, util};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -10,15 +11,28 @@ impl Command for CommandImpl {
|
||||
}
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("Secure Enclave subcommand")
|
||||
// .arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
SubCommand::with_name(self.name())
|
||||
.about("Secure Enclave subcommand")
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, _sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
if seutil::is_support_se() {
|
||||
success!("Secure Enclave is supported.")
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
if which::which("swift-secure-enclave-tool").is_err() {
|
||||
failure!("Secure Enclave tool not found.");
|
||||
}
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::new();
|
||||
json.insert("se_supported", seutil::is_support_se());
|
||||
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
failure!("Secure Enclave is NOT supported.")
|
||||
success!(
|
||||
"Secure Enclave is {}supported.",
|
||||
iff!(seutil::is_support_se(), "", "NOT ")
|
||||
);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::keyutil::{parse_key_uri, KeyUri};
|
||||
use crate::seutil;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use crate::keyutil::parse_key_uri;
|
||||
use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use p256::elliptic_curve::sec1::FromEncodedPoint;
|
||||
use p256::{EncodedPoint, PublicKey};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use spki::EncodePublicKey;
|
||||
use std::collections::BTreeMap;
|
||||
use rust_util::XResult;
|
||||
use crate::util::base64_decode;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -18,39 +19,45 @@ impl Command for CommandImpl {
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name())
|
||||
.about("Secure Enclave ECDH subcommand")
|
||||
.arg(
|
||||
Arg::with_name("key")
|
||||
.long("key")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.help("Key uri"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("epk")
|
||||
.long("epk")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.help("E-Public key"),
|
||||
)
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_key_uri_arg())
|
||||
.arg(cmdutil::build_epk_arg())
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
if !seutil::is_support_se() {
|
||||
return simple_error!("Secure Enclave is NOT supported.");
|
||||
}
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
seutil::check_se_supported()?;
|
||||
let key = sub_arg_matches.value_of("key").unwrap();
|
||||
let epk = sub_arg_matches.value_of("epk").unwrap();
|
||||
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output {
|
||||
util_msg::set_logger_std_out(false);
|
||||
}
|
||||
|
||||
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
|
||||
let key_uri = parse_key_uri(key)?;
|
||||
let se_key_uri = key_uri.as_secure_enclave_key()?;
|
||||
debugging!("Secure enclave key URI: {:?}", se_key_uri);
|
||||
|
||||
let ephemeral_public_key_der_bytes;
|
||||
let ephemeral_public_key_der_bytes = parse_epk(epk)?;
|
||||
|
||||
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
|
||||
let dh = seutil::secure_enclave_p256_dh(
|
||||
&private_key,
|
||||
&ephemeral_public_key_der_bytes,
|
||||
)?;
|
||||
let dh_hex = hex::encode(&dh);
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("shared_secret_hex", dh_hex);
|
||||
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
information!("Shared secret: {}", dh_hex);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_epk(epk: &str) -> XResult<Vec<u8>> {
|
||||
if epk.starts_with("04") {
|
||||
let ephemeral_public_key_point_bytes = opt_result!(
|
||||
hex::decode(epk),
|
||||
@@ -65,27 +72,14 @@ impl Command for CommandImpl {
|
||||
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();
|
||||
Ok(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: {}");
|
||||
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)
|
||||
}
|
||||
|
||||
let dh = seutil::secure_enclave_p256_dh(
|
||||
&se_key_uri.private_key,
|
||||
&ephemeral_public_key_der_bytes,
|
||||
)?;
|
||||
let dh_hex = hex::encode(&dh);
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("shared_secret_hex", dh_hex);
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
} else {
|
||||
information!("Shared secret: {}", dh_hex);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::keyutil::{parse_key_uri, KeyUri};
|
||||
use crate::seutil;
|
||||
use crate::keyutil::parse_key_uri;
|
||||
use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
|
||||
use crate::util::{base64_decode, base64_encode};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use std::collections::BTreeMap;
|
||||
use swift_secure_enclave_tool_rs::DigestType;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -16,13 +16,7 @@ impl Command for CommandImpl {
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name())
|
||||
.about("Secure Enclave EC sign subcommand")
|
||||
.arg(
|
||||
Arg::with_name("key")
|
||||
.long("key")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.help("Key uri"),
|
||||
)
|
||||
.arg(cmdutil::build_key_uri_arg())
|
||||
.arg(
|
||||
Arg::with_name("input")
|
||||
.short("i")
|
||||
@@ -36,13 +30,13 @@ impl Command for CommandImpl {
|
||||
.takes_value(true)
|
||||
.help("Input in base64"),
|
||||
)
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
if !seutil::is_support_se() {
|
||||
return simple_error!("Secure Enclave is NOT supported.");
|
||||
}
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
seutil::check_se_supported()?;
|
||||
let key = sub_arg_matches.value_of("key").unwrap();
|
||||
let input_bytes = match sub_arg_matches.value_of("input") {
|
||||
None => match sub_arg_matches.value_of("input-base64") {
|
||||
@@ -51,22 +45,20 @@ impl Command for CommandImpl {
|
||||
},
|
||||
Some(input) => input.as_bytes().to_vec(),
|
||||
};
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output {
|
||||
util_msg::set_logger_std_out(false);
|
||||
}
|
||||
|
||||
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
|
||||
let key_uri = parse_key_uri(key)?;
|
||||
let se_key_uri = key_uri.as_secure_enclave_key()?;
|
||||
debugging!("Secure enclave key URI: {:?}", se_key_uri);
|
||||
|
||||
let signature = seutil::secure_enclave_p256_sign(&se_key_uri.private_key, &input_bytes)?;
|
||||
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
|
||||
let signature = seutil::secure_enclave_p256_sign(&private_key, &input_bytes, DigestType::Raw)?;
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("signature_base64", base64_encode(&signature));
|
||||
json.insert("signature_hex", hex::encode(&signature));
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
success!("Signature: {}", base64_encode(&signature));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::cmd_hmac_encrypt;
|
||||
use crate::pkiutil::bytes_to_pem;
|
||||
use crate::seutil;
|
||||
use crate::util::base64_encode;
|
||||
use crate::{cmdutil, seutil, util};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use p256::PublicKey;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use spki::DecodePublicKey;
|
||||
use std::collections::BTreeMap;
|
||||
use swift_secure_enclave_tool_rs::{ControlFlag, KeyMlKem};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -30,50 +33,105 @@ impl Command for CommandImpl {
|
||||
.takes_value(true)
|
||||
.help("Host name"),
|
||||
)
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(
|
||||
Arg::with_name("control-flag")
|
||||
.long("control-flag")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.help("Control flag, e.g. none, user-presence, device-passcode, biometry-any, biometry-current-set"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("algorithm")
|
||||
.long("algorithm")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.default_value("p256")
|
||||
.help("Algorithm, e.g. p256, mlkem768, mlkem1024"),
|
||||
)
|
||||
.arg(cmdutil::build_with_hmac_encrypt_arg())
|
||||
.arg(cmdutil::build_with_pbe_encrypt_arg())
|
||||
.arg(cmdutil::build_double_pin_check_arg())
|
||||
.arg(cmdutil::build_pbe_iteration_arg())
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
if !seutil::is_support_se() {
|
||||
return simple_error!("Secure Enclave is NOT supported.");
|
||||
}
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
seutil::check_se_supported()?;
|
||||
let ty = sub_arg_matches.value_of("type").unwrap();
|
||||
let host = sub_arg_matches.value_of("host").unwrap_or("macbook");
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output {
|
||||
util_msg::set_logger_std_out(false);
|
||||
}
|
||||
|
||||
let sign = match ty {
|
||||
"signing" | "ecsign" | "sign" => true,
|
||||
"key_agreement" | "ecdh" | "dh" => false,
|
||||
_ => return simple_error!("Invalid type: {}", ty),
|
||||
};
|
||||
let control_flag = sub_arg_matches.value_of("control-flag").unwrap();
|
||||
let control_flag = match control_flag {
|
||||
"none" => ControlFlag::None,
|
||||
"user-presence" | "up" => ControlFlag::UserPresence,
|
||||
"device-passcode" | "passcode" | "pass" => ControlFlag::DevicePasscode,
|
||||
"biometry-any" | "bio-any" => ControlFlag::BiometryAny,
|
||||
"biometry-current-set" | "bio-current" => ControlFlag::BiometryCurrentSet,
|
||||
_ => return simple_error!("Invalid control flag: {}", control_flag),
|
||||
};
|
||||
let algorithm = sub_arg_matches.value_of("algorithm").unwrap();
|
||||
let (public_key_point, public_key_der, private_key) = match algorithm {
|
||||
"p256" => {
|
||||
seutil::generate_secure_enclave_p256_keypair(sign, control_flag)?
|
||||
}
|
||||
"mlkem768" | "mlkem1024" => {
|
||||
if sign {
|
||||
return simple_error!("Algorithm: {} only supports key_agreement", algorithm);
|
||||
}
|
||||
seutil::generate_secure_enclave_mlkem_keypair(
|
||||
iff!(algorithm == "mlkem768", KeyMlKem::MlKem768, KeyMlKem::MlKem1024), control_flag)?
|
||||
}
|
||||
_ => return simple_error!("Unknown algorithm: {}", algorithm),
|
||||
};
|
||||
|
||||
let (public_key_point, public_key_der, private_key) =
|
||||
seutil::generate_secure_enclave_p256_keypair(sign)?;
|
||||
|
||||
let public_key_point_hex = hex::encode(&public_key_point);
|
||||
let public_key_pem = bytes_to_pem("PUBLIC KEY", &*public_key_der);
|
||||
let key = format!(
|
||||
"key://{}:se/p256:{}:{}",
|
||||
let private_key = cmd_hmac_encrypt::do_encrypt(&private_key, &mut None, sub_arg_matches)?;
|
||||
let key_uri = format!(
|
||||
"key://{}:se/{}:{}:{}",
|
||||
host,
|
||||
algorithm,
|
||||
iff!(sign, "signing", "key_agreement"),
|
||||
private_key,
|
||||
);
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("public_key_point", public_key_point_hex);
|
||||
json.insert("public_key_pem", base64_encode(&*public_key_der));
|
||||
json.insert("key", key);
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
} else {
|
||||
success!("Public key(point): {}", public_key_point_hex);
|
||||
success!("Public key PEM: \n{}", public_key_pem);
|
||||
success!("Key: {}", key);
|
||||
}
|
||||
|
||||
print_se_key(json_output, &public_key_point, &public_key_der, &key_uri);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_se_key(
|
||||
json_output: bool,
|
||||
public_key_point: &[u8],
|
||||
public_key_der: &[u8],
|
||||
key_uri: &str,
|
||||
) {
|
||||
let public_key_point_hex = hex::encode(public_key_point);
|
||||
let public_key_pem = bytes_to_pem("PUBLIC KEY", public_key_der);
|
||||
let public_key = PublicKey::from_public_key_pem(&public_key_pem).ok();
|
||||
let public_key_jwk = public_key.map(|key| key.to_jwk_string());
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("public_key_point", public_key_point_hex);
|
||||
json.insert("public_key_pem", base64_encode(public_key_der));
|
||||
if let Some(public_key_jwk) = public_key_jwk {
|
||||
json.insert("public_key_jwk", base64_encode(public_key_jwk));
|
||||
}
|
||||
json.insert("key_uri", key_uri.to_string());
|
||||
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
success!("Public key(point): {}", public_key_point_hex);
|
||||
success!("Public key PEM: \n{}", public_key_pem);
|
||||
if let Some(public_key_jwk) = public_key_jwk {
|
||||
success!("Public key JWK: \n{}", &public_key_jwk);
|
||||
}
|
||||
success!("Key: {}", key_uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage};
|
||||
use crate::pkiutil::bytes_to_pem;
|
||||
use crate::seutil;
|
||||
use crate::util::base64_encode;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use crate::cmd_se_generate::print_se_key;
|
||||
use crate::keyutil::{parse_key_uri, KeyUsage};
|
||||
use crate::{cmd_hmac_decrypt, cmdutil, seutil};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -17,50 +14,27 @@ impl Command for CommandImpl {
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name())
|
||||
.about("Secure Enclave recover subcommand")
|
||||
.arg(
|
||||
Arg::with_name("key")
|
||||
.long("key")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.help("Key uri"),
|
||||
)
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_key_uri_arg())
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
if !seutil::is_support_se() {
|
||||
return simple_error!("Secure Enclave is NOT supported.");
|
||||
}
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
seutil::check_se_supported()?;
|
||||
let key = sub_arg_matches.value_of("key").unwrap();
|
||||
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output {
|
||||
util_msg::set_logger_std_out(false);
|
||||
}
|
||||
|
||||
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?;
|
||||
let key_uri = parse_key_uri(key)?;
|
||||
let se_key_uri = key_uri.as_secure_enclave_key()?;
|
||||
debugging!("Secure enclave key URI: {:?}", se_key_uri);
|
||||
|
||||
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
|
||||
let (public_key_point, public_key_der, _private_key) =
|
||||
seutil::recover_secure_enclave_p256_public_key(
|
||||
&se_key_uri.private_key,
|
||||
&private_key,
|
||||
se_key_uri.usage == KeyUsage::Singing,
|
||||
)?;
|
||||
|
||||
let public_key_point_hex = hex::encode(&public_key_point);
|
||||
let public_key_pem = bytes_to_pem("PUBLIC KEY", &*public_key_der);
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
json.insert("public_key_point", public_key_point_hex);
|
||||
json.insert("public_key_pem", base64_encode(&*public_key_der));
|
||||
json.insert("key", key.to_string());
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
} else {
|
||||
success!("Public key(point): {}", public_key_point_hex);
|
||||
success!("Public key PEM: \n{}", public_key_pem);
|
||||
success!("Key: {}", key);
|
||||
}
|
||||
print_se_key(json_output, &public_key_point, &public_key_der, key);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
85
src/cmd_sign_jwt.rs
Normal file
85
src/cmd_sign_jwt.rs
Normal 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
302
src/cmd_sign_jwt_piv.rs
Normal 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
74
src/cmd_sign_jwt_se.rs
Normal 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
109
src/cmd_sign_jwt_soft.rs
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
209
src/cmd_ssh_agent.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use ssh_agent::proto::{from_bytes, RsaPublicKey, signature, Signature, to_bytes}
|
||||
use ssh_agent::proto::message::{self, Message};
|
||||
use ssh_agent::proto::public_key::PublicKey;
|
||||
|
||||
use crate::digest::{copy_sha256, copy_sha512};
|
||||
use crate::digestutil::{copy_sha256, copy_sha512};
|
||||
use crate::pinutil;
|
||||
use crate::sshutil::{generate_ssh_string, with_sign};
|
||||
|
||||
@@ -139,10 +139,10 @@ impl Agent for SshAgent {
|
||||
pub struct CommandImpl;
|
||||
|
||||
impl Command for CommandImpl {
|
||||
fn name(&self) -> &str { "ssh-agent" }
|
||||
fn name(&self) -> &str { "ssh-agent-gpg" }
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("SSH-Agent subcommand")
|
||||
SubCommand::with_name(self.name()).about("SSH-Agent OpenPGP card subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").default_value("123456").help("OpenPGP card user pin"))
|
||||
.arg(Arg::with_name("pgp").long("pgp").help("Use PGP"))
|
||||
.arg(Arg::with_name("pgp-sign").long("pgp-sign").help("Use PGP sign"))
|
||||
@@ -1,7 +1,7 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use sshcerts::ssh::PublicKeyKind;
|
||||
use sshcerts::{Certificate, PrivateKey, PublicKey};
|
||||
use sshcerts_hatter_fork::ssh::PublicKeyKind;
|
||||
use sshcerts_hatter_fork::{Certificate, PrivateKey, PublicKey};
|
||||
use std::fs;
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -3,19 +3,19 @@ use ecdsa::elliptic_curve::pkcs8::der::Encode;
|
||||
use rand::random;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::{util_time, XResult};
|
||||
use sshcerts::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner};
|
||||
use sshcerts::utils::format_signature_for_ssh;
|
||||
use sshcerts::x509::extract_ssh_pubkey_from_x509_certificate;
|
||||
use sshcerts::{CertType, Certificate, PublicKey};
|
||||
use sshcerts_hatter_fork::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner};
|
||||
use sshcerts_hatter_fork::utils::format_signature_for_ssh;
|
||||
use sshcerts_hatter_fork::x509::extract_ssh_pubkey_from_x509_certificate;
|
||||
use sshcerts_hatter_fork::{CertType, Certificate, PublicKey};
|
||||
use std::fs;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
use yubikey::piv::{sign_data, AlgorithmId, SlotId};
|
||||
use yubikey::{Key, YubiKey};
|
||||
|
||||
use crate::digest::{sha256_bytes, sha384_bytes};
|
||||
use crate::digestutil::{sha256_bytes, sha384_bytes};
|
||||
use crate::pivutil::slot_equals;
|
||||
use crate::{pinutil, pivutil, util};
|
||||
use crate::{cmdutil, pinutil, pivutil, util};
|
||||
|
||||
pub struct CommandImpl;
|
||||
|
||||
@@ -27,9 +27,9 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("SSH PIV sign cert subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("key-id").short("k").long("key-id").takes_value(true).default_value("default_key_id").help("SSH user CA key id"))
|
||||
.arg(Arg::with_name("principal").short("P").long("principal").takes_value(true).default_value("root").multiple(true).help("SSH user CA principal"))
|
||||
.arg(Arg::with_name("pub").short("f").long("pub").alias("pub-file").required(true).takes_value(true).help("SSH public key file"))
|
||||
@@ -6,7 +6,7 @@ use rust_util::util_msg;
|
||||
use yubikey::{Key, YubiKey};
|
||||
use yubikey::piv::{AlgorithmId, sign_data};
|
||||
|
||||
use crate::{pinutil, pivutil, util};
|
||||
use crate::{cmdutil, pinutil, pivutil, util};
|
||||
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
|
||||
use crate::sshutil::SshVecWriter;
|
||||
|
||||
@@ -18,9 +18,9 @@ impl Command for CommandImpl {
|
||||
|
||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||
SubCommand::with_name(self.name()).about("SSH piv sign subcommand")
|
||||
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN"))
|
||||
.arg(Arg::with_name("no-pin").long("no-pin").help("No PIN"))
|
||||
.arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"))
|
||||
.arg(cmdutil::build_slot_arg())
|
||||
.arg(cmdutil::build_pin_arg())
|
||||
.arg(cmdutil::build_no_pin_arg())
|
||||
.arg(Arg::with_name("namespace").short("n").long("namespace").takes_value(true).help("Namespace"))
|
||||
.arg(Arg::with_name("in").long("in").required(true).takes_value(true).help("In file, - for stdin"))
|
||||
}
|
||||
@@ -91,14 +91,14 @@ impl Command for CommandImpl {
|
||||
sign_message.write_string(namespace.as_bytes());
|
||||
sign_message.write_string("".as_bytes());
|
||||
sign_message.write_string("sha512".as_bytes());
|
||||
let data_digest = crate::digest::sha512_bytes(&data);
|
||||
let data_digest = crate::digestutil::sha512_bytes(&data);
|
||||
debugging!("Data digest: {} (sha512)", hex::encode(&data_digest));
|
||||
sign_message.write_string(&data_digest);
|
||||
debugging!("Singed message: {}", hex::encode(&sign_message));
|
||||
let tobe_signed_data = if ec_bit_len == 256 {
|
||||
crate::digest::sha256_bytes(&sign_message)
|
||||
crate::digestutil::sha256_bytes(&sign_message)
|
||||
} else {
|
||||
crate::digest::sha384_bytes(&sign_message)
|
||||
crate::digestutil::sha384_bytes(&sign_message)
|
||||
};
|
||||
debugging!("Digest of signed message: {}", hex::encode(&tobe_signed_data));
|
||||
|
||||
129
src/cmd_ssh_pub_key.rs
Normal file
129
src/cmd_ssh_pub_key.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,12 @@ use openssl::hash::MessageDigest;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::sign::Verifier;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::util_msg;
|
||||
use x509_parser::certificate::X509Certificate;
|
||||
use x509_parser::prelude::FromDer;
|
||||
|
||||
use crate::digest;
|
||||
use crate::fido;
|
||||
use crate::fido::{U2fRegistrationData, U2fV2Challenge};
|
||||
use crate::{cmdutil, digestutil, util};
|
||||
use crate::fidoutil;
|
||||
use crate::fidoutil::{U2fRegistrationData, U2fV2Challenge};
|
||||
use crate::util::base64_encode;
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -29,12 +28,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("timeout").short("t").long("timeout").default_value("30").help("Timeout in seconds"))
|
||||
.arg(Arg::with_name("challenge").long("challenge").takes_value(true).help("Challenge HEX"))
|
||||
.arg(Arg::with_name("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
|
||||
Ok(t) => (t * 1000) as u64,
|
||||
@@ -48,12 +46,12 @@ impl Command for CommandImpl {
|
||||
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
|
||||
let u2fv2_challenge_str = u2fv2_challenge.to_json();
|
||||
|
||||
let app_id_hash = digest::sha256(app_id);
|
||||
let challenge_hash = digest::sha256(&u2fv2_challenge_str);
|
||||
let app_id_hash = digestutil::sha256(app_id);
|
||||
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
|
||||
|
||||
let flags = RegisterFlags::empty();
|
||||
|
||||
let status_tx = fido::start_status_updater();
|
||||
let status_tx = fidoutil::start_status_updater();
|
||||
|
||||
let (register_tx, register_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
@@ -98,8 +96,8 @@ impl Command for CommandImpl {
|
||||
+ u2f_registration_data.attestation_cert.as_ref().map(|c| c.len()).unwrap_or(0);
|
||||
let sign = ®ister_result.0[sign_prefix_len..];
|
||||
|
||||
let mut json = BTreeMap::new();
|
||||
if json_output {
|
||||
let mut json = BTreeMap::new();
|
||||
// println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
|
||||
if let Some(device_name) = u2f_registration_data.device_name {
|
||||
json.insert("device_name", device_name);
|
||||
@@ -117,6 +115,8 @@ impl Command for CommandImpl {
|
||||
json.insert("app_id_hash", hex::encode(&app_id_hash));
|
||||
json.insert("challenge", u2fv2_challenge_str);
|
||||
json.insert("challenge_hash", hex::encode(&challenge_hash));
|
||||
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
success!("Device info: {}", u2f_registration_data.device_info);
|
||||
information!("Register challenge: {}", u2fv2_challenge_str);
|
||||
@@ -149,9 +149,6 @@ impl Command for CommandImpl {
|
||||
warning!("Cannot find attestation cert!");
|
||||
}
|
||||
}
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ use openssl::pkey::PKey;
|
||||
use openssl::sign::Verifier;
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
|
||||
use crate::digest;
|
||||
use crate::fido;
|
||||
use crate::fido::U2fV2Challenge;
|
||||
use crate::{cmdutil, digestutil, util};
|
||||
use crate::fidoutil;
|
||||
use crate::fidoutil::U2fV2Challenge;
|
||||
use crate::util::base64_encode;
|
||||
|
||||
pub struct CommandImpl;
|
||||
@@ -31,12 +31,11 @@ impl Command for CommandImpl {
|
||||
.arg(Arg::with_name("challenge").long("challenge").takes_value(true).help("Challenge HEX"))
|
||||
.arg(Arg::with_name("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
|
||||
.arg(Arg::with_name("key-handle").short("k").long("key-handle").takes_value(true).multiple(true).help("Key handle"))
|
||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||
.arg(cmdutil::build_json_arg())
|
||||
}
|
||||
|
||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
let json_output = sub_arg_matches.is_present("json");
|
||||
if json_output { rust_util::util_msg::set_logger_std_out(false); }
|
||||
let json_output = cmdutil::check_json_output(sub_arg_matches);
|
||||
|
||||
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
|
||||
Ok(t) => (t * 1000) as u64,
|
||||
@@ -72,10 +71,10 @@ impl Command for CommandImpl {
|
||||
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
|
||||
let u2fv2_challenge_str = u2fv2_challenge.to_json();
|
||||
|
||||
let app_id_hash = digest::sha256(app_id);
|
||||
let challenge_hash = digest::sha256(&u2fv2_challenge_str);
|
||||
let app_id_hash = digestutil::sha256(app_id);
|
||||
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
|
||||
|
||||
let status_tx = fido::start_status_updater();
|
||||
let status_tx = fidoutil::start_status_updater();
|
||||
|
||||
information!("App id: {}, Start sign...", app_id);
|
||||
debugging!("Wait timeout: {} ms", timeout_ms);
|
||||
@@ -163,7 +162,7 @@ impl Command for CommandImpl {
|
||||
}
|
||||
}
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
util::print_pretty_json(&json);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
44
src/cmd_yubikey.rs
Normal file
44
src/cmd_yubikey.rs
Normal 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
82
src/cmdutil.rs
Normal 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
|
||||
}
|
||||
@@ -1,6 +1,25 @@
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256, Sha384, Sha512};
|
||||
|
||||
pub enum DigestAlgorithm {
|
||||
Sha256,
|
||||
#[allow(dead_code)]
|
||||
Sha384,
|
||||
}
|
||||
|
||||
impl DigestAlgorithm {
|
||||
pub fn digest(&self, data: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
DigestAlgorithm::Sha256 => sha256_bytes(data),
|
||||
DigestAlgorithm::Sha384 => sha384_bytes(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn digest_str(&self, s: &str) -> Vec<u8> {
|
||||
self.digest(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sha256(input: &str) -> Vec<u8> {
|
||||
sha256_bytes(input.as_bytes())
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
macro_rules! piv_ecdh {
|
||||
($p_algo: tt, $public_key_pem_opt: expr, $sub_arg_matches: expr, $json: expr, $json_output: expr) => ({
|
||||
use $p_algo::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
|
||||
use $p_algo::ecdh::EphemeralSecret;
|
||||
use $p_algo::{EncodedPoint, PublicKey};
|
||||
use $p_algo::{EncodedPoint, PublicKey, ecdh::EphemeralSecret};
|
||||
let public_key;
|
||||
if let Some(public_key_pem) = $public_key_pem_opt {
|
||||
public_key = opt_result!(public_key_pem.parse::<PublicKey>(), "Parse public key failed: {}");
|
||||
@@ -35,4 +34,30 @@ macro_rules! piv_ecdh {
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! parse_private_and_ecdh {
|
||||
($algo: tt, $private_key_bytes: tt, $ephemeral_public_key_bytes: tt) => ({
|
||||
use $algo::{SecretKey, PublicKey, ecdh::diffie_hellman, pkcs8::DecodePrivateKey};
|
||||
use spki::DecodePublicKey;
|
||||
let secret_key= SecretKey::from_pkcs8_der($private_key_bytes)?;
|
||||
let public_key = opt_result!(PublicKey::from_public_key_der(
|
||||
$ephemeral_public_key_bytes),"Parse ephemeral public key failed: {}");
|
||||
|
||||
let shared_secret = diffie_hellman(secret_key.to_nonzero_scalar(), public_key.as_affine());
|
||||
Ok(shared_secret.raw_secret_bytes().to_vec())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_p256_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
parse_private_and_ecdh!(p256, private_key_bytes, ephemeral_public_key_bytes)
|
||||
}
|
||||
|
||||
pub fn parse_p384_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
parse_private_and_ecdh!(p384, private_key_bytes, ephemeral_public_key_bytes)
|
||||
}
|
||||
|
||||
pub fn parse_p521_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
parse_private_and_ecdh!(p521, private_key_bytes, ephemeral_public_key_bytes)
|
||||
}
|
||||
|
||||
use rust_util::XResult;
|
||||
pub(crate) use piv_ecdh;
|
||||
171
src/ecdsautil.rs
171
src/ecdsautil.rs
@@ -5,6 +5,7 @@ use p256::NistP256;
|
||||
use p256::ecdsa::signature::hazmat::PrehashVerifier;
|
||||
use p384::NistP384;
|
||||
use p256::pkcs8::EncodePrivateKey;
|
||||
use p521::NistP521;
|
||||
use rust_util::XResult;
|
||||
use spki::EncodePublicKey;
|
||||
use crate::util::{base64_encode, try_decode};
|
||||
@@ -13,6 +14,12 @@ use crate::util::{base64_encode, try_decode};
|
||||
pub enum EcdsaAlgorithm {
|
||||
P256,
|
||||
P384,
|
||||
P521,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum EcdsaSignType {
|
||||
Der, Rs,
|
||||
}
|
||||
|
||||
pub fn parse_ecdsa_to_rs(signature_der: &[u8]) -> XResult<Vec<u8>> {
|
||||
@@ -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>)> {
|
||||
let mut vec_r: Vec<u8> = Vec::new();
|
||||
let mut vec_s: Vec<u8> = Vec::new();
|
||||
let vec_r: Vec<u8>;
|
||||
let vec_s: Vec<u8>;
|
||||
let (_, parsed_signature) = opt_result!(der_parser::parse_der(signature_der), "Parse signature failed: {}");
|
||||
match parsed_signature.content {
|
||||
BerObjectContent::Sequence(seq) => {
|
||||
match &seq[0].content {
|
||||
BerObjectContent::Integer(r) => {
|
||||
debugging!("Signature r: {}", hex::encode(r));
|
||||
if r.len() == ((256 / 8) + 1) || r.len() == ((384 / 8) + 1) {
|
||||
vec_r = r[1..].to_vec();
|
||||
} else {
|
||||
vec_r = r.to_vec();
|
||||
}
|
||||
vec_r = trim_ecdsa_point_coord(r);
|
||||
}
|
||||
_ => return simple_error!("Parse signature failed: [0]not integer"),
|
||||
}
|
||||
match &seq[1].content {
|
||||
BerObjectContent::Integer(s) => {
|
||||
debugging!("Signature s: {}", hex::encode(s));
|
||||
if s.len() == ((256 / 8) + 1) || s.len() == ((384 / 8) + 1) {
|
||||
vec_s = s[1..].to_vec();
|
||||
} else {
|
||||
vec_s = s.to_vec();
|
||||
}
|
||||
vec_s = trim_ecdsa_point_coord(s);
|
||||
}
|
||||
_ => return simple_error!("Parse signature failed: [1]not integer"),
|
||||
}
|
||||
@@ -55,27 +54,112 @@ pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)>
|
||||
Ok((vec_r, vec_s))
|
||||
}
|
||||
|
||||
pub fn generate_p256_keypair() -> XResult<(String, String, String)> {
|
||||
let secret_key = p256::SecretKey::random(&mut rand::thread_rng());
|
||||
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes());
|
||||
let secret_key_pem = secret_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
|
||||
let public_key_pem = secret_key.public_key().to_public_key_pem(LineEnding::LF)?;
|
||||
Ok((secret_key_der_base64, secret_key_pem, public_key_pem))
|
||||
const P256_LEN: usize = 32;
|
||||
const P384_LEN: usize = 48;
|
||||
const P521_LEN: usize = 66;
|
||||
|
||||
fn trim_ecdsa_point_coord(p: &[u8]) -> Vec<u8> {
|
||||
if p.len() == (P256_LEN + 1) || p.len() == (P384_LEN + 1) || p.len() == (P521_LEN + 1) {
|
||||
p[1..].to_vec()
|
||||
} else if p.len() == (P256_LEN - 1) || p.len() == (P384_LEN - 1) || p.len() == (P521_LEN - 1) {
|
||||
let mut v = vec![];
|
||||
v.push(0_u8);
|
||||
v.extend_from_slice(p);
|
||||
v
|
||||
} else {
|
||||
p.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_p384_keypair() -> XResult<(String, String, String)> {
|
||||
let secret_key = p384::SecretKey::random(&mut rand::thread_rng());
|
||||
|
||||
macro_rules! generate_inner_ecdsa_keypair {
|
||||
($algo: tt) => ({
|
||||
use $algo::SecretKey;
|
||||
|
||||
let secret_key = SecretKey::random(&mut rand::thread_rng());
|
||||
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes());
|
||||
let secret_key_pem = secret_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
|
||||
let public_key_pem = secret_key.public_key().to_public_key_pem(LineEnding::LF)?;
|
||||
Ok((secret_key_der_base64, secret_key_pem, public_key_pem))
|
||||
let public_key_der = secret_key.public_key().to_public_key_der()?.to_vec();
|
||||
let jwk_ec_key = secret_key.public_key().to_jwk().to_string();
|
||||
Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_ecdsa_keypair(algo: EcdsaAlgorithm) -> XResult<(String, String, String, Vec<u8>, String)> {
|
||||
match algo {
|
||||
EcdsaAlgorithm::P256 => generate_inner_ecdsa_keypair!(p256),
|
||||
EcdsaAlgorithm::P384 => generate_inner_ecdsa_keypair!(p384),
|
||||
EcdsaAlgorithm::P521 => generate_inner_ecdsa_keypair!(p521),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_ec_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
match parse_p521_public_key_to_point(public_key_bytes) {
|
||||
Ok(point) => Ok(point),
|
||||
Err(_) => match parse_p384_public_key_to_point(public_key_bytes) {
|
||||
Ok(point) => Ok(point),
|
||||
Err(_) => parse_p256_public_key_to_point(public_key_bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_p256_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
use p256::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
|
||||
use spki::DecodePublicKey;
|
||||
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
|
||||
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub fn parse_p384_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
use p384::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
|
||||
use spki::DecodePublicKey;
|
||||
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
|
||||
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub fn parse_p521_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
use p521::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
|
||||
use spki::DecodePublicKey;
|
||||
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
|
||||
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
|
||||
}
|
||||
|
||||
macro_rules! parse_ecdsa_private_key_to_public_key {
|
||||
($algo: tt, $parse_ecdsa_private_key: tt) => ({
|
||||
use $algo::{SecretKey, pkcs8::DecodePrivateKey};
|
||||
|
||||
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
|
||||
Ok(secret_key) => secret_key,
|
||||
Err(_) => match try_decode($parse_ecdsa_private_key) {
|
||||
Ok(private_key_der) => match SecretKey::from_pkcs8_der(&private_key_der) {
|
||||
Ok(secret_key) => secret_key,
|
||||
Err(e) => return simple_error!("Invalid PKCS#8 private key {}, error: {}", $parse_ecdsa_private_key, e),
|
||||
}
|
||||
Err(_) => return simple_error!("Invalid PKCS#8 private key: {}", $parse_ecdsa_private_key),
|
||||
}
|
||||
};
|
||||
let public_key_document = opt_result!(secret_key.public_key().to_public_key_der(), "Conver to public key failed: {}");
|
||||
Ok(public_key_document.to_vec())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_p256_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
|
||||
parse_ecdsa_private_key_to_public_key!(p256, private_key_pkcs8)
|
||||
}
|
||||
|
||||
pub fn parse_p384_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
|
||||
parse_ecdsa_private_key_to_public_key!(p384, private_key_pkcs8)
|
||||
}
|
||||
|
||||
pub fn parse_p521_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
|
||||
parse_ecdsa_private_key_to_public_key!(p521, private_key_pkcs8)
|
||||
}
|
||||
|
||||
|
||||
macro_rules! parse_ecdsa_private_key {
|
||||
($algo: tt, $parse_ecdsa_private_key: tt) => ({
|
||||
use $algo::ecdsa::{SigningKey};
|
||||
use $algo::pkcs8::DecodePrivateKey;
|
||||
use $algo::SecretKey;
|
||||
use $algo::{SecretKey, pkcs8::DecodePrivateKey};
|
||||
|
||||
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
|
||||
Ok(secret_key) => secret_key,
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
pub fn sign_p256_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult<Vec<u8>> {
|
||||
use p256::ecdsa::{SigningKey, Signature};
|
||||
use p256::ecdsa::signature::hazmat::PrehashSigner;
|
||||
|
||||
let signing_key = SigningKey::from_slice(&private_key_d)?;
|
||||
let signature: Signature = signing_key.sign_prehash(pre_hash)?;
|
||||
|
||||
Ok(signature.to_bytes().to_vec())
|
||||
pub fn parse_p521_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
|
||||
parse_ecdsa_private_key!(p521, private_key_pkcs8)
|
||||
}
|
||||
|
||||
pub fn sign_p384_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult<Vec<u8>> {
|
||||
use p384::ecdsa::{SigningKey, Signature};
|
||||
use p384::ecdsa::signature::hazmat::PrehashSigner;
|
||||
|
||||
let signing_key = SigningKey::from_slice(&private_key_d)?;
|
||||
let signature: Signature = signing_key.sign_prehash(pre_hash)?;
|
||||
macro_rules! sign_ecdsa_rs_or_der {
|
||||
($algo: tt, $private_key_d: tt, $pre_hash: tt, $is_rs: tt) => ({
|
||||
use $algo::ecdsa::{SigningKey, Signature, signature::hazmat::PrehashSigner};
|
||||
|
||||
let signing_key = SigningKey::from_slice($private_key_d)?;
|
||||
let signature: Signature = signing_key.sign_prehash($pre_hash)?;
|
||||
|
||||
if $is_rs {
|
||||
Ok(signature.to_bytes().to_vec())
|
||||
} else {
|
||||
Ok(signature.to_der().as_bytes().to_vec())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ecdsa_sign(algo: EcdsaAlgorithm, private_key_d: &[u8], pre_hash: &[u8], sign_type: EcdsaSignType) -> XResult<Vec<u8>> {
|
||||
let is_rs = sign_type == EcdsaSignType::Rs;
|
||||
match algo {
|
||||
EcdsaAlgorithm::P256 => sign_ecdsa_rs_or_der!(p256, private_key_d, pre_hash, is_rs),
|
||||
EcdsaAlgorithm::P384 => sign_ecdsa_rs_or_der!(p384, private_key_d, pre_hash, is_rs),
|
||||
EcdsaAlgorithm::P521 => sign_ecdsa_rs_or_der!(p521, private_key_d, pre_hash, is_rs),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
macro_rules! ecdsa_verify_signature {
|
||||
($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({
|
||||
use ecdsa::Signature;
|
||||
@@ -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 {
|
||||
EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature),
|
||||
EcdsaAlgorithm::P384 => ecdsa_verify_signature!(NistP384, pk_point, prehash, signature),
|
||||
EcdsaAlgorithm::P521 => ecdsa_verify_signature!(NistP521, pk_point, prehash, signature),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
66
src/ecutil.rs
Normal file
66
src/ecutil.rs
Normal 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(),
|
||||
))
|
||||
}
|
||||
@@ -4,14 +4,12 @@ use std::thread;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use authenticator::{RegisterResult, StatusUpdate};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use rand::Rng;
|
||||
use rust_util::XResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::pkiutil::bytes_to_pem;
|
||||
use crate::util::base64_encode;
|
||||
use crate::util::{base64_encode, base64_encode_url_safe_no_pad};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct U2FDeviceInfo {
|
||||
@@ -103,7 +101,7 @@ impl U2fV2Challenge {
|
||||
None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix),
|
||||
Some(challenge_hex) => {
|
||||
let challenge_bytes = opt_result!(hex::decode(challenge_hex), "Decode challenge hex failed: {}");
|
||||
let challenge = URL_SAFE_NO_PAD.encode(challenge_bytes);
|
||||
let challenge = base64_encode_url_safe_no_pad(challenge_bytes);
|
||||
U2fV2Challenge::new(challenge, app_id)
|
||||
}
|
||||
})
|
||||
@@ -122,7 +120,7 @@ impl U2fV2Challenge {
|
||||
rand_bytes[..8].clone_from_slice(×tamp_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)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ use yubico_manager::config::{Config, Mode, Slot};
|
||||
use yubico_manager::hmacmode::HmacKey;
|
||||
use yubico_manager::sec::hmac_sha1;
|
||||
use yubico_manager::Yubico;
|
||||
use crate::digest::{copy_sha256, sha256, sha256_bytes};
|
||||
use crate::util::{base64_decode, base64_encode};
|
||||
use crate::digestutil::{copy_sha256, sha256_bytes};
|
||||
use crate::util;
|
||||
use crate::util::{base64_decode, base64_encode, base64_encode_url_safe_no_pad, base64_uri_decode};
|
||||
|
||||
const HMAC_ENC_PREFIX: &str = "hmac_enc:";
|
||||
|
||||
@@ -19,7 +20,7 @@ pub fn hmac_encrypt_from_string(plaintext: &str) -> XResult<String> {
|
||||
}
|
||||
|
||||
pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
|
||||
let hmac_nonce: [u8; 8] = random();
|
||||
let hmac_nonce: [u8; 16] = random();
|
||||
let aes_gcm_nonce: [u8; 16] = random();
|
||||
|
||||
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
|
||||
@@ -33,8 +34,8 @@ pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
|
||||
|
||||
Ok(format!("{}{}:{}:{}",
|
||||
HMAC_ENC_PREFIX,
|
||||
hex::encode(&hmac_nonce),
|
||||
hex::encode(&aes_gcm_nonce),
|
||||
base64_encode_url_safe_no_pad(hmac_nonce),
|
||||
base64_encode_url_safe_no_pad(aes_gcm_nonce),
|
||||
base64_encode(&ciphertext)
|
||||
))
|
||||
}
|
||||
@@ -43,14 +44,6 @@ pub fn is_hmac_encrypted(ciphertext: &str) -> bool {
|
||||
ciphertext.starts_with(HMAC_ENC_PREFIX)
|
||||
}
|
||||
|
||||
pub fn try_hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
|
||||
if is_hmac_encrypted(ciphertext) {
|
||||
hmac_decrypt_to_string(ciphertext)
|
||||
} else {
|
||||
Ok(ciphertext.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
|
||||
let plaintext = hmac_decrypt(ciphertext)?;
|
||||
Ok(String::from_utf8(plaintext)?)
|
||||
@@ -61,8 +54,8 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
|
||||
return simple_error!("Invalid ciphertext: {}", ciphertext);
|
||||
}
|
||||
let parts = ciphertext.split(":").collect::<Vec<_>>();
|
||||
let hmac_nonce = hex::decode(parts[1])?;
|
||||
let aes_gcm_nonce = hex::decode(parts[2])?;
|
||||
let hmac_nonce = try_decode_hmac_val(parts[1])?;
|
||||
let aes_gcm_nonce = try_decode_hmac_val(parts[2])?;
|
||||
let ciphertext = base64_decode(parts[3])?;
|
||||
|
||||
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
|
||||
@@ -76,6 +69,16 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
pub fn try_decode_hmac_val(s: &str) -> XResult<Vec<u8>> {
|
||||
match hex::decode(s) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => match base64_uri_decode(s) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(_) => simple_error!("Try decode failed: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
let mut yubi = Yubico::new();
|
||||
let device = match yubi.find_yubikey() {
|
||||
@@ -92,7 +95,7 @@ pub fn compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
|
||||
.set_mode(Mode::Sha1)
|
||||
.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())
|
||||
}
|
||||
|
||||
@@ -128,10 +131,10 @@ pub fn output_hmac_result(sub_arg_matches: &ArgMatches, json_output: bool, chall
|
||||
let sha512_output = sub_arg_matches.is_present("sha512");
|
||||
|
||||
let hex_string = hex::encode(result);
|
||||
let hex_sha1 = iff!(sha1_output, Some(crate::digest::sha1_bytes(result)), None);
|
||||
let hex_sha256 = iff!(sha256_output, Some(crate::digest::sha256_bytes(result)), None);
|
||||
let hex_sha384 = iff!(sha384_output, Some(crate::digest::sha384_bytes(result)), None);
|
||||
let hex_sha512 = iff!(sha512_output, Some(crate::digest::sha512_bytes(result)), None);
|
||||
let hex_sha1 = iff!(sha1_output, Some(crate::digestutil::sha1_bytes(result)), None);
|
||||
let hex_sha256 = iff!(sha256_output, Some(crate::digestutil::sha256_bytes(result)), None);
|
||||
let hex_sha384 = iff!(sha384_output, Some(crate::digestutil::sha384_bytes(result)), None);
|
||||
let hex_sha512 = iff!(sha512_output, Some(crate::digestutil::sha512_bytes(result)), None);
|
||||
|
||||
if json_output {
|
||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||
@@ -142,7 +145,7 @@ pub fn output_hmac_result(sub_arg_matches: &ArgMatches, json_output: bool, chall
|
||||
hex_sha384.map(|hex_sha384| json.insert("response_sha384_hex", hex::encode(hex_sha384)));
|
||||
hex_sha512.map(|hex_sha512| json.insert("response_sha512_hex", hex::encode(hex_sha512)));
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json).expect("Convert to JSON failed!"));
|
||||
util::print_pretty_json(&json);
|
||||
} else {
|
||||
success!("Challenge HEX: {}", hex::encode(challenge_bytes));
|
||||
success!("Response HEX: {}", hex_string);
|
||||
|
||||
168
src/keychain.rs
Normal file
168
src/keychain.rs
Normal 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
|
||||
}
|
||||
395
src/keyutil.rs
395
src/keyutil.rs
@@ -1,29 +1,233 @@
|
||||
use crate::pivutil::{FromStr, ToStr};
|
||||
use jwt::AlgorithmType;
|
||||
use percent_encoding::NON_ALPHANUMERIC;
|
||||
use regex::Regex;
|
||||
use rust_util::XResult;
|
||||
use yubikey::piv::{AlgorithmId, SlotId};
|
||||
|
||||
// reference: https://git.hatter.ink/hatter/card-cli/issues/6
|
||||
#[derive(Debug)]
|
||||
pub enum KeyUri {
|
||||
SecureEnclaveKey(SecureEnclaveKey),
|
||||
SecureEnclave(SecureEnclaveKey),
|
||||
YubikeyPiv(YubikeyPivKey),
|
||||
YubikeyHmacEncSoft(YubikeyHmacEncSoftKey),
|
||||
ExternalCommand(ExternalCommandKey),
|
||||
}
|
||||
|
||||
// #[derive(Debug, PartialEq, Eq)]
|
||||
// pub enum KeyModule {
|
||||
// SecureEnclave,
|
||||
// OpenPgpCard,
|
||||
// PersonalIdentityVerification,
|
||||
// }
|
||||
//
|
||||
// impl KeyModule {
|
||||
// pub fn from(module: &str) -> Option<Self> {
|
||||
// match module {
|
||||
// "se" => Some(Self::SecureEnclave),
|
||||
// "pgp" => Some(Self::OpenPgpCard),
|
||||
// "piv" => Some(Self::PersonalIdentityVerification),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl KeyUri {
|
||||
pub fn as_secure_enclave_key(&self) -> XResult<&SecureEnclaveKey> {
|
||||
match self {
|
||||
KeyUri::SecureEnclave(key) => Ok(key),
|
||||
_ => simple_error!("Not a secure enclave key."),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_preferred_algorithm_type(&self) -> AlgorithmType {
|
||||
let algorithm_id = match &self {
|
||||
KeyUri::SecureEnclave(_) => return AlgorithmType::Es256,
|
||||
KeyUri::YubikeyPiv(key) => key.algorithm,
|
||||
KeyUri::YubikeyHmacEncSoft(key) => key.algorithm,
|
||||
KeyUri::ExternalCommand(key) => key.algorithm,
|
||||
};
|
||||
match algorithm_id {
|
||||
KeyAlgorithmId::Rsa1024
|
||||
| KeyAlgorithmId::Rsa2048
|
||||
| KeyAlgorithmId::Rsa3072
|
||||
| KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256,
|
||||
KeyAlgorithmId::EccP256 => AlgorithmType::Es256,
|
||||
KeyAlgorithmId::EccP384 => AlgorithmType::Es384,
|
||||
KeyAlgorithmId::EccP521 => AlgorithmType::Es512,
|
||||
// ML-KEM not supports JWS
|
||||
KeyAlgorithmId::MlKem512
|
||||
| KeyAlgorithmId::MlKem768
|
||||
| KeyAlgorithmId::MlKem1024 => AlgorithmType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::to_string_trait_impl)]
|
||||
impl ToString for KeyUri {
|
||||
fn to_string(&self) -> String {
|
||||
let mut key_uri = String::with_capacity(64);
|
||||
key_uri.push_str("key://");
|
||||
match self {
|
||||
// key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)
|
||||
// key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)
|
||||
KeyUri::SecureEnclave(key) => {
|
||||
key_uri.push_str(&key.host);
|
||||
key_uri.push_str(":se/p256:");
|
||||
key_uri.push_str(&key.usage.to_string());
|
||||
key_uri.push(':');
|
||||
key_uri.push_str(&key.private_key);
|
||||
}
|
||||
// key://yubikey-5n:piv/p256::9a
|
||||
KeyUri::YubikeyPiv(key) => {
|
||||
key_uri.push_str(&key.key_name);
|
||||
key_uri.push_str(":piv/");
|
||||
key_uri.push_str(key.algorithm.to_str());
|
||||
key_uri.push_str("::");
|
||||
key_uri.push_str(key.slot.to_str());
|
||||
}
|
||||
// key://-:soft/p256::hmac_enc:...
|
||||
KeyUri::YubikeyHmacEncSoft(key) => {
|
||||
key_uri.push_str(&key.key_name);
|
||||
key_uri.push_str(":soft/");
|
||||
key_uri.push_str(key.algorithm.to_str());
|
||||
key_uri.push_str("::");
|
||||
key_uri.push_str(key.hmac_enc_private_key.as_str());
|
||||
}
|
||||
// key://external-command-file-name:external_command/p256::parameter
|
||||
KeyUri::ExternalCommand(key) => {
|
||||
let encoded_external_command =
|
||||
percent_encoding::utf8_percent_encode(&key.external_command, NON_ALPHANUMERIC)
|
||||
.to_string();
|
||||
key_uri.push_str(&encoded_external_command);
|
||||
key_uri.push_str(":external_command/");
|
||||
key_uri.push_str(key.algorithm.to_str());
|
||||
key_uri.push_str("::");
|
||||
key_uri.push_str(&key.parameter);
|
||||
}
|
||||
}
|
||||
key_uri
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum KeyAlgorithmId {
|
||||
Rsa1024,
|
||||
Rsa2048,
|
||||
Rsa3072,
|
||||
Rsa4096,
|
||||
EccP256,
|
||||
EccP384,
|
||||
EccP521,
|
||||
MlKem512,
|
||||
MlKem768,
|
||||
MlKem1024,
|
||||
}
|
||||
|
||||
impl KeyAlgorithmId {
|
||||
pub fn from_algorithm_id(algorithm_id: AlgorithmId) -> Self {
|
||||
match algorithm_id {
|
||||
AlgorithmId::Rsa1024 => Self::Rsa1024,
|
||||
AlgorithmId::Rsa2048 => Self::Rsa2048,
|
||||
AlgorithmId::EccP256 => Self::EccP256,
|
||||
AlgorithmId::EccP384 => Self::EccP384,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_algorithm_id(self) -> Option<AlgorithmId> {
|
||||
match self {
|
||||
KeyAlgorithmId::Rsa1024 => Some(AlgorithmId::Rsa1024),
|
||||
KeyAlgorithmId::Rsa2048 => Some(AlgorithmId::Rsa2048),
|
||||
KeyAlgorithmId::Rsa3072 => None,
|
||||
KeyAlgorithmId::Rsa4096 => None,
|
||||
KeyAlgorithmId::EccP256 => Some(AlgorithmId::EccP256),
|
||||
KeyAlgorithmId::EccP384 => Some(AlgorithmId::EccP384),
|
||||
KeyAlgorithmId::EccP521 => None,
|
||||
KeyAlgorithmId::MlKem512 => None,
|
||||
KeyAlgorithmId::MlKem768 => None,
|
||||
KeyAlgorithmId::MlKem1024 => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rsa(&self) -> bool {
|
||||
match self {
|
||||
KeyAlgorithmId::Rsa1024
|
||||
| KeyAlgorithmId::Rsa2048
|
||||
| KeyAlgorithmId::Rsa3072
|
||||
| KeyAlgorithmId::Rsa4096 => true,
|
||||
KeyAlgorithmId::EccP256
|
||||
| KeyAlgorithmId::EccP384
|
||||
| KeyAlgorithmId::EccP521
|
||||
| KeyAlgorithmId::MlKem512
|
||||
| KeyAlgorithmId::MlKem768
|
||||
| KeyAlgorithmId::MlKem1024 => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ecc(&self) -> bool {
|
||||
match self {
|
||||
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => true,
|
||||
KeyAlgorithmId::Rsa1024
|
||||
| KeyAlgorithmId::Rsa2048
|
||||
| KeyAlgorithmId::Rsa3072
|
||||
| KeyAlgorithmId::Rsa4096
|
||||
| KeyAlgorithmId::MlKem512
|
||||
| KeyAlgorithmId::MlKem768
|
||||
| KeyAlgorithmId::MlKem1024 => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn is_mlkem(&self) -> bool {
|
||||
match self {
|
||||
| KeyAlgorithmId::MlKem512
|
||||
| KeyAlgorithmId::MlKem768
|
||||
| KeyAlgorithmId::MlKem1024 => true,
|
||||
KeyAlgorithmId::EccP256
|
||||
| KeyAlgorithmId::EccP384
|
||||
| KeyAlgorithmId::EccP521
|
||||
| KeyAlgorithmId::Rsa1024
|
||||
| KeyAlgorithmId::Rsa2048
|
||||
| KeyAlgorithmId::Rsa3072
|
||||
| KeyAlgorithmId::Rsa4096 => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_jwa_name(&self) -> &str {
|
||||
match self {
|
||||
KeyAlgorithmId::Rsa1024
|
||||
| KeyAlgorithmId::Rsa2048
|
||||
| KeyAlgorithmId::Rsa3072
|
||||
| KeyAlgorithmId::Rsa4096 => "RS256",
|
||||
KeyAlgorithmId::EccP256 => "ES256,",
|
||||
KeyAlgorithmId::EccP384 => "ES384",
|
||||
KeyAlgorithmId::EccP521 => "ES512",
|
||||
KeyAlgorithmId::MlKem512
|
||||
| KeyAlgorithmId::MlKem768
|
||||
| KeyAlgorithmId::MlKem1024 => "__UNKNOWN__",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for KeyAlgorithmId {
|
||||
fn from_str(s: &str) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match s {
|
||||
"rsa1024" => Some(KeyAlgorithmId::Rsa1024),
|
||||
"rsa2048" => Some(KeyAlgorithmId::Rsa2048),
|
||||
"rsa3072" => Some(KeyAlgorithmId::Rsa3072),
|
||||
"rsa4096" => Some(KeyAlgorithmId::Rsa4096),
|
||||
"p256" => Some(KeyAlgorithmId::EccP256),
|
||||
"p384" => Some(KeyAlgorithmId::EccP384),
|
||||
"p521" => Some(KeyAlgorithmId::EccP521),
|
||||
"mlkem512" => Some(KeyAlgorithmId::MlKem512),
|
||||
"mlkem768" => Some(KeyAlgorithmId::MlKem768),
|
||||
"mlkem1024" => Some(KeyAlgorithmId::MlKem1024),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStr for KeyAlgorithmId {
|
||||
fn to_str(&self) -> &str {
|
||||
match self {
|
||||
KeyAlgorithmId::Rsa1024 => "rsa1024",
|
||||
KeyAlgorithmId::Rsa2048 => "rsa2048",
|
||||
KeyAlgorithmId::Rsa3072 => "rsa3072",
|
||||
KeyAlgorithmId::Rsa4096 => "rsa4096",
|
||||
KeyAlgorithmId::EccP256 => "p256",
|
||||
KeyAlgorithmId::EccP384 => "p384",
|
||||
KeyAlgorithmId::EccP521 => "p521",
|
||||
KeyAlgorithmId::MlKem512 => "mlkem512",
|
||||
KeyAlgorithmId::MlKem768 => "mlkem768",
|
||||
KeyAlgorithmId::MlKem1024 => "mlkem1024",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum KeyUsage {
|
||||
@@ -43,7 +247,18 @@ impl KeyUsage {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::to_string_trait_impl)]
|
||||
impl ToString for KeyUsage {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
KeyUsage::Any => "*",
|
||||
KeyUsage::Singing => "signing",
|
||||
KeyUsage::KeyAgreement => "key_agreement",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SecureEnclaveKey {
|
||||
pub host: String,
|
||||
@@ -51,21 +266,41 @@ pub struct SecureEnclaveKey {
|
||||
pub private_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct YubikeyPivKey {
|
||||
pub key_name: String,
|
||||
pub algorithm: KeyAlgorithmId,
|
||||
pub slot: SlotId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct YubikeyHmacEncSoftKey {
|
||||
pub key_name: String,
|
||||
pub algorithm: KeyAlgorithmId,
|
||||
pub hmac_enc_private_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExternalCommandKey {
|
||||
pub external_command: String,
|
||||
pub algorithm: KeyAlgorithmId,
|
||||
pub parameter: String,
|
||||
}
|
||||
|
||||
pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
|
||||
let regex = Regex::new(r##"^key://([a-zA-Z\-\._]*):(\w+)/(\w+):(\w+)?:(.*)$"##).unwrap();
|
||||
let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap();
|
||||
let captures = match regex.captures(key_uri) {
|
||||
None => return simple_error!("Invalid key uri: {}", key_uri),
|
||||
Some(captures) => captures,
|
||||
};
|
||||
let host = captures.get(1).unwrap().as_str();
|
||||
let host_or_name = captures.get(1).unwrap().as_str();
|
||||
let module = captures.get(2).unwrap().as_str();
|
||||
let algorithm = captures.get(3).unwrap().as_str();
|
||||
let usage = captures.get(4).unwrap().as_str();
|
||||
let left_part = captures.get(5).unwrap().as_str();
|
||||
|
||||
if "se" != module {
|
||||
return simple_error!("Key uri's module must be se.");
|
||||
}
|
||||
match module {
|
||||
"se" => {
|
||||
if "p256" != algorithm {
|
||||
return simple_error!("Key uri's algorithm must be p256.");
|
||||
}
|
||||
@@ -75,40 +310,136 @@ pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
|
||||
}
|
||||
Some(key_usage) => key_usage,
|
||||
};
|
||||
|
||||
let parsed_key_uri = KeyUri::SecureEnclaveKey(SecureEnclaveKey {
|
||||
host: host.to_string(),
|
||||
let parsed_key_uri = KeyUri::SecureEnclave(SecureEnclaveKey {
|
||||
host: host_or_name.to_string(),
|
||||
usage: key_usage,
|
||||
private_key: left_part.to_string(),
|
||||
});
|
||||
|
||||
debugging!("Parsed key uri: {:?}", parsed_key_uri);
|
||||
Ok(parsed_key_uri)
|
||||
}
|
||||
"piv" => {
|
||||
if !usage.is_empty() {
|
||||
return simple_error!("Key uri's usage must be empty.");
|
||||
}
|
||||
let algorithm = opt_value_result!(
|
||||
KeyAlgorithmId::from_str(algorithm),
|
||||
"Invalid algorithm id: {}",
|
||||
algorithm
|
||||
);
|
||||
let slot = opt_value_result!(
|
||||
SlotId::from_str(left_part),
|
||||
"Invalid slot id: {}",
|
||||
left_part
|
||||
);
|
||||
let parsed_key_uri = KeyUri::YubikeyPiv(YubikeyPivKey {
|
||||
key_name: host_or_name.to_string(),
|
||||
algorithm,
|
||||
slot,
|
||||
});
|
||||
debugging!("Parsed key uri: {:?}", parsed_key_uri);
|
||||
Ok(parsed_key_uri)
|
||||
}
|
||||
"soft" => {
|
||||
if !usage.is_empty() {
|
||||
return simple_error!("Key uri's usage must be empty.");
|
||||
}
|
||||
let algorithm = opt_value_result!(
|
||||
KeyAlgorithmId::from_str(algorithm),
|
||||
"Invalid algorithm id: {}",
|
||||
algorithm
|
||||
);
|
||||
let hmac_enc_private_key = left_part.to_string();
|
||||
let parsed_key_uri = KeyUri::YubikeyHmacEncSoft(YubikeyHmacEncSoftKey {
|
||||
key_name: host_or_name.to_string(),
|
||||
algorithm,
|
||||
hmac_enc_private_key,
|
||||
});
|
||||
debugging!("Parsed key uri: {:?}", parsed_key_uri);
|
||||
Ok(parsed_key_uri)
|
||||
}
|
||||
"external_command" => {
|
||||
if !usage.is_empty() {
|
||||
return simple_error!("Key uri's usage must be empty.");
|
||||
}
|
||||
let external_command = opt_result!(
|
||||
percent_encoding::percent_decode_str(host_or_name).decode_utf8(),
|
||||
"Decode external command failed: {}"
|
||||
);
|
||||
let algorithm = opt_value_result!(
|
||||
KeyAlgorithmId::from_str(algorithm),
|
||||
"Invalid algorithm id: {}",
|
||||
algorithm
|
||||
);
|
||||
let parameter = left_part.to_string();
|
||||
let parsed_key_uri = KeyUri::ExternalCommand(ExternalCommandKey {
|
||||
external_command: external_command.to_string(),
|
||||
algorithm,
|
||||
parameter,
|
||||
});
|
||||
debugging!("Parsed key uri: {:?}", parsed_key_uri);
|
||||
Ok(parsed_key_uri)
|
||||
}
|
||||
_ => simple_error!("Key uri's module must be se."),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_key_uri_01() {
|
||||
let se_key_uri =
|
||||
parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap();
|
||||
assert_eq!(
|
||||
"key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)",
|
||||
se_key_uri.to_string()
|
||||
);
|
||||
match se_key_uri {
|
||||
KeyUri::SecureEnclaveKey(se_key_uri) => {
|
||||
KeyUri::SecureEnclave(se_key_uri) => {
|
||||
assert_eq!("hatter-mac-pro", se_key_uri.host);
|
||||
assert_eq!(KeyUsage::Singing, se_key_uri.usage);
|
||||
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
|
||||
}
|
||||
_ => {
|
||||
panic!("Key uri not parsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_key_uri_02() {
|
||||
let se_key_uri =
|
||||
parse_key_uri("key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)")
|
||||
parse_key_uri("key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
"key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)",
|
||||
se_key_uri.to_string()
|
||||
);
|
||||
match se_key_uri {
|
||||
KeyUri::SecureEnclaveKey(se_key_uri) => {
|
||||
assert_eq!("hatter-mac-pro", se_key_uri.host);
|
||||
KeyUri::SecureEnclave(se_key_uri) => {
|
||||
assert_eq!("hatter-mac-m1", se_key_uri.host);
|
||||
assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage);
|
||||
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
|
||||
}
|
||||
_ => {
|
||||
panic!("Key uri not parsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_key_uri_03() {
|
||||
let se_key_uri = parse_key_uri("key://yubikey-5n:piv/p256::9a").unwrap();
|
||||
assert_eq!(
|
||||
"key://yubikey-5n:piv/p256::authentication",
|
||||
se_key_uri.to_string()
|
||||
);
|
||||
match se_key_uri {
|
||||
KeyUri::YubikeyPiv(piv_key_uri) => {
|
||||
assert_eq!("yubikey-5n", piv_key_uri.key_name);
|
||||
assert_eq!(KeyAlgorithmId::EccP256, piv_key_uri.algorithm);
|
||||
assert_eq!(SlotId::Authentication, piv_key_uri.slot);
|
||||
}
|
||||
_ => {
|
||||
panic!("Key uri not parsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
190
src/main.rs
190
src/main.rs
@@ -6,73 +6,90 @@ use rust_util::util_clap::{Command, CommandError};
|
||||
|
||||
mod argsutil;
|
||||
mod cmd_chall;
|
||||
mod cmd_challconfig;
|
||||
mod cmd_ecverify;
|
||||
mod cmd_chall_config;
|
||||
mod cmd_convert_jwk_to_pem;
|
||||
mod cmd_convert_pem_to_jwk;
|
||||
mod cmd_ec_verify;
|
||||
mod cmd_external_ecdh;
|
||||
mod cmd_external_public_key;
|
||||
mod cmd_external_sign;
|
||||
mod cmd_external_spec;
|
||||
mod cmd_file_sign;
|
||||
mod cmd_file_verify;
|
||||
mod cmd_hmac_decrypt;
|
||||
mod cmd_hmac_encrypt;
|
||||
mod cmd_hmac_sha1;
|
||||
mod cmd_hmacencrypt;
|
||||
mod cmd_hmacdecrypt;
|
||||
mod cmd_keypair_generate;
|
||||
mod cmd_keypair_keychain_export;
|
||||
mod cmd_keypair_keychain_import;
|
||||
mod cmd_list;
|
||||
mod cmd_parseecdsasignature;
|
||||
#[cfg(feature = "with-sequoia-openpgp")]
|
||||
mod cmd_pgp;
|
||||
mod cmd_pgpageaddress;
|
||||
mod cmd_pgpcardadmin;
|
||||
mod cmd_pgpcarddecrypt;
|
||||
mod cmd_pgpcardlist;
|
||||
mod cmd_pgp_age_address;
|
||||
mod cmd_pgp_card_admin;
|
||||
mod cmd_pgp_card_decrypt;
|
||||
mod cmd_pgp_card_list;
|
||||
#[cfg(feature = "with-sequoia-openpgp")]
|
||||
mod cmd_pgpcardmake;
|
||||
mod cmd_pgpcardsign;
|
||||
mod cmd_pgp_card_make;
|
||||
mod cmd_pgp_card_sign;
|
||||
mod cmd_piv;
|
||||
mod cmd_pivdecrypt;
|
||||
mod cmd_pivecdh;
|
||||
mod cmd_pivecsign;
|
||||
mod cmd_pivgenerate;
|
||||
mod cmd_pivmeta;
|
||||
mod cmd_pivrsasign;
|
||||
mod cmd_pivsummary;
|
||||
mod cmd_pivverify;
|
||||
mod cmd_rsadecrypt;
|
||||
mod cmd_rsaencrypt;
|
||||
mod cmd_rsaverify;
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
mod cmd_piv_decrypt;
|
||||
mod cmd_piv_ecdh;
|
||||
mod cmd_piv_ecsign;
|
||||
mod cmd_piv_generate;
|
||||
mod cmd_piv_meta;
|
||||
mod cmd_piv_rsasign;
|
||||
mod cmd_piv_summary;
|
||||
mod cmd_piv_verify;
|
||||
mod cmd_rsa_decrypt;
|
||||
mod cmd_rsa_encrypt;
|
||||
mod cmd_rsa_verify;
|
||||
mod cmd_se;
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
mod cmd_se_ecdh;
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
mod cmd_se_ecsign;
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
mod cmd_se_generate;
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
mod cmd_se_recover;
|
||||
mod cmd_signfile;
|
||||
mod cmd_signjwt;
|
||||
mod cmd_signjwtsoft;
|
||||
mod cmd_sshagent;
|
||||
mod cmd_sshparse;
|
||||
mod cmd_sshparsesign;
|
||||
mod cmd_sshpivcert;
|
||||
mod cmd_sshpivsign;
|
||||
mod cmd_sshpubkey;
|
||||
mod cmd_u2fregister;
|
||||
mod cmd_u2fsign;
|
||||
mod cmd_verifyfile;
|
||||
mod cmd_parseecdsasignature;
|
||||
mod cmd_generatekeypair;
|
||||
mod digest;
|
||||
mod cmd_sign_jwt;
|
||||
mod cmd_sign_jwt_piv;
|
||||
mod cmd_sign_jwt_se;
|
||||
mod cmd_sign_jwt_soft;
|
||||
mod cmd_ssh_agent;
|
||||
mod cmd_ssh_agent_gpg;
|
||||
mod cmd_ssh_parse;
|
||||
mod cmd_ssh_parse_sign;
|
||||
mod cmd_ssh_piv_cert;
|
||||
mod cmd_ssh_piv_sign;
|
||||
mod cmd_ssh_pub_key;
|
||||
mod cmd_u2f_register;
|
||||
mod cmd_u2f_sign;
|
||||
mod cmdutil;
|
||||
mod digestutil;
|
||||
mod ecdhutil;
|
||||
mod ecdsautil;
|
||||
mod fido;
|
||||
mod ecutil;
|
||||
mod fidoutil;
|
||||
mod hmacutil;
|
||||
mod keychain;
|
||||
mod keyutil;
|
||||
mod pbeutil;
|
||||
mod pgpcardutil;
|
||||
mod pinutil;
|
||||
mod pivutil;
|
||||
mod pkiutil;
|
||||
mod rsautil;
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
mod seutil;
|
||||
mod signfile;
|
||||
mod sshutil;
|
||||
mod util;
|
||||
mod yubikeyutil;
|
||||
mod cmd_yubikey;
|
||||
mod mlkemutil;
|
||||
|
||||
use zeroizing_alloc::ZeroAlloc;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: ZeroAlloc<std::alloc::System> = ZeroAlloc(std::alloc::System);
|
||||
|
||||
pub struct DefaultCommandImpl;
|
||||
|
||||
@@ -103,55 +120,62 @@ fn inner_main() -> CommandError {
|
||||
Box::new(cmd_list::CommandImpl),
|
||||
Box::new(cmd_chall::CommandImpl),
|
||||
Box::new(cmd_hmac_sha1::CommandImpl),
|
||||
Box::new(cmd_hmacencrypt::CommandImpl),
|
||||
Box::new(cmd_hmacdecrypt::CommandImpl),
|
||||
Box::new(cmd_challconfig::CommandImpl),
|
||||
Box::new(cmd_rsaencrypt::CommandImpl),
|
||||
Box::new(cmd_rsadecrypt::CommandImpl),
|
||||
Box::new(cmd_rsaverify::CommandImpl),
|
||||
Box::new(cmd_hmac_encrypt::CommandImpl),
|
||||
Box::new(cmd_hmac_decrypt::CommandImpl),
|
||||
Box::new(cmd_chall_config::CommandImpl),
|
||||
Box::new(cmd_rsa_encrypt::CommandImpl),
|
||||
Box::new(cmd_rsa_decrypt::CommandImpl),
|
||||
Box::new(cmd_rsa_verify::CommandImpl),
|
||||
#[cfg(feature = "with-sequoia-openpgp")]
|
||||
Box::new(cmd_pgp::CommandImpl),
|
||||
Box::new(cmd_pgpcardadmin::CommandImpl),
|
||||
Box::new(cmd_pgpcardlist::CommandImpl),
|
||||
Box::new(cmd_pgpcardsign::CommandImpl),
|
||||
Box::new(cmd_pgpcarddecrypt::CommandImpl),
|
||||
Box::new(cmd_pgp_card_admin::CommandImpl),
|
||||
Box::new(cmd_pgp_card_list::CommandImpl),
|
||||
Box::new(cmd_pgp_card_sign::CommandImpl),
|
||||
Box::new(cmd_pgp_card_decrypt::CommandImpl),
|
||||
#[cfg(feature = "with-sequoia-openpgp")]
|
||||
Box::new(cmd_pgpcardmake::CommandImpl),
|
||||
Box::new(cmd_pgp_card_make::CommandImpl),
|
||||
Box::new(cmd_piv::CommandImpl),
|
||||
Box::new(cmd_pivsummary::CommandImpl),
|
||||
Box::new(cmd_pivmeta::CommandImpl),
|
||||
Box::new(cmd_pivverify::CommandImpl),
|
||||
Box::new(cmd_pivrsasign::CommandImpl),
|
||||
Box::new(cmd_pivecdh::CommandImpl),
|
||||
Box::new(cmd_pivecsign::CommandImpl),
|
||||
Box::new(cmd_pivdecrypt::CommandImpl),
|
||||
Box::new(cmd_pivgenerate::CommandImpl),
|
||||
Box::new(cmd_u2fregister::CommandImpl),
|
||||
Box::new(cmd_u2fsign::CommandImpl),
|
||||
Box::new(cmd_sshagent::CommandImpl),
|
||||
Box::new(cmd_sshparsesign::CommandImpl),
|
||||
Box::new(cmd_sshpivsign::CommandImpl),
|
||||
Box::new(cmd_sshpivcert::CommandImpl),
|
||||
Box::new(cmd_sshpubkey::CommandImpl),
|
||||
Box::new(cmd_sshparse::CommandImpl),
|
||||
Box::new(cmd_pgpageaddress::CommandImpl),
|
||||
Box::new(cmd_signjwt::CommandImpl),
|
||||
Box::new(cmd_signjwtsoft::CommandImpl),
|
||||
Box::new(cmd_signfile::CommandImpl),
|
||||
Box::new(cmd_verifyfile::CommandImpl),
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
Box::new(cmd_piv_summary::CommandImpl),
|
||||
Box::new(cmd_piv_meta::CommandImpl),
|
||||
Box::new(cmd_piv_verify::CommandImpl),
|
||||
Box::new(cmd_piv_rsasign::CommandImpl),
|
||||
Box::new(cmd_piv_ecdh::CommandImpl),
|
||||
Box::new(cmd_piv_ecsign::CommandImpl),
|
||||
Box::new(cmd_piv_decrypt::CommandImpl),
|
||||
Box::new(cmd_piv_generate::CommandImpl),
|
||||
Box::new(cmd_u2f_register::CommandImpl),
|
||||
Box::new(cmd_u2f_sign::CommandImpl),
|
||||
Box::new(cmd_ssh_agent::CommandImpl),
|
||||
Box::new(cmd_ssh_agent_gpg::CommandImpl),
|
||||
Box::new(cmd_ssh_parse_sign::CommandImpl),
|
||||
Box::new(cmd_ssh_piv_sign::CommandImpl),
|
||||
Box::new(cmd_ssh_piv_cert::CommandImpl),
|
||||
Box::new(cmd_ssh_pub_key::CommandImpl),
|
||||
Box::new(cmd_ssh_parse::CommandImpl),
|
||||
Box::new(cmd_pgp_age_address::CommandImpl),
|
||||
Box::new(cmd_sign_jwt_piv::CommandImpl),
|
||||
Box::new(cmd_sign_jwt_soft::CommandImpl),
|
||||
Box::new(cmd_sign_jwt_se::CommandImpl),
|
||||
Box::new(cmd_sign_jwt::CommandImpl),
|
||||
Box::new(cmd_file_sign::CommandImpl),
|
||||
Box::new(cmd_file_verify::CommandImpl),
|
||||
Box::new(cmd_se::CommandImpl),
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
Box::new(cmd_se_generate::CommandImpl),
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
Box::new(cmd_se_recover::CommandImpl),
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
Box::new(cmd_se_ecsign::CommandImpl),
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
Box::new(cmd_se_ecdh::CommandImpl),
|
||||
Box::new(cmd_ecverify::CommandImpl),
|
||||
Box::new(cmd_ec_verify::CommandImpl),
|
||||
Box::new(cmd_parseecdsasignature::CommandImpl),
|
||||
Box::new(cmd_generatekeypair::CommandImpl),
|
||||
Box::new(cmd_keypair_generate::CommandImpl),
|
||||
Box::new(cmd_keypair_keychain_import::CommandImpl),
|
||||
Box::new(cmd_keypair_keychain_export::CommandImpl),
|
||||
Box::new(cmd_convert_pem_to_jwk::CommandImpl),
|
||||
Box::new(cmd_convert_jwk_to_pem::CommandImpl),
|
||||
Box::new(cmd_external_spec::CommandImpl),
|
||||
Box::new(cmd_external_public_key::CommandImpl),
|
||||
Box::new(cmd_external_sign::CommandImpl),
|
||||
Box::new(cmd_external_ecdh::CommandImpl),
|
||||
Box::new(cmd_yubikey::CommandImpl),
|
||||
];
|
||||
|
||||
#[allow(clippy::vec_init_then_push)]
|
||||
@@ -159,8 +183,6 @@ fn inner_main() -> CommandError {
|
||||
let mut features: Vec<&str> = vec![];
|
||||
#[cfg(feature = "with-sequoia-openpgp")]
|
||||
features.push("sequoia-openpgp");
|
||||
#[cfg(feature = "with-secure-enclave")]
|
||||
features.push("secure-enclave");
|
||||
features
|
||||
};
|
||||
let about = format!(
|
||||
|
||||
241
src/mlkemutil.rs
Normal file
241
src/mlkemutil.rs
Normal 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
119
src/pbeutil.rs
Normal 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()));
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use clap::ArgMatches;
|
||||
use jwt::AlgorithmType;
|
||||
use rust_util::XResult;
|
||||
use spki::{ObjectIdentifier, SubjectPublicKeyInfoOwned};
|
||||
use spki::der::{Decode, Encode};
|
||||
@@ -56,6 +57,32 @@ pub trait ToStr {
|
||||
fn to_str(&self) -> &str;
|
||||
}
|
||||
|
||||
pub trait FromStr {
|
||||
fn from_str(s: &str) -> Option<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl ToStr for AlgorithmType {
|
||||
fn to_str(&self) -> &str {
|
||||
match self {
|
||||
AlgorithmType::Hs256 => "HS256",
|
||||
AlgorithmType::Hs384 => "HS384",
|
||||
AlgorithmType::Hs512 => "HS512",
|
||||
AlgorithmType::Rs256 => "RS256",
|
||||
AlgorithmType::Rs384 => "RS384",
|
||||
AlgorithmType::Rs512 => "RS512",
|
||||
AlgorithmType::Es256 => "ES256",
|
||||
AlgorithmType::Es384 => "ES384",
|
||||
AlgorithmType::Es512 => "ES512",
|
||||
AlgorithmType::Ps256 => "PS256",
|
||||
AlgorithmType::Ps384 => "PS384",
|
||||
AlgorithmType::Ps512 => "PS512",
|
||||
AlgorithmType::None => "NONE",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStr for PinPolicy {
|
||||
fn to_str(&self) -> &str {
|
||||
match self {
|
||||
@@ -78,6 +105,21 @@ impl ToStr for TouchPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AlgorithmId {
|
||||
fn from_str(s: &str) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match s {
|
||||
"rsa1024" => Some(AlgorithmId::Rsa1024),
|
||||
"rsa2048" => Some(AlgorithmId::Rsa2048),
|
||||
"p256" => Some(AlgorithmId::EccP256),
|
||||
"p384" => Some(AlgorithmId::EccP384),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStr for AlgorithmId {
|
||||
fn to_str(&self) -> &str {
|
||||
match self {
|
||||
@@ -169,6 +211,7 @@ pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
|
||||
"9c" | "sign" | "signature" => SlotId::Signature,
|
||||
"9d" | "keym" | "keymanagement" => SlotId::KeyManagement,
|
||||
"9e" | "card" | "cardauthentication" => SlotId::CardAuthentication,
|
||||
"f9" | "attest" | "attestation" => SlotId::Attestation,
|
||||
"r1" | "82" => SlotId::Retired(RetiredSlotId::R1),
|
||||
"r2" | "83" => SlotId::Retired(RetiredSlotId::R2),
|
||||
"r3" | "84" => SlotId::Retired(RetiredSlotId::R3),
|
||||
@@ -193,6 +236,54 @@ pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
|
||||
})
|
||||
}
|
||||
|
||||
impl FromStr for SlotId {
|
||||
fn from_str(s: &str) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
get_slot_id(s).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStr for SlotId {
|
||||
fn to_str(&self) -> &str {
|
||||
match self {
|
||||
SlotId::Authentication => "authentication",
|
||||
SlotId::Signature => "signature",
|
||||
SlotId::KeyManagement => "keymanagement",
|
||||
SlotId::CardAuthentication => "cardauthentication",
|
||||
SlotId::Retired(retried) => match retried {
|
||||
RetiredSlotId::R1 => "r1",
|
||||
RetiredSlotId::R2 => "r2",
|
||||
RetiredSlotId::R3 => "r3",
|
||||
RetiredSlotId::R4 => "r4",
|
||||
RetiredSlotId::R5 => "r5",
|
||||
RetiredSlotId::R6 => "r6",
|
||||
RetiredSlotId::R7 => "r7",
|
||||
RetiredSlotId::R8 => "r8",
|
||||
RetiredSlotId::R9 => "r9",
|
||||
RetiredSlotId::R10 => "r10",
|
||||
RetiredSlotId::R11 => "r11",
|
||||
RetiredSlotId::R12 => "r12",
|
||||
RetiredSlotId::R13 => "r13",
|
||||
RetiredSlotId::R14 => "r14",
|
||||
RetiredSlotId::R15 => "r15",
|
||||
RetiredSlotId::R16 => "r16",
|
||||
RetiredSlotId::R17 => "r17",
|
||||
RetiredSlotId::R18 => "r18",
|
||||
RetiredSlotId::R19 => "r19",
|
||||
RetiredSlotId::R20 => "r20",
|
||||
}
|
||||
SlotId::Attestation => "attestation",
|
||||
SlotId::Management(management) => match management {
|
||||
ManagementSlotId::Pin => "pin",
|
||||
ManagementSlotId::Puk => "puk",
|
||||
ManagementSlotId::Management => "management",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_read_pin(yk: &mut YubiKey, slot_id: SlotId, sub_arg_matches: &ArgMatches) -> Option<String> {
|
||||
if never_use_pin(yk, slot_id) {
|
||||
None
|
||||
|
||||
@@ -7,7 +7,7 @@ use rust_util::XResult;
|
||||
use sequoia_openpgp::crypto::mpi::PublicKey;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
|
||||
use crate::digest::sha256_bytes;
|
||||
use crate::digestutil::sha256_bytes;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PkiAlgorithm {
|
||||
|
||||
@@ -1,8 +1,65 @@
|
||||
use std::collections::HashMap;
|
||||
use ecdsa::elliptic_curve::rand_core::OsRng;
|
||||
use openssl::bn::{BigNum, BigNumContext};
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::{Padding, Rsa};
|
||||
use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use rust_util::{util_msg, XResult};
|
||||
use rust_util::util_msg::MessageType;
|
||||
use spki::DecodePublicKey;
|
||||
use rsa::pkcs1::DecodeRsaPublicKey;
|
||||
use rsa::traits::PublicKeyParts;
|
||||
use spki::EncodePublicKey;
|
||||
use rsa::pkcs1::LineEnding;
|
||||
use rsa::pkcs8::EncodePrivateKey;
|
||||
use sha2::{Sha256, Sha384, Sha512};
|
||||
use crate::digestutil;
|
||||
use crate::util::{base64_decode, base64_encode};
|
||||
|
||||
pub enum RsaSignAlgorithm {
|
||||
Rs256,
|
||||
Rs384,
|
||||
Rs512,
|
||||
}
|
||||
|
||||
impl RsaSignAlgorithm {
|
||||
pub fn from_str(alg: &str) -> Option<RsaSignAlgorithm> {
|
||||
match alg {
|
||||
"RS256" => Some(RsaSignAlgorithm::Rs256),
|
||||
"RS384" => Some(RsaSignAlgorithm::Rs384),
|
||||
"RS512" => Some(RsaSignAlgorithm::Rs512),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(rsa_private_key: &RsaPrivateKey, rsa_sign_algorithm: RsaSignAlgorithm, message: &[u8], is_raw: bool) -> XResult<Vec<u8>> {
|
||||
match rsa_sign_algorithm {
|
||||
RsaSignAlgorithm::Rs256 => {
|
||||
let raw_in = iff!(is_raw, digestutil::sha256_bytes(message), message.to_vec());
|
||||
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha256>(), &raw_in)?)
|
||||
}
|
||||
RsaSignAlgorithm::Rs384 => {
|
||||
let raw_in = iff!(is_raw, digestutil::sha384_bytes(message), message.to_vec());
|
||||
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha384>(), &raw_in)?)
|
||||
}
|
||||
RsaSignAlgorithm::Rs512 => {
|
||||
let raw_in = iff!(is_raw, digestutil::sha512_bytes(message), message.to_vec());
|
||||
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha512>(), &raw_in)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_rsa_keypair(bit_size: usize) -> XResult<(String, String, String, Vec<u8>, String)> {
|
||||
let rsa_private_key = opt_result!(RsaPrivateKey::new(&mut OsRng, bit_size), "Generate RSA private key failed: {}");
|
||||
let rsa_public_key = rsa_private_key.to_public_key();
|
||||
let secret_key_der_base64 = base64_encode(rsa_private_key.to_pkcs8_der()?.as_bytes());
|
||||
let secret_key_pem = rsa_private_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
|
||||
let public_key_pem = rsa_public_key.to_public_key_pem(LineEnding::LF)?;
|
||||
let public_key_der = rsa_public_key.to_public_key_der()?.to_vec();
|
||||
let jwk_ec_key = rsa_public_key_to_jwk(&rsa_public_key)?;
|
||||
Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RsaCrt {
|
||||
@@ -151,3 +208,43 @@ fn pkcs1_padding_for_sign(bs: &[u8], bit_len: usize) -> XResult<Vec<u8>> {
|
||||
output.extend_from_slice(bs);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn convert_rsa_to_jwk(public_key: &str) -> XResult<String> {
|
||||
let rsa_public_key = try_parse_rsa(public_key)?;
|
||||
rsa_public_key_to_jwk(&rsa_public_key)
|
||||
}
|
||||
|
||||
pub fn rsa_public_key_to_jwk(rsa_public_key: &RsaPublicKey) -> XResult<String> {
|
||||
let e_bytes = rsa_public_key.e().to_bytes_be();
|
||||
let n_bytes = rsa_public_key.n().to_bytes_be();
|
||||
|
||||
let mut jwk = HashMap::new();
|
||||
jwk.insert("kty", "RSA".to_string());
|
||||
jwk.insert("n", base64_encode(&n_bytes));
|
||||
jwk.insert("e", base64_encode(&e_bytes));
|
||||
|
||||
Ok(serde_json::to_string(&jwk).unwrap())
|
||||
}
|
||||
|
||||
pub fn try_parse_rsa(public_key: &str) -> XResult<RsaPublicKey> {
|
||||
debugging!("Try parse RSA public key PEM.");
|
||||
// parse RSA public key PEM not works? why?
|
||||
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_pem(public_key) {
|
||||
return Ok(rsa_public_key);
|
||||
}
|
||||
debugging!("Try parse RSA PKCS#1 public key PEM.");
|
||||
if let Ok(rsa_public_key) = RsaPublicKey::from_pkcs1_pem(public_key) {
|
||||
return Ok(rsa_public_key);
|
||||
}
|
||||
if let Ok(public_key_der) = base64_decode(public_key) {
|
||||
debugging!("Try parse RSA public key DER.");
|
||||
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_der(&public_key_der) {
|
||||
return Ok(rsa_public_key);
|
||||
}
|
||||
debugging!("Try parse RSA PKCS#1 public key DER.");
|
||||
if let Ok(rsa_public_key) = RsaPublicKey::from_pkcs1_der(&public_key_der) {
|
||||
return Ok(rsa_public_key);
|
||||
}
|
||||
}
|
||||
simple_error!("Invalid RSA public key.")
|
||||
}
|
||||
|
||||
146
src/seutil.rs
146
src/seutil.rs
@@ -1,113 +1,81 @@
|
||||
use crate::util::{base64_decode, base64_encode};
|
||||
use rust_util::XResult;
|
||||
use swift_rs::swift;
|
||||
use swift_rs::{Bool, SRString};
|
||||
|
||||
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);
|
||||
use se_tool::KeyPurpose;
|
||||
use swift_secure_enclave_tool_rs as se_tool;
|
||||
use swift_secure_enclave_tool_rs::{ControlFlag, DigestType, KeyMlKem};
|
||||
|
||||
pub fn is_support_se() -> bool {
|
||||
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)> {
|
||||
let p256_keypair_result = if sign {
|
||||
unsafe { generate_secure_enclave_p256_ecsign_keypair() }
|
||||
pub fn check_se_supported() -> XResult<()> {
|
||||
if !is_support_se() {
|
||||
simple_error!("Secure Enclave is NOT supported.")
|
||||
} 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(
|
||||
private_key: &str,
|
||||
sign: bool,
|
||||
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
|
||||
let p256_keypair_result = if sign {
|
||||
unsafe { recover_secure_enclave_p256_ecsign_public_key(SRString::from(private_key)) }
|
||||
let private_key_representation = base64_decode(private_key)?;
|
||||
let key_material = if sign {
|
||||
se_tool::recover_keypair(KeyPurpose::Signing, &private_key_representation)
|
||||
} else {
|
||||
unsafe { recover_secure_enclave_p256_ecdh_public_key(SRString::from(private_key)) }
|
||||
};
|
||||
parse_p256_keypair_result(p256_keypair_result.as_str())
|
||||
se_tool::recover_keypair(KeyPurpose::KeyAgreement, &private_key_representation)
|
||||
}?;
|
||||
Ok((
|
||||
key_material.public_key_point,
|
||||
key_material.public_key_der,
|
||||
base64_encode(&key_material.private_key_representation),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn secure_enclave_p256_dh(
|
||||
private_key: &str,
|
||||
ephemeral_public_key_bytes: &[u8],
|
||||
) -> XResult<Vec<u8>> {
|
||||
let dh_result = unsafe {
|
||||
compute_secure_enclave_p256_ecdh(
|
||||
SRString::from(private_key),
|
||||
SRString::from(base64_encode(ephemeral_public_key_bytes).as_str()),
|
||||
)
|
||||
};
|
||||
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
|
||||
))
|
||||
let private_key_representation = base64_decode(private_key)?;
|
||||
let shared_secret =
|
||||
se_tool::private_key_ecdh(&private_key_representation, ephemeral_public_key_bytes)?;
|
||||
Ok(shared_secret)
|
||||
}
|
||||
|
||||
pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8]) -> XResult<Vec<u8>> {
|
||||
let signature_result = unsafe {
|
||||
compute_secure_enclave_p256_ecsign(
|
||||
SRString::from(private_key),
|
||||
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))
|
||||
pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8], digest_type: DigestType) -> XResult<Vec<u8>> {
|
||||
let private_key_representation = base64_decode(private_key)?;
|
||||
let signature = se_tool::private_key_sign_digested(&private_key_representation, content, digest_type)?;
|
||||
Ok(signature)
|
||||
}
|
||||
33
src/util.rs
33
src/util.rs
@@ -1,9 +1,10 @@
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
|
||||
use base64::{DecodeError, Engine};
|
||||
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
|
||||
use base64::{DecodeError, Engine};
|
||||
use rust_util::XResult;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn base64_encode<T: AsRef<[u8]>>(input: T) -> String {
|
||||
STANDARD.encode(input)
|
||||
@@ -21,6 +22,21 @@ pub fn base64_uri_decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeErro
|
||||
URL_SAFE_NO_PAD.decode(input)
|
||||
}
|
||||
|
||||
pub fn to_pem(header: &str, bytes: &[u8]) -> String {
|
||||
let mut buf = String::new();
|
||||
buf.push_str(&format!("-----BEGIN {}-----\n", header));
|
||||
let bas64ed = base64_encode(bytes);
|
||||
let len = bas64ed.len();
|
||||
for (i, c) in bas64ed.chars().enumerate() {
|
||||
buf.push(c);
|
||||
if i > 0 && i < len && i % 64 == 0 {
|
||||
buf.push('\n');
|
||||
}
|
||||
}
|
||||
buf.push_str(&format!("\n-----END {}-----\n", header));
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
|
||||
match hex::decode(input) {
|
||||
Ok(v) => Ok(v),
|
||||
@@ -30,7 +46,7 @@ pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => simple_error!("decode hex or base64 error: {}", e),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +61,17 @@ pub fn read_file_or_stdin(file: &str) -> XResult<Vec<u8>> {
|
||||
if file == "-" {
|
||||
read_stdin()
|
||||
} else {
|
||||
Ok(opt_result!(fs::read(file), "Read file: {} failed: {}", file))
|
||||
Ok(opt_result!(
|
||||
fs::read(file),
|
||||
"Read file: {} failed: {}",
|
||||
file
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_pretty_json<T>(value: &T)
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
println!("{}", serde_json::to_string_pretty(value).unwrap());
|
||||
}
|
||||
|
||||
58
src/yubikeyutil.rs
Normal file
58
src/yubikeyutil.rs
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
]
|
||||
)
|
||||
@@ -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
417
swift-rs/Cargo.lock
generated
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
]
|
||||
)
|
||||
@@ -1,483 +0,0 @@
|
||||
# swift-rs
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
@@ -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
|
||||
}};
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
@@ -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
Reference in New Issue
Block a user