Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cd6665b
wip: update device engagement for nfc
Ryanmtate Apr 4, 2025
4211b69
add nfc device engagement handover ndef records
Ryanmtate May 1, 2025
6904164
wip: encapsulate in nfc_handover file
Ryanmtate May 16, 2025
4e4b533
update test cases to use latest device engagement
Ryanmtate May 16, 2025
c7bb085
replace hand-rolled NDEF with ndef_rs
alichay Jul 15, 2025
610b2c1
wip: refactor device engagement, decouple from iso mdl presentation s…
Ryanmtate Jul 29, 2025
d7f3774
draft: separate out BLE for NFC
alichay Jul 30, 2025
2c363c0
initial impl of direct handover
alichay Aug 19, 2025
2640ae1
wip: construct urn handover select ndef message
Ryanmtate Aug 19, 2025
cee5bfa
update nfc device engagement handover methods
Ryanmtate Aug 20, 2025
9315f1b
revert handover direct message to use Tp record type
Ryanmtate Aug 20, 2025
51e2878
add tnep status record ndef record
Ryanmtate Aug 20, 2025
0e8f115
add type annotations for handover message slice
Ryanmtate Aug 20, 2025
55ea559
first signs of life for APDU/TNEP handling
alichay Aug 29, 2025
826bac1
temp static handover. presentation broken
alichay Sep 4, 2025
4638ace
Bump time version
alichay Sep 4, 2025
81345e7
fix QR presentation: return QR engagement to status quo
alichay Sep 4, 2025
1fc2343
validate static NDEF - this is known correct
alichay Sep 4, 2025
295b1a1
flip UUID bytes in `ac` record
alichay Sep 15, 2025
3222bd0
claim handover 1.5 instead of 1.2
alichay Sep 23, 2025
e171e27
store NFC messages in Handover enum
alichay Sep 24, 2025
c9db1fa
cleanup pass
alichay Sep 29, 2025
c8b31c5
remove unfinished negotiated handover
alichay Sep 29, 2025
40e8c24
handover -> apdu_handover, ndef -> ndef_handover
alichay Sep 29, 2025
16c381c
add strict mode, for only responding after selecting mDOC
alichay Oct 6, 2025
38c1095
cleanup: revert unneeded changes
alichay Oct 6, 2025
b60c9ef
ensure device engagement types are serializable
Ryanmtate Oct 10, 2025
3b06da6
appease clippy - explicitly elide lifetimes
alichay Oct 29, 2025
9c40304
Merge remote-tracking branch 'origin/main' into feat/isomdl-device-en…
alichay Oct 29, 2025
4657dfb
rm src/.DS_Store
alichay Oct 30, 2025
46fda3e
address TODOs and block off negotiated handover
alichay Nov 11, 2025
bf5655b
ephemeral_key: pub -> pub(crate)
alichay Nov 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ hmac = "0.12.1"
aes = "0.8.2"
sec1 = "0.7.1"
uuid = { version = "1.3", features = ["v1", "v4", "std", "rng", "serde"] }
time = { version = "0.3.20", features = ["formatting", "parsing", "macros"] }
time = { version = "0.3.35", features = ["formatting", "parsing", "macros"] }
zeroize = { version = "1.5", features = ["zeroize_derive"] }
signature = { version = "2.0.0", features = ["std"] }
async-signature = "0.3.0"
Expand Down Expand Up @@ -63,6 +63,7 @@ ciborium = "0.2.2"
digest = "0.10.7"
tracing = "0.1.41"
sha1 = "0.10.6"
ndef-rs = "0.2.2"

[dev-dependencies]
hex = "0.4.3"
Expand Down
8 changes: 5 additions & 3 deletions src/definitions/device_engagement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
//! It includes fields such as the `version`, `security details, `device retrieval methods, `server retrieval methods, and `protocol information.
//!
//! The module also provides implementations for conversions between [DeviceEngagement] and [ciborium::Value], as well as other utility functions.
pub mod error;
pub mod nfc;
pub mod nfc_options;

use std::{collections::BTreeMap, vec};

use anyhow::Result;
Expand All @@ -19,8 +23,6 @@ use crate::definitions::helpers::Tag24;
use crate::definitions::helpers::{ByteStr, NonEmptyVec};
use crate::definitions::CoseKey;

pub mod error;
pub mod nfc_options;
pub type EDeviceKeyBytes = Tag24<CoseKey>;
pub type EReaderKeyBytes = Tag24<CoseKey>;

Expand Down Expand Up @@ -102,7 +104,7 @@ pub struct ServerRetrievalMethods {
}

/// Represents the options for `Bluetooth Low Energy` (BLE) device engagement.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(try_from = "ciborium::Value", into = "ciborium::Value")]
pub struct BleOptions {
/// The peripheral server mode for `BLE` device engagement.
Expand Down
277 changes: 277 additions & 0 deletions src/definitions/device_engagement/nfc/apdu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
use strum_macros::EnumIter;

// This has been written according to ISO 7816-4 (2005).
// This only implements what is required for NFC handover to BLE.

use crate::definitions::device_engagement::nfc::{
impl_partial_enum,
util::{IntoRaw, KnownOrRaw},
};

pub struct Response {
pub code: ResponseCode,
pub payload: Vec<u8>,
}
impl From<Response> for Vec<u8> {
fn from(response: Response) -> Self {
let mut response_bytes = Vec::with_capacity(2 + response.payload.len());
response_bytes.extend_from_slice(&response.payload);
response_bytes.extend_from_slice(&response.code.to_bytes());
response_bytes
}
}

#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseCode {
Ok = 0x9000,
IncorrectLength = 0x6700,
IncorrectP1OrP2 = 0x6B00,
ConditionsNotSatisfied = 0x6985,
FileOrApplicationNotFound = 0x6A82,
InstructionNotSupported = 0x6D00,
Unspecified = 0x6F00,
}

impl ResponseCode {
pub fn to_bytes(self) -> [u8; 2] {
[(self as u16 >> 8) as u8, self as u16 as u8]
}
}

impl From<ResponseCode> for Response {
fn from(code: ResponseCode) -> Self {
Response {
code,
payload: Vec::new(),
}
}
}

#[repr(u16)]
#[derive(Debug, EnumIter, Clone, Copy, PartialEq, Eq)]
pub enum FileId {
CapabilityContainer = 0xE103,
NdefFile = 0xE104,
}
impl_partial_enum!(FileId, u16);

#[allow(dead_code)]
#[derive(Debug)]
pub enum Apdu<'a> {
SelectFile {
occurrence: select::Occurrence,
control_info: select::ControlInfo,
file_id: KnownOrRaw<u16, FileId>,
},
SelectAid {
occurrence: select::Occurrence,
control_info: select::ControlInfo,
aid: &'a [u8],
},
ReadBinary {
slice: std::ops::Range<usize>,
},
UpdateBinary {
offset: usize,
data: &'a [u8],
},
}

macro_rules! apdu_fail {
($code:expr) => {
return Err(Response::from($code))
};
}

pub mod select {
use super::{Response, ResponseCode};

#[repr(u8)]
#[rustfmt::skip]
#[derive(strum_macros::FromRepr, Debug, Clone, Copy)]
pub enum Occurrence {
FirstOrOnly = 0b0000,
Last = 0b0001,
Next = 0b0010,
Prev = 0b0011,
}

#[repr(u8)]
#[rustfmt::skip]
#[derive(strum_macros::FromRepr, Debug, Clone, Copy)]
pub enum ControlInfo {
FciTemplate = 0b0000,
FcpTemplate = 0b0100,
FmdTemplate = 0b1000,
NoResponse = 0b1100,
}

pub fn get_request_info(p2: u8) -> (Occurrence, ControlInfo) {
let (occurrence, control_info) = (p2 & 0b0011, p2 & 0b1100);

// Safety: These enums cover the entire possible bit range of the masked values.
let occurrence = Occurrence::from_repr(occurrence).unwrap();
let control_info = ControlInfo::from_repr(control_info).unwrap();

(occurrence, control_info)
}

impl ControlInfo {
pub fn get_payload(&self, _full_id: &[u8]) -> Result<Response, Response> {
match *self {
ControlInfo::NoResponse => Ok(ResponseCode::Ok.into()),
// The initial 6f header can be omitted, and the name payload
// can be omitted if the full ID is provided.
// Since we only match on full ID, this means the entire payload is optional.
ControlInfo::FciTemplate => Ok(ResponseCode::Ok.into()),
_ => apdu_fail!(ResponseCode::InstructionNotSupported),
}
}
}
}

impl<'a> Apdu<'a> {
pub fn parse(command_bytes: &'a [u8]) -> Result<Self, Response> {
if command_bytes.len() < 4 {
apdu_fail!(ResponseCode::IncorrectLength);
}

let (cla, ins, p1, p2) = (
command_bytes[0],
command_bytes[1],
command_bytes[2],
command_bytes[3],
);

let (payload_len, l_c_len) = if command_bytes.len() > 4 {
if command_bytes[4] == 0x00 && command_bytes.len() > 6 {
// 3 byte L_c (first byte is 0x00)
let payload_len_bytes = &command_bytes[5..7];
(
u16::from_be_bytes([payload_len_bytes[0], payload_len_bytes[1]]),
3,
)
} else {
(command_bytes[4] as u16, 1)
}
} else {
(0, 0)
};
let (payload_len, l_c_len) = (payload_len as usize, l_c_len as usize);

let command_remainder = &command_bytes[4 + l_c_len..];

let mut response_len = 0;
if command_remainder.len() > payload_len {
if command_remainder.len() - payload_len != l_c_len {
tracing::error!(
"Expected the remainder({}) after payload len({}) to be same as Lc len ({})",
command_remainder.len(),
payload_len,
l_c_len
);
apdu_fail!(ResponseCode::Unspecified);
}
let resp_bytes = &command_remainder[payload_len..];
response_len = match l_c_len {
1 => resp_bytes[0] as usize,
3 => u16::from_be_bytes([resp_bytes[1], resp_bytes[2]]) as usize,
_ => {
unreachable!()
}
}
}

tracing::debug!("Processing APDU command: CLA: {cla}, INS: {ins}, P1: {p1}, P2: {p2}, LC Len: {l_c_len}, Payload Length: {payload_len}, Resp len: {response_len}");

let ins_bit1 = (ins & 0b0000_0001) != 0;
let p1_bit8 = (p1 & 0b1000_0000) != 0;

Ok(match (ins, p1, p2) {
(0xA4, _, _) => {
// Select §7.1.1
let (occurrence, control_info) = select::get_request_info(p2);

match p1 {
0x00 => {
// Select file
let file_id_raw = &command_remainder[..payload_len];
if file_id_raw.len() != 2 {
apdu_fail!(ResponseCode::IncorrectLength);
}
let file_id = u16::from_be_bytes([file_id_raw[0], file_id_raw[1]]).into();
Apdu::SelectFile {
occurrence,
control_info,
file_id,
}
}
0x04 => {
// Select AID
let aid = &command_remainder[..payload_len];
Apdu::SelectAid {
occurrence,
control_info,
aid,
}
}
_ => {
apdu_fail!(ResponseCode::InstructionNotSupported)
}
}
}
(0xB0, _, _) => {
// Read binary §7.2.3
let response_len = payload_len;
#[allow(unused)]
let payload_len = (); // Shadow payload_len so it's not erroneously referenced.

let offset;
match (ins_bit1, p1_bit8) {
(false, true) => {
// We don't support P1 containing an EF identifier right now.
// See ISO 7816-4:2005 §7.2.2
apdu_fail!(ResponseCode::Unspecified)
}
(false, false) => {
offset = u16::from_be_bytes([p1, p2]) as usize;
}
_ => {
// We don't support P1 containing an EF identifier right now.
// Since this is a different instruction, we just return instruction unsupported.
apdu_fail!(ResponseCode::InstructionNotSupported)
}
}

Apdu::ReadBinary {
slice: offset..offset + response_len,
}
}
(0xD6, _, _) => {
// Update binary §7.2.5
let offset;
match (ins_bit1, p1_bit8) {
(false, true) => {
// We don't support P1 containing an EF identifier right now.
// See ISO 7816-4:2005 §7.2.2
apdu_fail!(ResponseCode::Unspecified)
}
(false, false) => {
offset = u16::from_be_bytes([p1, p2]) as usize;
}
_ => {
// We don't support P1 containing an EF identifier right now.
// Since this is a different instruction, we just return instruction unsupported.
apdu_fail!(ResponseCode::InstructionNotSupported)
}
}
Apdu::UpdateBinary {
offset,
data: &command_remainder[0..payload_len],
}
}
_ => apdu_fail!(ResponseCode::InstructionNotSupported),
})
}
}
Loading