Add vsmartcard example

This commit is contained in:
Sosthène Guédon
2022-10-25 10:10:39 +02:00
parent 65fd2b42c8
commit 89d8410324
6 changed files with 224 additions and 31 deletions
+10 -3
View File
@@ -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 = []
+25
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}