252 lines
8.7 KiB
Rust
252 lines
8.7 KiB
Rust
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use openpgp_card_pcsc::PcscBackend;
|
|
use openpgp_card_sequoia::state::{Admin, Sign, Transaction, User};
|
|
use openpgp_card_sequoia::types::{
|
|
Algo, CardBackend, Curve, EccType, Error, PublicKeyMaterial, StatusBytes,
|
|
};
|
|
use openpgp_card_sequoia::Card;
|
|
|
|
pub(crate) fn cards() -> Result<Vec<Box<dyn CardBackend + Send + Sync>>, Error> {
|
|
PcscBackend::cards(None).map(|cards| cards.into_iter().map(|c| c.into()).collect())
|
|
}
|
|
|
|
pub(crate) fn open_card(ident: &str) -> Result<Box<dyn CardBackend + Send + Sync>, Error> {
|
|
Ok(PcscBackend::open_by_ident(ident, None)?.into())
|
|
}
|
|
|
|
/// Get pin from file. Or via user input, if no file and no pinpad is available.
|
|
///
|
|
/// If a pinpad is available, return Null (the pinpad will be used to get access to the card).
|
|
///
|
|
/// `msg` is the message to show when asking the user to enter a PIN.
|
|
pub(crate) fn get_pin(
|
|
card: &mut Card<Transaction<'_>>,
|
|
pin_file: Option<PathBuf>,
|
|
msg: &str,
|
|
) -> Result<Option<Vec<u8>>> {
|
|
if let Some(path) = pin_file {
|
|
// we have a pin file
|
|
Ok(Some(load_pin(&path).context(format!(
|
|
"Failed to read PIN file {}",
|
|
path.display()
|
|
))?))
|
|
} else if !card.feature_pinpad_verify() {
|
|
// we have no pin file and no pinpad
|
|
let pin = rpassword::prompt_password(msg).context("Failed to read PIN")?;
|
|
Ok(Some(pin.into_bytes()))
|
|
} else {
|
|
// we have a pinpad
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Let the user input a PIN twice, return PIN if both entries match, error otherwise
|
|
pub(crate) fn input_pin_twice(msg1: &str, msg2: &str) -> Result<Vec<u8>> {
|
|
// get new user pin
|
|
let newpin1 = rpassword::prompt_password(msg1)?;
|
|
let newpin2 = rpassword::prompt_password(msg2)?;
|
|
|
|
if newpin1 != newpin2 {
|
|
Err(anyhow::anyhow!("PINs do not match."))
|
|
} else {
|
|
Ok(newpin1.as_bytes().to_vec())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn verify_to_user<'app, 'open>(
|
|
card: &'open mut Card<Transaction<'app>>,
|
|
pin: Option<&[u8]>,
|
|
) -> Result<Card<User<'app, 'open>>, Box<dyn std::error::Error>> {
|
|
if let Some(pin) = pin {
|
|
card.verify_user(pin)?;
|
|
} else {
|
|
if !card.feature_pinpad_verify() {
|
|
return Err(anyhow!("No user PIN file provided, and no pinpad found").into());
|
|
};
|
|
|
|
card.verify_user_pinpad(&|| println!("Enter user PIN on card reader pinpad."))?;
|
|
}
|
|
|
|
card.user_card()
|
|
.ok_or_else(|| anyhow!("Couldn't get user access").into())
|
|
}
|
|
|
|
pub(crate) fn verify_to_sign<'app, 'open>(
|
|
card: &'open mut Card<Transaction<'app>>,
|
|
pin: Option<&[u8]>,
|
|
) -> Result<Card<Sign<'app, 'open>>, Box<dyn std::error::Error>> {
|
|
if let Some(pin) = pin {
|
|
card.verify_user_for_signing(pin)?;
|
|
} else {
|
|
if !card.feature_pinpad_verify() {
|
|
return Err(anyhow!("No user PIN file provided, and no pinpad found").into());
|
|
}
|
|
card.verify_user_for_signing_pinpad(&|| println!("Enter user PIN on card reader pinpad."))?;
|
|
}
|
|
card.signing_card()
|
|
.ok_or_else(|| anyhow!("Couldn't get sign access").into())
|
|
}
|
|
|
|
pub(crate) fn verify_to_admin<'app, 'open>(
|
|
card: &'open mut Card<Transaction<'app>>,
|
|
pin: Option<&[u8]>,
|
|
) -> Result<Card<Admin<'app, 'open>>, Box<dyn std::error::Error>> {
|
|
if let Some(pin) = pin {
|
|
card.verify_admin(pin)?;
|
|
} else {
|
|
if !card.feature_pinpad_verify() {
|
|
return Err(anyhow!("No admin PIN file provided, and no pinpad found").into());
|
|
}
|
|
|
|
card.verify_admin_pinpad(&|| println!("Enter admin PIN on card reader pinpad."))?;
|
|
}
|
|
card.admin_card()
|
|
.ok_or_else(|| anyhow!("Couldn't get admin access").into())
|
|
}
|
|
|
|
pub(crate) fn load_pin(pin_file: &Path) -> Result<Vec<u8>> {
|
|
let pin = std::fs::read_to_string(pin_file)?;
|
|
Ok(pin.trim().as_bytes().to_vec())
|
|
}
|
|
|
|
pub(crate) fn open_or_stdin(f: Option<&Path>) -> Result<Box<dyn std::io::Read + Send + Sync>> {
|
|
match f {
|
|
Some(f) => Ok(Box::new(
|
|
std::fs::File::open(f).context("Failed to open input file")?,
|
|
)),
|
|
None => Ok(Box::new(std::io::stdin())),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn open_or_stdout(f: Option<&Path>) -> Result<Box<dyn std::io::Write + Send + Sync>> {
|
|
match f {
|
|
Some(f) => Ok(Box::new(
|
|
std::fs::File::create(f).context("Failed to open input file")?,
|
|
)),
|
|
None => Ok(Box::new(std::io::stdout())),
|
|
}
|
|
}
|
|
|
|
fn get_ssh_pubkey(pkm: &PublicKeyMaterial, ident: String) -> Result<sshkeys::PublicKey> {
|
|
let cardname = format!("opgpcard:{}", ident);
|
|
|
|
let (key_type, kind) = match pkm {
|
|
PublicKeyMaterial::R(rsa) => {
|
|
let key_type = sshkeys::KeyType::from_name("ssh-rsa")?;
|
|
|
|
let kind = sshkeys::PublicKeyKind::Rsa(sshkeys::RsaPublicKey {
|
|
e: rsa.v().to_vec(),
|
|
n: rsa.n().to_vec(),
|
|
});
|
|
|
|
Ok((key_type, kind))
|
|
}
|
|
PublicKeyMaterial::E(ecc) => {
|
|
if let Algo::Ecc(ecc_attrs) = ecc.algo() {
|
|
match ecc_attrs.ecc_type() {
|
|
EccType::EdDSA => {
|
|
let key_type = sshkeys::KeyType::from_name("ssh-ed25519")?;
|
|
|
|
let kind = sshkeys::PublicKeyKind::Ed25519(sshkeys::Ed25519PublicKey {
|
|
key: ecc.data().to_vec(),
|
|
sk_application: None,
|
|
});
|
|
|
|
Ok((key_type, kind))
|
|
}
|
|
EccType::ECDSA => {
|
|
let (curve, name) = match ecc_attrs.curve() {
|
|
Curve::NistP256r1 => Ok((
|
|
sshkeys::Curve::from_identifier("nistp256")?,
|
|
"ecdsa-sha2-nistp256",
|
|
)),
|
|
Curve::NistP384r1 => Ok((
|
|
sshkeys::Curve::from_identifier("nistp384")?,
|
|
"ecdsa-sha2-nistp384",
|
|
)),
|
|
Curve::NistP521r1 => Ok((
|
|
sshkeys::Curve::from_identifier("nistp521")?,
|
|
"ecdsa-sha2-nistp521",
|
|
)),
|
|
_ => Err(anyhow!("Unexpected ECDSA curve {:?}", ecc_attrs.curve())),
|
|
}?;
|
|
|
|
let key_type = sshkeys::KeyType::from_name(name)?;
|
|
|
|
let kind = sshkeys::PublicKeyKind::Ecdsa(sshkeys::EcdsaPublicKey {
|
|
curve,
|
|
key: ecc.data().to_vec(),
|
|
sk_application: None,
|
|
});
|
|
|
|
Ok((key_type, kind))
|
|
}
|
|
_ => Err(anyhow!("Unexpected EccType {:?}", ecc_attrs.ecc_type())),
|
|
}
|
|
} else {
|
|
Err(anyhow!("Unexpected Algo in EccPub {:?}", ecc))
|
|
}
|
|
}
|
|
_ => Err(anyhow!("Unexpected PublicKeyMaterial type {:?}", pkm)),
|
|
}?;
|
|
|
|
let pk = sshkeys::PublicKey {
|
|
key_type,
|
|
comment: Some(cardname),
|
|
kind,
|
|
};
|
|
|
|
Ok(pk)
|
|
}
|
|
|
|
/// Return a String representation of an ssh public key, in a form like:
|
|
/// "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAuTuxILMTvzTIRvaRqqUM3aRDoEBgz/JAoWKsD1ECxy opgpcard:FFFE:43194240"
|
|
pub(crate) fn get_ssh_pubkey_string(pkm: &PublicKeyMaterial, ident: String) -> Result<String> {
|
|
let pk = get_ssh_pubkey(pkm, ident)?;
|
|
|
|
let mut v = vec![];
|
|
pk.write(&mut v)?;
|
|
|
|
let s = String::from_utf8_lossy(&v).to_string();
|
|
|
|
Ok(s.trim().into())
|
|
}
|
|
|
|
/// Gnuk doesn't allow the User password (pw1) to be changed while no
|
|
/// private key material exists on the card.
|
|
///
|
|
/// This fn checks for Gnuk's Status code and the case that no keys exist
|
|
/// on the card, and prints a note to the user, pointing out that the
|
|
/// absence of keys on the card might be the reason for the error they get.
|
|
pub(crate) fn print_gnuk_note(err: Error, card: &Card<Transaction>) -> Result<()> {
|
|
if matches!(
|
|
err,
|
|
Error::CardStatus(StatusBytes::ConditionOfUseNotSatisfied)
|
|
) {
|
|
// check if no keys exist on the card
|
|
let fps = card.fingerprints()?;
|
|
if fps.signature() == None && fps.decryption() == None && fps.authentication() == None {
|
|
println!(
|
|
"\nNOTE: Some cards (e.g. Gnuk) don't allow \
|
|
User PIN change while no keys exist on the card."
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn pem_encode(data: Vec<u8>) -> String {
|
|
const PEM_TAG: &str = "CERTIFICATE";
|
|
|
|
let pem = pem::Pem {
|
|
tag: String::from(PEM_TAG),
|
|
contents: data,
|
|
};
|
|
|
|
pem::encode(&pem)
|
|
}
|