doc: document constants
Some checks are pending
CI / Check (push) Waiting to run
CI / Test Suite (push) Waiting to run
CI / Rustfmt (push) Waiting to run
CI / Clippy (push) Waiting to run
CI / cargo-deny (push) Waiting to run

This commit is contained in:
Grimmauld 2025-02-15 23:01:46 +01:00
parent c596cfbcd9
commit b7b4e79c14
No known key found for this signature in database
5 changed files with 59 additions and 70 deletions

View file

@ -2,14 +2,24 @@ use std::{fmt::Display, time::Duration};
use iso7816_tlv::simple::Tlv; use iso7816_tlv::simple::Tlv;
use sha1::Digest; use sha1::Digest;
/// te apdu instruction required to select the Oath application on the YubiKey
pub const INS_SELECT: u8 = 0xa4; pub const INS_SELECT: u8 = 0xa4;
/// the stream of data bytes required to select the Oath application on the YubiKey
pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01]; pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01];
/// The default duration that generated codes are valid for is 30 seconds.
/// Non-default values are saved and retrieved from the YubiKey.
/// If not explicitly declared otherwise, this is the fallback that will be used.
pub const DEFAULT_PERIOD: Duration = Duration::from_secs(30); pub const DEFAULT_PERIOD: Duration = Duration::from_secs(30);
pub const DEFAULT_DIGITS: OathDigits = OathDigits::Six;
pub const DEFAULT_IMF: u32 = 0; /// Oath keys on the YubiKey are stored by a handle.
/// If the original handle is too long, it will be shortend to an hmac.
/// If the key is too short, it will be padded to have the minimum length.
pub const HMAC_MINIMUM_KEY_SIZE: usize = 14; pub const HMAC_MINIMUM_KEY_SIZE: usize = 14;
/// Error Response codes as returned by the YubiKey
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)] #[repr(u16)]
pub enum ErrorResponse { pub enum ErrorResponse {
@ -60,6 +70,7 @@ impl std::fmt::Display for ErrorResponse {
impl std::error::Error for ErrorResponse {} impl std::error::Error for ErrorResponse {}
/// Success response codes as returned by the YubiKey
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
#[repr(u16)] #[repr(u16)]
pub enum SuccessResponse { pub enum SuccessResponse {
@ -79,6 +90,7 @@ impl SuccessResponse {
} }
} }
/// Set of possible instructions to use with the YubiKey via apdu
#[repr(u8)] #[repr(u8)]
pub enum Instruction { pub enum Instruction {
Put = 0x01, Put = 0x01,
@ -93,12 +105,7 @@ pub enum Instruction {
SendRemaining = 0xa5, SendRemaining = 0xa5,
} }
#[repr(u8)] /// possible tlv tag values to use with apdu
pub enum Mask {
Algo = 0x0f,
Type = 0xf0,
}
#[repr(u8)] #[repr(u8)]
pub enum Tag { pub enum Tag {
Name = 0x71, Name = 0x71,
@ -115,6 +122,7 @@ pub enum Tag {
Touch = 0x7c, Touch = 0x7c,
} }
/// Different Hash algorithms supported by the YubiKey to generate OTPs
#[derive(Debug, PartialEq, Copy, Clone)] #[derive(Debug, PartialEq, Copy, Clone)]
#[repr(u8)] #[repr(u8)]
pub enum HashAlgo { pub enum HashAlgo {
@ -124,7 +132,8 @@ pub enum HashAlgo {
} }
impl HashAlgo { impl HashAlgo {
// returns a function capable of hashing a byte array /// returns a function capable of hashing a byte array
/// necessary to be able to validate keys before enrolling them on the hardware key
pub fn get_hash_fun(&self) -> impl Fn(&[u8]) -> Vec<u8> { pub fn get_hash_fun(&self) -> impl Fn(&[u8]) -> Vec<u8> {
match self { match self {
Self::Sha1 => |m: &[u8]| { Self::Sha1 => |m: &[u8]| {
@ -145,7 +154,7 @@ impl HashAlgo {
} }
} }
// returns digest output size in number of bytes /// returns digest output size in number of bytes
pub fn digest_size(&self) -> usize { pub fn digest_size(&self) -> usize {
match self { match self {
Self::Sha1 => 20, Self::Sha1 => 20,
@ -155,6 +164,7 @@ impl HashAlgo {
} }
} }
/// different types of oath to generate OTPs
#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)] #[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)]
#[repr(u8)] #[repr(u8)]
pub enum OathType { pub enum OathType {
@ -162,12 +172,8 @@ pub enum OathType {
Hotp = 0x20, Hotp = 0x20,
} }
#[derive(Clone, Copy, Debug, PartialEq)] /// describes display information of a code, keeping track of the code and number of digits
pub enum OathDigits { /// digits *should* be either 6 or 8, but nothing is preventing the yubikey from returning e.g. 7 digits, which would "just work".
Six = 6,
Eight = 8,
}
#[derive(Debug, PartialEq, Hash, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Hash, Eq, Copy, Clone)]
pub struct OathCodeDisplay { pub struct OathCodeDisplay {
code: u32, code: u32,
@ -181,19 +187,24 @@ impl Display for OathCodeDisplay {
} }
impl OathCodeDisplay { impl OathCodeDisplay {
/// extracts display information (digits and code) from a `Tlv` instance
pub fn from_tlv(tlv: Tlv) -> Option<Self> { pub fn from_tlv(tlv: Tlv) -> Option<Self> {
if Into::<u8>::into(tlv.tag()) == (Tag::TruncatedResponse as u8) && tlv.value().len() == 5 { if Into::<u8>::into(tlv.tag()) == (Tag::TruncatedResponse as u8) {
let display = OathCodeDisplay::new(tlv.value()[..].try_into().unwrap()); TryInto::<&[u8; 5]>::try_into(tlv.value())
Some(display) .ok()
.and_then(OathCodeDisplay::new)
} else { } else {
None None
} }
} }
pub fn new(bytes: &[u8; 5]) -> Self { /// extract display information from a 5-bytes buffer
Self { pub fn new(bytes: &[u8; 5]) -> Option<Self> {
TryInto::<[u8; 4]>::try_into(&bytes[1..])
.ok()
.map(|code_bytes| Self {
digits: bytes[0], digits: bytes[0],
code: u32::from_be_bytes((&bytes[1..5]).try_into().unwrap()), code: u32::from_be_bytes(code_bytes),
} })
} }
} }

View file

@ -1,8 +1,8 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
use ykoath2::constants::{HashAlgo, OathDigits, OathType, DEFAULT_PERIOD}; use ykoath2::constants::{HashAlgo, OathType, DEFAULT_PERIOD};
use ykoath2::oath_credential::OathCredential; use ykoath2::oath_credential::OathCredential;
use ykoath2::oath_credentialid::CredentialIDData; use ykoath2::oath_credential_id::CredentialIDData;
use ykoath2::OathSession; use ykoath2::OathSession;
// use crate::args::Cli; // use crate::args::Cli;
@ -55,7 +55,7 @@ fn main() {
cred.clone(), cred.clone(),
"f5up4ub3dw".as_bytes(), "f5up4ub3dw".as_bytes(),
HashAlgo::Sha256, HashAlgo::Sha256,
OathDigits::Six, 6,
None, None,
) )
.unwrap(); .unwrap();

View file

@ -1,12 +1,24 @@
//! # Rust bindings to the Oath application on the YubiKey
//!
//! Bindings closely resemble the reverse-engineered [python library](https://github.com/Yubico/yubikey-manager/blob/main/yubikit/oath.py),
//! as well as the discontinued crate [ykoath](https://crates.io/crates/ykoath)
//!
/// constants relevant for apdu, pcsc, error handling
pub mod constants; pub mod constants;
/// OathCredential stores one credential
pub mod oath_credential; pub mod oath_credential;
pub mod oath_credentialid;
/// OathCredentialId stores information about issuer, credential name, time raster, display
pub mod oath_credential_id;
mod refreshable_oath_credential; mod refreshable_oath_credential;
mod transaction; mod transaction;
use constants::*; use constants::*;
use oath_credential::*; use oath_credential::*;
use oath_credentialid::*; use oath_credential_id::*;
use refreshable_oath_credential::*; use refreshable_oath_credential::*;
use transaction::*; use transaction::*;
@ -38,6 +50,7 @@ fn time_challenge(timestamp: Option<SystemTime>, period: Option<Duration>) -> [u
.to_be_bytes() .to_be_bytes()
} }
/// keeps track of transactions with a named YubiKey
pub struct OathSession { pub struct OathSession {
version: Vec<u8>, version: Vec<u8>,
salt: Vec<u8>, salt: Vec<u8>,
@ -97,10 +110,6 @@ impl OathSession {
None => return Ok(()), None => return Ok(()),
}; };
if !self.locked {
return Ok(());
}
let hmac = hmac_sha1(key, &chal); let hmac = hmac_sha1(key, &chal);
let random_chal = getrandom::u64()?.to_be_bytes(); let random_chal = getrandom::u64()?.to_be_bytes();
let data = &[ let data = &[
@ -120,14 +129,13 @@ impl OathSession {
self.locked = false; self.locked = false;
Ok(()) Ok(())
} else { } else {
Err(Error::Authentication) Err(Error::Unknown(
"Unlocking session failed unexpectedly".to_string(),
))
} }
} }
pub fn set_key(&mut self, key: &[u8]) -> Result<(), Error> { pub fn set_key(&mut self, key: &[u8]) -> Result<(), Error> {
if self.locked {
return Err(Error::Authentication);
}
let random_chal = getrandom::u64()?.to_be_bytes(); let random_chal = getrandom::u64()?.to_be_bytes();
let hmac = hmac_sha1(key, &random_chal); let hmac = hmac_sha1(key, &random_chal);
let data = &[ let data = &[
@ -152,10 +160,6 @@ impl OathSession {
} }
pub fn unset_key(&mut self) -> Result<(), Error> { pub fn unset_key(&mut self) -> Result<(), Error> {
if self.locked {
return Err(Error::Authentication);
}
self.transaction_context.apdu( self.transaction_context.apdu(
0, 0,
Instruction::SetCode as u8, Instruction::SetCode as u8,
@ -173,10 +177,6 @@ impl OathSession {
old: CredentialIDData, old: CredentialIDData,
new: CredentialIDData, new: CredentialIDData,
) -> Result<CredentialIDData, Error> { ) -> Result<CredentialIDData, Error> {
if self.locked {
return Err(Error::Authentication);
}
self.require_version(vec![5, 3, 1])?; self.require_version(vec![5, 3, 1])?;
self.transaction_context.apdu( self.transaction_context.apdu(
0, 0,
@ -189,10 +189,6 @@ impl OathSession {
} }
pub fn delete_code(&self, cred: OathCredential) -> Result<(), Error> { pub fn delete_code(&self, cred: OathCredential) -> Result<(), Error> {
if self.locked {
return Err(Error::Authentication);
}
self.transaction_context.apdu( self.transaction_context.apdu(
0, 0,
Instruction::Delete as u8, Instruction::Delete as u8,
@ -208,13 +204,9 @@ impl OathSession {
cred: OathCredential, cred: OathCredential,
secret: &[u8], secret: &[u8],
algo: HashAlgo, algo: HashAlgo,
digits: OathDigits, digits: u8,
counter: Option<u32>, counter: Option<u32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
if self.locked {
return Err(Error::Authentication);
}
let secret_short = hmac_shorten_key(secret, algo); let secret_short = hmac_shorten_key(secret, algo);
let mut secret_padded = [0u8; HMAC_MINIMUM_KEY_SIZE]; let mut secret_padded = [0u8; HMAC_MINIMUM_KEY_SIZE];
let len_to_copy = secret_short.len().min(HMAC_MINIMUM_KEY_SIZE); // Avoid copying more than 14 let len_to_copy = secret_short.len().min(HMAC_MINIMUM_KEY_SIZE); // Avoid copying more than 14
@ -226,7 +218,7 @@ impl OathSession {
to_tlv( to_tlv(
Tag::Key, Tag::Key,
&[ &[
[(cred.id_data.oath_type as u8) | (algo as u8), digits as u8].to_vec(), [(cred.id_data.oath_type as u8) | (algo as u8), digits].to_vec(),
secret_padded.to_vec(), secret_padded.to_vec(),
] ]
.concat(), .concat(),
@ -270,10 +262,6 @@ impl OathSession {
cred: &OathCredential, cred: &OathCredential,
timestamp_sys: Option<SystemTime>, timestamp_sys: Option<SystemTime>,
) -> Result<OathCodeDisplay, Error> { ) -> Result<OathCodeDisplay, Error> {
if self.locked {
return Err(Error::Authentication);
}
if self.name != cred.device_id { if self.name != cred.device_id {
return Err(Error::DeviceMismatch); return Err(Error::DeviceMismatch);
} }
@ -308,10 +296,6 @@ impl OathSession {
/// Read the OATH codes from the device, calculate TOTP codes that don't /// Read the OATH codes from the device, calculate TOTP codes that don't
/// need touch /// need touch
pub fn calculate_oath_codes(&self) -> Result<Vec<RefreshableOathCredential>, Error> { pub fn calculate_oath_codes(&self) -> Result<Vec<RefreshableOathCredential>, Error> {
if self.locked {
return Err(Error::Authentication);
}
let timestamp = SystemTime::now(); let timestamp = SystemTime::now();
// Request OATH codes from device // Request OATH codes from device
let response = self.transaction_context.apdu_read_all( let response = self.transaction_context.apdu_read_all(
@ -347,10 +331,6 @@ impl OathSession {
Ok(key_buffer) Ok(key_buffer)
} }
pub fn list_oath_codes(&self) -> Result<Vec<CredentialIDData>, Error> { pub fn list_oath_codes(&self) -> Result<Vec<CredentialIDData>, Error> {
if self.locked {
return Err(Error::Authentication);
}
// Request OATH codes from device // Request OATH codes from device
let response = let response =
self.transaction_context self.transaction_context

View file

@ -14,14 +14,13 @@ pub enum Error {
Pcsc(pcsc::Error), Pcsc(pcsc::Error),
Parsing(String), Parsing(String),
DeviceMismatch, DeviceMismatch,
Authentication,
Random(getrandom::Error), Random(getrandom::Error),
Version(Vec<u8>, Vec<u8>), Version(Vec<u8>, Vec<u8>),
} }
impl Error { impl Error {
fn from_apdu_response(sw1: u8, sw2: u8) -> Result<(), Self> { fn from_apdu_response(sw1: u8, sw2: u8) -> Result<(), Self> {
let code: u16 = (sw1 as u16 | sw2 as u16) << 8; let code: u16 = ((sw1 as u16) << 8) | (sw2 as u16);
if let Some(e) = ErrorResponse::any_match(code) { if let Some(e) = ErrorResponse::any_match(code) {
return Err(Self::Protocol(e)); return Err(Self::Protocol(e));
} }
@ -55,7 +54,6 @@ impl Display for Error {
Self::Pcsc(error) => f.write_fmt(format_args!("{}", error)), Self::Pcsc(error) => f.write_fmt(format_args!("{}", error)),
Self::Parsing(msg) => f.write_str(msg), Self::Parsing(msg) => f.write_str(msg),
Self::DeviceMismatch => f.write_str("Devices do not match"), Self::DeviceMismatch => f.write_str("Devices do not match"),
Self::Authentication => f.write_str("Authentication failure"),
Self::Random(error_response) => f.write_fmt(format_args!("{}", error_response)), Self::Random(error_response) => f.write_fmt(format_args!("{}", error_response)),
Self::Version(ver, req) => f.write_fmt(format_args!( Self::Version(ver, req) => f.write_fmt(format_args!(
"Version requirement not met: is {}, required {}", "Version requirement not met: is {}, required {}",