Initial commit

This commit is contained in:
pjht 2023-08-14 10:04:07 -05:00
commit b0b6fdf309
Signed by: pjht
GPG Key ID: 7B5F6AFBEC7EE78E
4 changed files with 1643 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1498
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View 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
View 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(())
}