diff --git a/Cargo.lock b/Cargo.lock index ed249cd..c552681 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.11.14" +version = "1.11.15" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", @@ -535,6 +535,7 @@ dependencies = [ "reqwest", "ring 0.17.14", "rpassword", + "rsa 0.9.8", "rust_util", "secrecy", "security-framework 3.2.0", diff --git a/Cargo.toml b/Cargo.toml index ce13adc..78e3036 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.11.14" +version = "1.11.15" authors = ["Hatter Jiang "] edition = "2018" @@ -57,6 +57,7 @@ aes-gcm-stream = "0.2" swift-secure-enclave-tool-rs = "0.1" u2f-hatter-fork = "0.2" security-framework = { version = "3.0", features = ["OSX_10_15"] } +rsa = "0.9.8" #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_convert_jwk_to_pem.rs b/src/cmd_convert_jwk_to_pem.rs new file mode 100644 index 0000000..70c40f3 --- /dev/null +++ b/src/cmd_convert_jwk_to_pem.rs @@ -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) + } +} diff --git a/src/cmd_convert_pem_to_jwk.rs b/src/cmd_convert_pem_to_jwk.rs index 41cd132..62f5ac9 100644 --- a/src/cmd_convert_pem_to_jwk.rs +++ b/src/cmd_convert_pem_to_jwk.rs @@ -1,4 +1,4 @@ -use crate::{ecutil, util}; +use crate::{ecutil, rsautil, util}; use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use serde_json::Value; @@ -25,7 +25,13 @@ impl Command for CommandImpl { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { let public_key = sub_arg_matches.value_of("public-key").unwrap(); - let jwk = ecutil::convert_ec_public_key_to_jwk(public_key)?; + 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(); diff --git a/src/ecutil.rs b/src/ecutil.rs index 1ce35f0..15e9e2e 100644 --- a/src/ecutil.rs +++ b/src/ecutil.rs @@ -1,6 +1,7 @@ use crate::util::base64_decode; +use p256::pkcs8::LineEnding; use rust_util::XResult; -use spki::DecodePublicKey; +use spki::{DecodePublicKey, EncodePublicKey}; pub fn convert_ec_public_key_to_jwk(public_key: &str) -> XResult { if let Ok(jwk) = convert_ec_public_key_p256_to_jwk(public_key) { @@ -14,8 +15,10 @@ pub fn convert_ec_public_key_to_jwk(public_key: &str) -> XResult { pub fn convert_ec_public_key_p256_to_jwk(public_key: &str) -> XResult { 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)? }; @@ -24,10 +27,40 @@ pub fn convert_ec_public_key_p256_to_jwk(public_key: &str) -> XResult { pub fn convert_ec_public_key_p384_to_jwk(public_key: &str) -> XResult { 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)> { + 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)> { + 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)> { + 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(), + )) +} diff --git a/src/main.rs b/src/main.rs index 0f47e12..e9bf4bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,19 @@ use rust_util::util_clap::{Command, CommandError}; mod argsutil; mod cmd_chall; mod cmd_chall_config; +mod cmd_convert_jwk_to_pem; +mod cmd_convert_pem_to_jwk; mod cmd_ec_verify; -mod cmd_hmac_sha1; -mod cmd_hmac_encrypt; +mod cmd_file_sign; +mod cmd_file_verify; mod cmd_hmac_decrypt; +mod cmd_hmac_encrypt; +mod cmd_hmac_sha1; +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_pgp_age_address; @@ -38,10 +46,9 @@ mod cmd_se_ecdh; mod cmd_se_ecsign; mod cmd_se_generate; mod cmd_se_recover; -mod cmd_file_sign; mod cmd_sign_jwt; -mod cmd_sign_jwt_soft; mod cmd_sign_jwt_se; +mod cmd_sign_jwt_soft; mod cmd_ssh_agent; mod cmd_ssh_parse; mod cmd_ssh_parse_sign; @@ -50,16 +57,14 @@ mod cmd_ssh_piv_sign; mod cmd_ssh_pub_key; mod cmd_u2f_register; mod cmd_u2f_sign; -mod cmd_file_verify; -mod cmd_parseecdsasignature; -mod cmd_keypair_generate; -mod cmd_keypair_keychain_import; -mod cmd_keypair_keychain_export; +mod cmdutil; mod digestutil; mod ecdhutil; mod ecdsautil; +mod ecutil; mod fidoutil; mod hmacutil; +mod keychain; mod keyutil; mod pgpcardutil; mod pinutil; @@ -70,10 +75,6 @@ mod seutil; mod signfile; mod sshutil; mod util; -mod keychain; -mod cmdutil; -mod cmd_convert_pem_to_jwk; -mod ecutil; pub struct DefaultCommandImpl; @@ -152,6 +153,7 @@ fn inner_main() -> CommandError { 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), ]; #[allow(clippy::vec_init_then_push)] diff --git a/src/rsautil.rs b/src/rsautil.rs index 531d42e..78c4e58 100644 --- a/src/rsautil.rs +++ b/src/rsautil.rs @@ -1,8 +1,14 @@ +use std::collections::HashMap; use openssl::bn::{BigNum, BigNumContext}; use openssl::pkey::PKey; use openssl::rsa::{Padding, Rsa}; +use rsa::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 crate::util::{base64_decode, base64_encode}; #[derive(Debug)] pub struct RsaCrt { @@ -151,3 +157,40 @@ fn pkcs1_padding_for_sign(bs: &[u8], bit_len: usize) -> XResult> { output.extend_from_slice(bs); Ok(output) } + +pub fn convert_rsa_to_jwk(public_key: &str) -> XResult { + let rsa_public_key = try_parse_rsa(public_key)?; + + 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()) +} + +fn try_parse_rsa(public_key: &str) -> XResult { + 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.") +}