Skip to content

Commit

Permalink
Add challenge-response method for KeepassXC support
Browse files Browse the repository at this point in the history
  • Loading branch information
szszszsz committed May 6, 2023
1 parent a0c009d commit e5b3c1b
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 13 deletions.
1 change: 1 addition & 0 deletions examples/usbip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ impl trussed_usbip::Apps<VirtClient, dispatch::Dispatch> for Apps {
Location::Internal,
CustomStatus::ReverseHotpSuccess as u8,
CustomStatus::ReverseHotpError as u8,
[0x42, 0x42, 0x42, 0x42],
);
let otp = oath_authenticator::Authenticator::new(
builder.build("otp", dispatch::BACKENDS),
Expand Down
79 changes: 73 additions & 6 deletions src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ use core::time::Duration;

use flexiber::{Encodable, EncodableHeapless};
use heapless_bytes::Bytes;
use iso7816::Status::NotFound;
use iso7816::Status::{
NotFound, UnspecifiedNonpersistentExecutionError, UnspecifiedPersistentExecutionError,
};
use iso7816::{Data, Status};
use trussed::types::Location;
use trussed::types::{KeyId, Message};
use trussed::{client, syscall, try_syscall, types::PathBuf};

use crate::command::{EncryptionKeyType, VerifyCode};
use crate::calculate::hmac_challenge;
use crate::command::{EncryptionKeyType, VerifyCode, YKGetHMAC};
use crate::credential::Credential;
use crate::oath::Kind;
use crate::oath::{Algorithm, Kind};
use crate::{
command, ensure, oath,
state::{CommandState, State},
Expand All @@ -37,18 +40,23 @@ pub struct Options {
/// The custom status id to be set for the failed verification for the Reverse HOTP.
/// By design this should be animated as: blink red LED infinite times, highest priority.
pub custom_status_reverse_hotp_error: u8,

/// A serial number to be returned in YK Challenge-Response and Status commands
pub serial_number: [u8; 4],
}

impl Options {
pub const fn new(
location: Location,
custom_status_reverse_hotp_success: u8,
custom_status_reverse_hotp_error: u8,
serial_number: [u8; 4],
) -> Self {
Self {
location,
custom_status_reverse_hotp_success,
custom_status_reverse_hotp_error,
serial_number,
}
}
}
Expand All @@ -72,7 +80,7 @@ struct OathVersion {
impl Default for OathVersion {
/// For ykman, 4.2.6 is the first version to support "touch" requirement
fn default() -> Self {
// OathVersion { major: 1, minor: 0, patch: 0}
// TODO: set this up automatically during the build from the project version
OathVersion {
major: 4,
minor: 11,
Expand Down Expand Up @@ -293,6 +301,10 @@ where
Command::SetPin(spin) => self.set_pin(spin, reply),
Command::ChangePin(cpin) => self.change_pin(cpin, reply),

Command::YKSerial => self.yk_serial(reply),
Command::YKGetStatus => self.yk_status(reply),
Command::YKGetHMAC(req) => self.yk_hmac(req, reply),

Command::SendRemaining => self.send_remaining(reply),
_ => Err(Status::ConditionsOfUseNotSatisfied),
};
Expand Down Expand Up @@ -694,8 +706,8 @@ where
return Err(Status::UnspecifiedPersistentExecutionError);
}
}
Kind::HotpReverse => {
// This credential kind should never be access through calculate()
_ => {
// This credential kind should never be accessed through calculate()
return Err(Status::SecurityStatusNotSatisfied);
}
};
Expand Down Expand Up @@ -1282,6 +1294,61 @@ where
}
return Ok(());
}

fn yk_hmac<const R: usize>(&mut self, req: YKGetHMAC, reply: &mut Data<{ R }>) -> Result {
// Get HMAC slot command

let credential = self
.load_credential(req.get_credential_label()?)
.ok_or(Status::NotFound)?;
let key: &[u8] = &credential.secret;

// Make sure the set Credential is the right kind
ensure(
credential.kind == Kind::Hmac,
Status::IncorrectDataParameter,
)?;

let signature = hmac_challenge(&mut self.trussed, Algorithm::Sha1, req.challenge, key)?;

// TODO remove this check?
const HMAC_SHA1_LENGTH: usize = 20;
ensure(
signature.len() == HMAC_SHA1_LENGTH,
UnspecifiedNonpersistentExecutionError,
)?;

reply
.extend_from_slice(signature.as_slice())
.map_err(|_| UnspecifiedNonpersistentExecutionError)?;
Ok(())
}

fn yk_status<const R: usize>(&self, reply: &mut Data<{ R }>) -> Result {
// Get 6 bytes status; 3 bytes version, 3 bytes other data
// TODO Discuss, should this be application or runner firmware version
let v = OathVersion::default();
let firmware_version = &[v.major, v.minor, v.patch];
reply
.extend_from_slice(firmware_version)
.map_err(|_| UnspecifiedPersistentExecutionError)?;

// Add filler to match the expected 6 bytes
// TODO Check the actual data format for the YK request
let other_data = &[0x42, 0x42, 0x42];
reply
.extend_from_slice(other_data)
.map_err(|_| UnspecifiedPersistentExecutionError)?;
Ok(())
}

fn yk_serial<const R: usize>(&self, reply: &mut Data<{ R }>) -> Result {
// Get 4-byte serial
reply
.extend_from_slice(&self.options.serial_number)
.map_err(|_| UnspecifiedPersistentExecutionError)?;
Ok(())
}
}

impl<T> iso7816::App for Authenticator<T> {
Expand Down
25 changes: 24 additions & 1 deletion src/calculate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use iso7816::Status;

use crate::oath;
use crate::Result;
use trussed::types::Signature;
use trussed::{
client, try_syscall,
types::{KeyId, Location},
Expand Down Expand Up @@ -51,14 +52,36 @@ where
.signature;
dynamic_truncation(&digest)
}
// Sha512 => unimplemented!(),
Sha512 => return Err(Status::FunctionNotSupported),
};

Ok(truncated.to_be_bytes())
})?
}

pub fn hmac_challenge<T>(
trussed: &mut T,
algorithm: oath::Algorithm,
challenge: &[u8],
key: &[u8],
) -> Result<Signature>
where
T: client::Client + client::HmacSha1,
{
with_key(trussed, key, |trussed, key| {
use oath::Algorithm::*;
match algorithm {
Sha1 => {
let digest = try_syscall!(trussed.sign_hmacsha1(key, challenge))
.map_err(|_| Status::UnspecifiedPersistentExecutionError)?
.signature;
Ok(digest)
}
_ => Err(Status::InstructionNotSupportedOrInvalid),
}
})?
}

fn dynamic_truncation(digest: &[u8]) -> u32 {
// TL;DR: The standard assumes that you use the low 4 bits of the last byte of the hash, regardless of its length. So replace 19 in the original DT definition with 31 for SHA-256 or 63 for SHA-512 and you are good to go.

Expand Down
128 changes: 123 additions & 5 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use core::convert::{TryFrom, TryInto};
use serde::{Deserialize, Serialize};

use iso7816::{Data, Status};
use iso7816::command::class::Class;
use iso7816::Status::InstructionNotSupportedOrInvalid;
use iso7816::{Data, Instruction, Status};
use YKCommand::GetSerial;

use crate::oath::{Kind, YKCommand};
use crate::{ensure, oath};

const FAILED_PARSING_ERROR: Status = iso7816::Status::IncorrectDataParameter;
Expand Down Expand Up @@ -39,6 +43,85 @@ pub enum Command<'l> {
VerifyCode(VerifyCode<'l>),
/// Send remaining data in the buffer
SendRemaining,

YKSerial,
YKGetStatus,
YKGetHMAC(YKGetHMAC<'l>),
}

#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct YKGetHMAC<'l> {
/// challenge, padded with PKCS#7 to 64 bytes
pub challenge: &'l [u8],
/// The P1 parameter selecting the command, or the HMAC slot
pub slot_cmd: Option<YKCommand>,
}

impl<'l, const C: usize> TryFrom<&'l Data<C>> for YKGetHMAC<'l> {
type Error = Status;
fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
Ok(Self {
challenge: data,
slot_cmd: None,
})
}
}

impl<'l> YKGetHMAC<'l> {
pub fn get_credential_label(&self) -> Result<&[u8], Status> {
Ok(match self.slot_cmd.ok_or(Status::IncorrectDataParameter)? {
YKCommand::HmacSlot1 => "HmacSlot1",
YKCommand::HmacSlot2 => "HmacSlot2",
_ => {
return Err(Status::IncorrectDataParameter);
}
}
.as_bytes())
}
fn with_slot(&self, slot: u8) -> Result<Self, Status> {
let slot = YKCommand::try_from(slot)?;
match slot {
YKCommand::HmacSlot1 => {}
YKCommand::HmacSlot2 => {}
_ => return Err(Status::IncorrectDataParameter),
};
Ok(YKGetHMAC {
challenge: self.challenge,
slot_cmd: Some(slot),
})
}
}

impl<'l> TryFrom<&'l [u8]> for YKGetHMAC<'l> {
type Error = Status;
fn try_from(data: &'l [u8]) -> Result<Self, Self::Error> {
// Input data should always be padded to 64 bytes
ensure(data.len() == 64, Status::IncorrectDataParameter)?;
// PKCS#7 padding; compatibility with Yubikey's PKCS#7 version
// TODO extract to separate crate or use known good implementation
let challenge = {
let mut idx = data.len();
while idx > 0 {
idx -= 1;
// TODO OPT can use unchecked get here
if data[idx] != data[63] {
break;
}
}
if idx == 0 && data[0] == data[1] {
// All sent is padding
return Err(Status::IncorrectDataParameter);
}
// We have at least one element in the challenge
debug_now!("Found length {}", idx);
&data[..=idx]
};

Ok(Self {
challenge,
slot_cmd: None,
})
}
}

/// TODO: change into enum
Expand Down Expand Up @@ -487,6 +570,11 @@ impl<'l, const C: usize> TryFrom<&'l Data<C>> for Register<'l> {
let (secret_header, secret) = second.as_bytes().split_at(2);

let kind: oath::Kind = secret_header[0].try_into()?;
if kind == Kind::Hmac {
// TODO encode verification logic into separate types
ensure(secret.len() == 20, FAILED_PARSING_ERROR)?;
}

let algorithm: oath::Algorithm = secret_header[0].try_into()?;
let digits = secret_header[1];

Expand Down Expand Up @@ -542,6 +630,36 @@ impl<'l, const C: usize> TryFrom<&'l Data<C>> for Register<'l> {
}
}

impl<'l> Command<'l> {
/// Parse the Yubikey's Challenge-Response request
fn try_parse_yk_req(
class: Class,
instruction: Instruction,
p1: u8,
p2: u8,
data: &'l [u8],
) -> Result<Self, Status> {
let instruction_byte: u8 = instruction.into();
let yk_instruction: oath::YKInstruction = instruction_byte
.try_into()
.map_err(|_| InstructionNotSupportedOrInvalid)?;
match (class.into_inner(), yk_instruction, p1, p2) {
// Get serial
(0x00, oath::YKInstruction::ApiRequest, maybe_cmd_get_serial, 0x00)
if maybe_cmd_get_serial == GetSerial.as_u8() =>
{
Ok(Self::YKSerial)
}
// Get HMAC slot command
(0x00, oath::YKInstruction::ApiRequest, slot, 0x00) => Ok(Self::YKGetHMAC({
YKGetHMAC::try_from(data)?.with_slot(slot)?
})),
// Get status
(0x00, oath::YKInstruction::Status, 0x00, 0x00) => Ok(Self::YKGetStatus),
_ => Err(InstructionNotSupportedOrInvalid),
}
}
}
impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
type Error = Status;
/// The first layer of unraveling the iso7816::Command onion.
Expand All @@ -567,9 +685,9 @@ impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
return Err(Status::LogicalChannelNotSupported);
}

// TODO: should we check `command.expected() == 0`, where specified?

if (0x00, iso7816::Instruction::Select, 0x04, 0x00)
if let Ok(req) = Self::try_parse_yk_req(class, instruction, p1, p2, data) {
Ok(req)
} else if (0x00, iso7816::Instruction::Select, 0x04, 0x00)
== (class.into_inner(), instruction, p1, p2)
{
Ok(Self::Select(Select::try_from(data)?))
Expand Down Expand Up @@ -619,7 +737,7 @@ impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
Self::SetPin(SetPin::try_from(data)?)
}
(0x00, oath::Instruction::SendRemaining, 0x00, 0x00) => Self::SendRemaining,
_ => return Err(Status::InstructionNotSupportedOrInvalid),
_ => return Err(InstructionNotSupportedOrInvalid),
})
}
}
Expand Down
Loading

0 comments on commit e5b3c1b

Please sign in to comment.