feat: supports yubikey init

This commit is contained in:
2023-08-13 21:26:16 +08:00
parent e16c28f2ab
commit b5e13dc13a
8 changed files with 393 additions and 48 deletions

View File

@@ -8,7 +8,7 @@ use hyper::body::Buf;
use josekit::jwk::Jwk;
use rust_util::{debugging, opt_value_result, simple_error, success, XResult};
use rust_util::util_clap::{Command, CommandError};
use serde_json::{json, Value};
use serde_json::{json, Map, Value};
use crate::jose;
@@ -19,7 +19,7 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Local mini KMS cli")
.arg(Arg::with_name("connect").long("connect").takes_value(true).default_value("127.0.0.1:5567").help("Connect server"))
.arg(Arg::with_name("connect").long("connect").short("C").takes_value(true).default_value("127.0.0.1:5567").help("Connect server"))
.arg(Arg::with_name("init").long("init").help("Init server"))
.arg(Arg::with_name("direct-init").long("direct-init").help("Direct init server"))
.arg(Arg::with_name("offline-init").long("offline-init").help("Offline init server"))
@@ -31,7 +31,8 @@ impl Command for CommandImpl {
.arg(Arg::with_name("key").long("key").takes_value(true).help("Read/Write key name"))
.arg(Arg::with_name("value-hex").long("value-hex").takes_value(true).help("Value(hex), for encrypt"))
.arg(Arg::with_name("value-base64").long("value-base64").takes_value(true).help("Value(base64), for encrypt"))
.arg(Arg::with_name("force-write").long("force-write").help("Force write value"))
.arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge"))
.arg(Arg::with_name("force-write").long("force-write").short("F").help("Force write value"))
}
fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -64,33 +65,22 @@ impl Command for CommandImpl {
}
async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
let value_hex = sub_arg_matches.value_of("value-hex");
let value_base64 = sub_arg_matches.value_of("value-base64");
let yubikey_challenge = sub_arg_matches.value_of("yubikey-challenge");
let client = Client::new();
let uri = format!("http://{}/init", connect);
debugging!("Request uri: {}", &uri);
let body = if let Some(value_hex) = value_hex {
json!({
"clear_master_key_hex": value_hex,
})
let mut body_map = Map::new();
if let Some(value_hex) = value_hex {
body_map.insert("clear_master_key_hex".to_string(), value_hex.into());
} else if let Some(value_base64) = value_base64 {
json!({
"clear_master_key_base64": value_base64,
})
body_map.insert("clear_master_key_base64".to_string(), value_base64.into());
} else {
return simple_error!("Requires value hex or value base64");
};
let body = serde_json::to_string(&body)?;
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK {
let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data);
}
if let Some(yubikey_challenge) = yubikey_challenge {
body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into());
}
let _data = do_inner_request(sub_arg_matches, "init", &Value::Object(body_map)).await?;
success!("Init finished");
Ok(Some(0))
}

View File

@@ -6,6 +6,7 @@ mod db;
mod proc;
mod jose;
mod cli;
mod yubikey_hmac;
mod serve;
mod serve_common;
mod serve_status;

View File

@@ -5,17 +5,20 @@ use hyper::{Body, Client, Method, Request, Response, Server, StatusCode};
use hyper::client::HttpConnector;
use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn};
use rust_util::{failure_and_exit, information, success, XResult};
use rust_util::{failure_and_exit, information, success, warning, XResult};
use rust_util::util_clap::{Command, CommandError};
use serde_json::{json, Value};
use tokio::runtime::Runtime;
use crate::{db, jose, proc};
use crate::do_response;
use crate::serve_common::{self, GenericError, MemoryKey, Result};
use crate::serve_encrypt_decrypt;
use crate::serve_init;
use crate::serve_init::InitRequest;
use crate::serve_read_write;
use crate::serve_status;
use crate::yubikey_hmac;
pub struct CommandImpl;
@@ -24,8 +27,10 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Local mini KMS serve")
.arg(Arg::with_name("listen").long("listen").takes_value(true).default_value("127.0.0.1:5567").help("Listen"))
.arg(Arg::with_name("local-db").long("local-db").takes_value(true).default_value("local-mini-kms.db").help("Local db file"))
.arg(Arg::with_name("listen").long("listen").short("L").takes_value(true).default_value("127.0.0.1:5567").help("Listen"))
.arg(Arg::with_name("local-db").long("local-db").short("d").takes_value(true).default_value("local-mini-kms.db").help("Local db file"))
.arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge"))
.arg(Arg::with_name("init-encrypted-master-key").long("init-encrypted-master-key").short("k").takes_value(true).help("Init encrypted mater key"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -36,8 +41,10 @@ impl Command for CommandImpl {
Err(e) => failure_and_exit!("Init server failed: {}", e),
}
let rt = Runtime::new().expect("Create tokio runtime error");
init_with_yubikey_challenge(&rt, sub_arg_matches);
let listen = sub_arg_matches.value_of("listen").expect("Get argument listen error");
let rt = tokio::runtime::Runtime::new().expect("Create tokio runtime error");
rt.block_on(async {
let addr = listen.parse().expect(&format!("Parse listen error: {}", listen));
let client = Client::new();
@@ -156,3 +163,49 @@ Supports commands:
Ok(Response::builder().body("Root Not Found\n".into())?)
}
}
fn init_with_yubikey_challenge(rt: &Runtime, sub_arg_matches: &ArgMatches) {
let mut yubikey_challenge = sub_arg_matches.value_of("yubikey-challenge").map(ToString::to_string);
let init_encrypted_master_key = sub_arg_matches.value_of("init-encrypted-master-key");
if yubikey_challenge.is_none() {
yubikey_challenge = rpassword::prompt_password("Yubikey challenge: ").ok();
}
let (challenge_key, init_encrypted_master_key) = match (yubikey_challenge, init_encrypted_master_key) {
(Some(yubikey_challenge), Some(init_encrypted_master_key)) => {
match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) {
Err(e) => {
warning!("Yubikey challenge failed: {}", e);
return;
}
Ok(challenge_key) => (challenge_key, init_encrypted_master_key),
}
}
(Some(_), None) | (None, Some(_)) => {
warning!("Arguments yubikey-challenge and init-encrypted-master-key should both assigned.");
return;
}
_ => return,
};
match jose::deserialize_jwe_aes(&init_encrypted_master_key, &challenge_key) {
Err(e) => warning!("Yubikey seal master key failed: {}", e),
Ok((key, _)) => {
success!("Yubikey un-seal master key success");
let init_master_key_result = rt.block_on(async {
serve_init::inner_init_request(InitRequest {
yubikey_challenge: None,
clear_master_key_hex: Some(hex::encode(&key)),
clear_master_key_base64: None,
encrypted_master_key: None,
}).await
});
match init_master_key_result {
Err(e) => warning!("Init master key failed: {}", e),
Ok((StatusCode::OK, _)) => success!("Init master key success"),
Ok((_, response)) => warning!("Init master failed: {}", response),
}
}
}
}

View File

@@ -2,7 +2,7 @@ use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use hyper::{Body, Request, Response, StatusCode};
use hyper::body::Buf;
use rust_util::{debugging, information, opt_result, XResult};
use rust_util::{debugging, information, opt_result, success, warning, XResult};
use seckey::SecBytes;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -12,22 +12,28 @@ use crate::{db, jose};
use crate::db::Key;
use crate::do_response;
use crate::serve_common::{self, Result};
use crate::yubikey_hmac;
pub async fn init(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_init(req).await)
}
#[derive(Serialize, Deserialize)]
struct InitRequest {
clear_master_key_hex: Option<String>,
clear_master_key_base64: Option<String>,
encrypted_master_key: Option<String>,
pub struct InitRequest {
pub yubikey_challenge: Option<String>,
pub clear_master_key_hex: Option<String>,
pub clear_master_key_base64: Option<String>,
pub encrypted_master_key: Option<String>,
}
async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?;
let init_request: InitRequest = serde_json::from_reader(whole_body.reader())?;
inner_init_request(init_request).await
}
pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCode, Value)> {
let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
match &*startup_rw_lock {
None => return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))),
@@ -74,6 +80,17 @@ async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
}
}
information!("Set master key success");
if let Some(yubikey_challenge) = &init_request.yubikey_challenge {
match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) {
Err(e) => warning!("Yubikey challenge failed: {}", e),
Ok(challenge_key) => match jose::serialize_jwe_aes(&clear_master_key, &challenge_key) {
Err(e) => warning!("Yubikey seal master key failed: {}", e),
Ok(jwe) => success!("Yubikey sealed master key: {}", jwe)
},
}
}
let sec_bytes = SecBytes::with(clear_master_key.len(), |buf| buf.copy_from_slice(&clear_master_key.as_slice()[..]));
let mut clear_master_key = clear_master_key;
clear_master_key.zeroize();

29
src/yubikey_hmac.rs Normal file
View File

@@ -0,0 +1,29 @@
use std::ops::Deref;
use rust_util::{opt_result, success, XResult};
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::Yubico;
pub fn yubikey_challenge_as_32_bytes(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
let mut yubi = Yubico::new();
let device = opt_result!(yubi.find_yubikey(), "Find yubikey failed: {}");
success!("Found key, Vendor ID: {:?}, Product ID: {:?}", device.vendor_id, device.product_id);
let config = Config::default()
.set_vendor_id(device.vendor_id)
.set_product_id(device.product_id)
.set_variable_size(true)
.set_mode(Mode::Sha1)
.set_slot(Slot::Slot2);
// In HMAC Mode, the result will always be the SAME for the SAME provided challenge
let hmac_result = opt_result!(yubi.challenge_response_hmac(&challenge_bytes, config), "Challenge HMAC failed: {}");
// Just for debug, lets check the hex
let v: &[u8] = hmac_result.deref();
let mut r = vec![];
r.extend_from_slice(v);
r.extend_from_slice(&v[0..12]);
Ok(r)
}