Initial commit
This commit is contained in:
commit
b0b6fdf309
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1498
Cargo.lock
generated
Normal file
1498
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "fido_ssh_maker"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.72"
|
||||
clap = { version = "4.3.21", features = ["derive"] }
|
||||
ctap-hid-fido2 = "3.5.0"
|
||||
ssh-encoding = { version = "0.1.0" }
|
||||
ssh-key = { version = "0.5.0", features = ["ed25519"] }
|
131
src/main.rs
Normal file
131
src/main.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use std::{
|
||||
fs::Permissions,
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use ctap_hid_fido2::{
|
||||
fidokey::{CredentialSupportedKeyType, MakeCredentialArgsBuilder},
|
||||
verifier, FidoKeyHidFactory, LibCfg,
|
||||
};
|
||||
use ssh_encoding::{Decode, Encode, LineEnding};
|
||||
use ssh_key::{private, PrivateKey};
|
||||
|
||||
/// Generate FIDO-backed SSH keys
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None, arg_required_else_help = true)]
|
||||
struct Args {
|
||||
/// SSH key comment
|
||||
#[arg(short = 'c', long, default_value = "")]
|
||||
comment: String,
|
||||
/// Overwrite existing key files
|
||||
#[arg(short = 'f', long)]
|
||||
force: bool,
|
||||
#[arg(short = 't', long = "type")]
|
||||
key_type: KeyTypeArg,
|
||||
#[arg(long = "write-attestation")]
|
||||
write_attestation: bool,
|
||||
/// Name of key to generate
|
||||
key_name: String,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Copy, Debug)]
|
||||
enum KeyTypeArg {
|
||||
Ed25519,
|
||||
Ecdsa,
|
||||
}
|
||||
|
||||
impl From<KeyTypeArg> for CredentialSupportedKeyType {
|
||||
fn from(value: KeyTypeArg) -> Self {
|
||||
match value {
|
||||
KeyTypeArg::Ed25519 => Self::Ed25519,
|
||||
KeyTypeArg::Ecdsa => Self::Ecdsa256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let privkey_path = Path::new(&args.key_name);
|
||||
let pubkey_path = PathBuf::from(format!("{}.pub", &args.key_name));
|
||||
let attest_info_path = PathBuf::from(format!("{}_attest.bin", &args.key_name));
|
||||
let attest_challenge_path = PathBuf::from(format!("{}_attest_chall.bin", &args.key_name));
|
||||
|
||||
if !args.force && (privkey_path.exists() || pubkey_path.exists() || args.write_attestation && (attest_info_path.exists() || attest_challenge_path.exists())) {
|
||||
return Err(anyhow!("Key files exist, use -f to overwrite."));
|
||||
}
|
||||
|
||||
let key_type = args.key_type.into();
|
||||
|
||||
match key_type {
|
||||
CredentialSupportedKeyType::Ecdsa256 => {
|
||||
println!("Generating public/private ecdsa-sk key pair.")
|
||||
}
|
||||
CredentialSupportedKeyType::Ed25519 => {
|
||||
println!("Generating public/private ed25519-sk key pair.")
|
||||
}
|
||||
}
|
||||
|
||||
let challenge = verifier::create_challenge();
|
||||
let make_credential_args = MakeCredentialArgsBuilder::new("ssh:", &challenge)
|
||||
.without_pin_and_uv()
|
||||
.key_type(key_type)
|
||||
.build();
|
||||
let mut libcfg = LibCfg::init();
|
||||
libcfg.keep_alive_msg = "Touch the authenticator now.".into();
|
||||
let device = FidoKeyHidFactory::create(&libcfg)?;
|
||||
let attestation = device.make_credential_with_args(&make_credential_args)?;
|
||||
dbg!(&attestation.attstmt_alg);
|
||||
let verify_result = verifier::verify_attestation("ssh:", &challenge, &attestation);
|
||||
if !verify_result.is_success {
|
||||
return Err(anyhow!("Failed to verify attestation"));
|
||||
}
|
||||
|
||||
let mut privkey_bytes = Vec::new();
|
||||
if matches!(key_type, CredentialSupportedKeyType::Ecdsa256) {
|
||||
"nistp256".encode(&mut privkey_bytes)?;
|
||||
}
|
||||
verify_result
|
||||
.credential_public_key
|
||||
.der
|
||||
.encode(&mut privkey_bytes)?;
|
||||
"ssh:".encode(&mut privkey_bytes)?;
|
||||
let flags = (attestation.flags_user_present_result as u8)
|
||||
| (attestation.flags_user_verified_result as u8) << 2
|
||||
| (attestation.flags_attested_credential_data_included as u8) << 6
|
||||
| (attestation.flags_extension_data_included as u8) << 7;
|
||||
privkey_bytes.push(flags);
|
||||
verify_result.credential_id.encode(&mut privkey_bytes)?;
|
||||
"".encode(&mut privkey_bytes)?;
|
||||
let privkey = match key_type {
|
||||
CredentialSupportedKeyType::Ecdsa256 => PrivateKey::new(
|
||||
private::SkEcdsaSha2NistP256::decode(&mut privkey_bytes.as_slice())?.into(),
|
||||
args.comment,
|
||||
)?,
|
||||
CredentialSupportedKeyType::Ed25519 => PrivateKey::new(
|
||||
private::SkEd25519::decode(&mut privkey_bytes.as_slice())?.into(),
|
||||
args.comment,
|
||||
)?,
|
||||
};
|
||||
|
||||
let mut ssh_attest = Vec::new();
|
||||
"ssh-sk-attest-v01".encode(&mut ssh_attest)?;
|
||||
attestation.attstmt_x5c[0].encode(&mut ssh_attest)?;
|
||||
attestation.attstmt_sig.encode(&mut ssh_attest)?;
|
||||
attestation.auth_data.encode(&mut ssh_attest)?;
|
||||
0u32.encode(&mut ssh_attest)?;
|
||||
"".encode(&mut ssh_attest)?;
|
||||
|
||||
std::fs::write(&privkey_path, &*privkey.to_openssh(LineEnding::default())?)?;
|
||||
std::fs::set_permissions(&privkey_path, Permissions::from_mode(0o600))?;
|
||||
std::fs::write(&pubkey_path, privkey.public_key().to_openssh()?)?;
|
||||
if args.write_attestation {
|
||||
std::fs::write(&attest_info_path, &ssh_attest)?;
|
||||
std::fs::write(&attest_challenge_path, &challenge)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user