mirror of
https://github.com/trussed-dev/piv-authenticator.git
synced 2026-06-20 04:16:15 -07:00
Add vsmartcard example
This commit is contained in:
+10
-3
@@ -2,16 +2,18 @@
|
||||
name = "piv-authenticator"
|
||||
version = "0.0.0-unreleased"
|
||||
authors = ["Nicolas Stalder <n@stalder.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/solokeys/piv-authenticator"
|
||||
documentation = "https://docs.rs/piv-authenticator"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[[example]]
|
||||
name = "virtual"
|
||||
required-features = ["virtual"]
|
||||
|
||||
[dependencies]
|
||||
apdu-dispatch = { version = "0.1", optional = true }
|
||||
delog = "0.1.6"
|
||||
delog = { version = "0.1.5", optional = true }
|
||||
flexiber = { version = "0.1", features = ["derive", "heapless"] }
|
||||
heapless = "0.7"
|
||||
hex-literal = "0.3"
|
||||
@@ -20,15 +22,20 @@ iso7816 = "0.1"
|
||||
serde = { version = "1", default-features = false }
|
||||
trussed = "0.1"
|
||||
untrusted = "0.9"
|
||||
vpicc = { version = "0.1.0", optional = true }
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
littlefs2 = "0.3.2"
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
trussed = { version = "0.1.0", features = ["virt"] }
|
||||
env_logger = "0.9"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
strict-pin = []
|
||||
std = []
|
||||
virtual = ["std", "vpicc","trussed/virt"]
|
||||
|
||||
log-all = []
|
||||
log-none = []
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (C) 2022 Nitrokey GmbH
|
||||
// SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
// To use this, make sure that you have vpcd from vsmartcard installed and configured (e. g.
|
||||
// install vsmartcard-vpcd on Debian). You might have to restart your pcscd, e. g.
|
||||
// `systemctl restart pcscd pcscd.socket`.
|
||||
//
|
||||
// Now you should be able to see the card in `pcsc_scan` and talk to it with `piv-tool`r
|
||||
//
|
||||
// Set `RUST_LOG=piv_authenticator::card=info` to see the executed commands.
|
||||
|
||||
// TODO: add CLI
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
trussed::virt::with_ram_client("opcard", |client| {
|
||||
let card = piv_authenticator::Authenticator::new(client);
|
||||
let mut virtual_card = piv_authenticator::vpicc::VirtualCard::new(card);
|
||||
let vpicc = vpicc::connect().expect("failed to connect to vpicc");
|
||||
vpicc
|
||||
.run(&mut virtual_card)
|
||||
.expect("failed to run virtual smartcard");
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -83,7 +83,7 @@ impl TryFrom<&[u8]> for GetData {
|
||||
.try_into()
|
||||
.map_err(|_| Status::IncorrectDataParameter)?;
|
||||
|
||||
info_now!("request to GetData for container {:?}", container);
|
||||
info!("request to GetData for container {:?}", container);
|
||||
Ok(Self(container))
|
||||
}
|
||||
}
|
||||
|
||||
+31
-25
@@ -1,8 +1,11 @@
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
#![cfg_attr(not(any(test, feature = "std")), no_std)]
|
||||
|
||||
#[cfg(not(feature = "delog"))]
|
||||
#[macro_use]
|
||||
extern crate delog;
|
||||
generate_macros!();
|
||||
extern crate log;
|
||||
|
||||
#[cfg(feature = "delog")]
|
||||
delog::generate_macros!();
|
||||
|
||||
#[macro_use(hex)]
|
||||
extern crate hex_literal;
|
||||
@@ -18,6 +21,9 @@ pub mod piv_types;
|
||||
pub mod state;
|
||||
pub use piv_types::{Pin, Puk};
|
||||
|
||||
#[cfg(feature = "virtual")]
|
||||
pub mod vpicc;
|
||||
|
||||
use core::convert::TryInto;
|
||||
|
||||
use flexiber::EncodableHeapless;
|
||||
@@ -70,7 +76,7 @@ where
|
||||
reply: &mut Data<R>,
|
||||
) -> Result {
|
||||
use piv_types::Algorithms::*;
|
||||
info_now!("selecting PIV maybe");
|
||||
info!("selecting PIV maybe");
|
||||
|
||||
let application_property_template = piv_types::ApplicationPropertyTemplate::default()
|
||||
.with_application_label(APPLICATION_LABEL)
|
||||
@@ -80,7 +86,7 @@ where
|
||||
application_property_template
|
||||
.encode_to_heapless_vec(reply)
|
||||
.unwrap();
|
||||
info_now!("returning: {}", hex_str!(reply));
|
||||
info!("returning: {:02X?}", reply);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -90,7 +96,7 @@ where
|
||||
reply: &mut Data<R>,
|
||||
) -> Result {
|
||||
// need to implement Debug on iso7816::Command
|
||||
// info_now!("PIV responding to {:?}", command);
|
||||
// info!("PIV responding to {:?}", command);
|
||||
let last_or_only = command.class().chain().last_or_only();
|
||||
|
||||
// TODO: avoid owned copy?
|
||||
@@ -124,7 +130,7 @@ where
|
||||
|
||||
// parse Iso7816Command as PivCommand
|
||||
let command: Command = (&entire_command).try_into()?;
|
||||
info_now!("parsed: {:?}", &command);
|
||||
info!("parsed: {:?}", &command);
|
||||
|
||||
match command {
|
||||
Command::Verify(verify) => self.verify(verify),
|
||||
@@ -281,14 +287,14 @@ where
|
||||
// return Err(Status::LogicalChannelNotSupported);
|
||||
// }
|
||||
|
||||
// // info_now!("CLA = {:?}", &command.class());
|
||||
// info_now!("INS = {:?}, P1 = {:X}, P2 = {:X}",
|
||||
// // info!("CLA = {:?}", &command.class());
|
||||
// info!("INS = {:?}, P1 = {:X}, P2 = {:X}",
|
||||
// &command.instruction(),
|
||||
// command.p1, command.p2,
|
||||
// );
|
||||
// // info_now!("extended = {:?}", command.extended);
|
||||
// // info!("extended = {:?}", command.extended);
|
||||
|
||||
// // info_now!("INS = {:?}" &command.instruction());
|
||||
// // info!("INS = {:?}" &command.instruction());
|
||||
// match command.instruction() {
|
||||
// Instruction::GetData => self.get_data(command, reply),
|
||||
// Instruction::PutData => self.put_data(command),
|
||||
@@ -431,7 +437,7 @@ where
|
||||
return Err(Status::IncorrectDataParameter);
|
||||
}
|
||||
|
||||
info_now!("looking for keyreference");
|
||||
info!("looking for keyreference");
|
||||
let key_handle = match self
|
||||
.state
|
||||
.persistent(&mut self.trussed)
|
||||
@@ -447,7 +453,7 @@ where
|
||||
let signature = try_syscall!(self.trussed.sign_ed255(key_handle, commitment))
|
||||
.map_err(|_error| {
|
||||
// NoSuchKey
|
||||
debug_now!("{:?}", &_error);
|
||||
debug!("{:?}", &_error);
|
||||
Status::UnspecifiedNonpersistentExecutionError
|
||||
})?
|
||||
.signature;
|
||||
@@ -495,8 +501,8 @@ where
|
||||
self.state.runtime.command_cache = None;
|
||||
|
||||
if our_challenge != response {
|
||||
debug_now!("{:?}", &our_challenge);
|
||||
debug_now!("{:?}", &response);
|
||||
debug!("{:?}", &our_challenge);
|
||||
debug!("{:?}", &response);
|
||||
return Err(Status::IncorrectDataParameter);
|
||||
}
|
||||
|
||||
@@ -768,7 +774,7 @@ where
|
||||
})
|
||||
})
|
||||
.map_err(|_e| {
|
||||
info_now!("error parsing GenerateAsymmetricKeypair: {:?}", &_e);
|
||||
info!("error parsing GenerateAsymmetricKeypair: {:?}", &_e);
|
||||
Status::IncorrectDataParameter
|
||||
})?;
|
||||
|
||||
@@ -837,7 +843,7 @@ where
|
||||
))
|
||||
.serialized_key;
|
||||
|
||||
// info_now!("supposed SEC1 pubkey, len {}: {:X?}", serialized_public_key.len(), &serialized_public_key);
|
||||
// info!("supposed SEC1 pubkey, len {}: {:X?}", serialized_public_key.len(), &serialized_public_key);
|
||||
|
||||
// P256 SEC1 has 65 bytes, Ed255 pubkeys have 32
|
||||
// let l2 = 65;
|
||||
@@ -853,7 +859,7 @@ where
|
||||
}
|
||||
|
||||
pub fn put_data(&mut self, command: &iso7816::Command<C>) -> Result {
|
||||
info_now!("PutData");
|
||||
info!("PutData");
|
||||
if command.p1 != 0x3f || command.p2 != 0xff {
|
||||
return Err(Status::IncorrectP1OrP2Parameter);
|
||||
}
|
||||
@@ -881,11 +887,11 @@ where
|
||||
// }).unwrap();
|
||||
})
|
||||
.map_err(|_e| {
|
||||
info_now!("error parsing PutData: {:?}", &_e);
|
||||
info!("error parsing PutData: {:?}", &_e);
|
||||
Status::IncorrectDataParameter
|
||||
})?;
|
||||
|
||||
// info_now!("PutData in {:?}: {:?}", data_object, data);
|
||||
// info!("PutData in {:?}: {:?}", data_object, data);
|
||||
|
||||
if data_object == [0x5f, 0xc1, 0x09] {
|
||||
// "Printed Information", supposedly
|
||||
@@ -976,7 +982,7 @@ where
|
||||
piv_types::CardCapabilityContainer::default()
|
||||
.encode_to_heapless_vec(reply)
|
||||
.unwrap();
|
||||
info_now!("returning CCC {}", hex_str!(reply));
|
||||
info!("returning CCC {:02X?}", reply);
|
||||
}
|
||||
|
||||
// '5FC1 02' (351B)
|
||||
@@ -986,14 +992,14 @@ where
|
||||
.with_guid(guid)
|
||||
.encode_to_heapless_vec(reply)
|
||||
.unwrap();
|
||||
info_now!("returning CHUID {}", hex_str!(reply));
|
||||
info!("returning CHUID {:02X?}", reply);
|
||||
}
|
||||
|
||||
// // '5FC1 05' (351B)
|
||||
// Container::X509CertificateForPivAuthentication => {
|
||||
// // return Err(Status::NotFound);
|
||||
|
||||
// // info_now!("loading 9a cert");
|
||||
// // info!("loading 9a cert");
|
||||
// // it seems like fetching this certificate is the way Filo's agent decides
|
||||
// // whether the key is "already setup":
|
||||
// // https://github.com/FiloSottile/yubikey-agent/blob/8781bc0082db5d35712a2244e3ab3086f415dd59/setup.go#L69-L70
|
||||
@@ -1001,7 +1007,7 @@ where
|
||||
// trussed::types::Location::Internal,
|
||||
// trussed::types::PathBuf::from(b"authentication-key.x5c"),
|
||||
// )).map_err(|_| {
|
||||
// // info_now!("error loading: {:?}", &e);
|
||||
// // info!("error loading: {:?}", &e);
|
||||
// Status::NotFound
|
||||
// } )?.data;
|
||||
|
||||
@@ -1029,7 +1035,7 @@ where
|
||||
instruction: YubicoPivExtension,
|
||||
reply: &mut Data<R>,
|
||||
) -> Result {
|
||||
info_now!("yubico extension: {:?}", &instruction);
|
||||
info!("yubico extension: {:?}", &instruction);
|
||||
match instruction {
|
||||
YubicoPivExtension::GetSerial => {
|
||||
// make up a 4-byte serial
|
||||
|
||||
+2
-2
@@ -459,7 +459,7 @@ where
|
||||
}
|
||||
|
||||
pub fn initialize(trussed: &'t mut T) -> Self {
|
||||
info_now!("initializing PIV state");
|
||||
info!("initializing PIV state");
|
||||
let management_key = syscall!(trussed.unsafe_inject_shared_key(
|
||||
YUBICO_DEFAULT_MANAGEMENT_KEY,
|
||||
trussed::types::Location::Internal,
|
||||
@@ -527,7 +527,7 @@ where
|
||||
try_syscall!(trussed.read_file(Location::Internal, PathBuf::from(Self::FILENAME)));
|
||||
if let Ok(data) = data {
|
||||
let previous_state = trussed::cbor_deserialize(&data.data).map_err(|_err| {
|
||||
info!("cbor deser error: {_err:?}", e);
|
||||
info!("cbor deser error: {_err:?}");
|
||||
info!("data: {:X?}", &data);
|
||||
});
|
||||
if let Ok(state) = previous_state {
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// Copyright (C) 2022 Nitrokey GmbH
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
use iso7816::{command::FromSliceError, Command, Status};
|
||||
use trussed::virt::{Client, Ram};
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use crate::Authenticator;
|
||||
|
||||
const REQUEST_LEN: usize = 7609;
|
||||
const RESPONSE_LEN: usize = 7609;
|
||||
const BUFFER_LEN: usize = 7609;
|
||||
|
||||
/// Virtual PIV smartcard implementation.
|
||||
///
|
||||
/// This struct provides a virtual PIV smart card implementation that can be used with
|
||||
/// `vpicc-rs` and [`vsmartcard`](https://frankmorgner.github.io/vsmartcard/) to emulate the card.
|
||||
pub struct VirtualCard {
|
||||
request_buffer: RequestBuffer<REQUEST_LEN>,
|
||||
response_buffer: ResponseBuffer<RESPONSE_LEN>,
|
||||
card: Authenticator<Client<Ram>, BUFFER_LEN>,
|
||||
}
|
||||
|
||||
impl VirtualCard {
|
||||
/// Creates a new virtual smart card from the given card.
|
||||
pub fn new(card: Authenticator<Client<Ram>, BUFFER_LEN>) -> Self {
|
||||
Self {
|
||||
request_buffer: Default::default(),
|
||||
response_buffer: Default::default(),
|
||||
card,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&mut self, request: &[u8]) -> (&[u8], Status) {
|
||||
parse_command(request)
|
||||
.and_then(|command| self.request_buffer.handle(command))
|
||||
.map(|command| {
|
||||
command
|
||||
.map(|command| {
|
||||
self.response_buffer
|
||||
.handle(&command, |c, b| self.card.respond(c, b))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_else(|status| (&[], status))
|
||||
}
|
||||
}
|
||||
|
||||
impl vpicc::VSmartCard for VirtualCard {
|
||||
fn power_on(&mut self) {}
|
||||
|
||||
fn power_off(&mut self) {
|
||||
self.card.deselect();
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.card.deselect();
|
||||
}
|
||||
|
||||
fn execute(&mut self, request: &[u8]) -> Vec<u8> {
|
||||
trace!("Received request {:x?}", request);
|
||||
let (data, status) = self.handle(request);
|
||||
let response = make_response(data, status);
|
||||
trace!("Sending response {:x?}", response);
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_command(data: &[u8]) -> Result<Command<REQUEST_LEN>, Status> {
|
||||
data.try_into().map_err(|err| {
|
||||
warn!("Failed to parse command: {err:?}");
|
||||
match err {
|
||||
FromSliceError::InvalidSliceLength
|
||||
| FromSliceError::TooShort
|
||||
| FromSliceError::TooLong => Status::WrongLength,
|
||||
FromSliceError::InvalidClass => Status::ClassNotSupported,
|
||||
FromSliceError::InvalidFirstBodyByteForExtended => Status::UnspecifiedCheckingError,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn make_response(data: &[u8], status: Status) -> Vec<u8> {
|
||||
let status: [u8; 2] = status.into();
|
||||
let mut response = Vec::with_capacity(data.len() + 2);
|
||||
response.extend_from_slice(data);
|
||||
response.extend_from_slice(&status);
|
||||
response
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RequestBuffer<const N: usize> {
|
||||
command: Option<Command<N>>,
|
||||
}
|
||||
|
||||
impl<const N: usize> RequestBuffer<N> {
|
||||
pub fn handle(&mut self, command: Command<N>) -> Result<Option<Command<N>>, Status> {
|
||||
if let Some(buffer) = &mut self.command {
|
||||
buffer
|
||||
.extend_from_command(&command)
|
||||
.map_err(|_| Status::WrongLength)?;
|
||||
}
|
||||
if command.class().chain().last_or_only() {
|
||||
if let Some(buffer) = self.command.take() {
|
||||
Ok(Some(buffer))
|
||||
} else {
|
||||
Ok(Some(command))
|
||||
}
|
||||
} else {
|
||||
if self.command.is_none() {
|
||||
self.command = Some(command);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ResponseBuffer<const N: usize> {
|
||||
buffer: heapless::Vec<u8, N>,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> ResponseBuffer<N> {
|
||||
pub fn handle<
|
||||
const C: usize,
|
||||
F: FnOnce(&Command<C>, &mut heapless::Vec<u8, N>) -> Result<(), Status>,
|
||||
>(
|
||||
&mut self,
|
||||
command: &Command<C>,
|
||||
exec: F,
|
||||
) -> (&[u8], Status) {
|
||||
if command.instruction() != iso7816::Instruction::GetResponse {
|
||||
self.buffer.clear();
|
||||
self.offset = 0;
|
||||
if let Err(status) = exec(command, &mut self.buffer) {
|
||||
return (&[], status);
|
||||
}
|
||||
}
|
||||
self.response(command.expected())
|
||||
}
|
||||
|
||||
fn response(&mut self, n: usize) -> (&[u8], Status) {
|
||||
let n = n.min(self.buffer.len() - self.offset);
|
||||
let data = &self.buffer[self.offset..][..n];
|
||||
self.offset += n;
|
||||
let status = if self.offset >= self.buffer.len() {
|
||||
Status::Success
|
||||
} else {
|
||||
let rest = self.buffer.len() - self.offset;
|
||||
Status::MoreAvailable(u8::try_from(rest).unwrap_or(u8::MAX))
|
||||
};
|
||||
(data, status)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user