Merge branch 'put-data'

This commit is contained in:
Sosthène Guédon
2023-03-20 12:16:56 +01:00
27 changed files with 2360 additions and 791 deletions
+7
View File
@@ -0,0 +1,7 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: piv-authenticator
Source: https://github.com/Nitrokey/piv-authenticator
Files: tests/default_admin_key
Copyright: 2022 Nitrokey GmbH
License: LGPL-3.0-only
+29 -4
View File
@@ -14,6 +14,11 @@ documentation = "https://docs.rs/piv-authenticator"
name = "virtual"
required-features = ["virtual"]
[[example]]
name = "usbip"
required-features = ["apdu-dispatch"]
[dependencies]
apdu-dispatch = { version = "0.1", optional = true }
delog = { version = "0.1.5", optional = true }
@@ -23,11 +28,13 @@ hex-literal = "0.3"
interchange = "0.2.2"
iso7816 = "0.1"
serde = { version = "1", default-features = false, features = ["derive"] }
trussed = "0.1"
trussed = { version = "0.1" }
untrusted = "0.9"
vpicc = { version = "0.1.0", optional = true }
log = "0.4"
heapless-bytes = "0.3.0"
subtle = { version = "2", default-features = false }
trussed-rsa-alloc = { git = "https://github.com/Nitrokey/trussed-rsa-backend.git", rev = "e29b26ab3800217b7eb73ecde67134bbb3acb9da", features = ["raw"] }
[dev-dependencies]
littlefs2 = "0.3.2"
@@ -37,15 +44,25 @@ env_logger = "0.9"
serde = { version = "1", features = ["derive"] }
serde_cbor = { version = "0.11", features = ["std"] }
hex = "0.4"
test-log = "0.2"
test-log = "0.2.11"
ron = "0.8"
des = "0.8"
aes = "0.8.2"
stoppable_thread = "0.2.1"
expectrl = "0.6.0"
# Examples
trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner", default-features = false, features = ["ccid"], rev = "d2957b6c24c2b0cafbbfacd6fecd62c80943630b"}
usbd-ccid = { version = "0.2.0", features = ["highspeed-usb"]}
rand = "0.8.5"
[features]
default = []
strict-pin = []
std = []
virtual = ["std", "vpicc","trussed/virt"]
virtual = ["std", "vpicc", "trussed-rsa-alloc/virt"]
pivy-tests = []
opensc-tests = []
log-all = []
log-none = []
@@ -55,4 +72,12 @@ log-warn = []
log-error = []
[patch.crates-io]
trussed = { git = "https://github.com/trussed-dev/trussed", rev = "28478f8abed11d78c51e6a6a32326821ed61957a"}
# trussed = { git = "https://github.com/Nitrokey/trussed", tag = "v0.1.0-nitrokey-4"}
trussed = { git = "https://github.com/trussed-dev/trussed", rev = "d9276a689d68ffeb4c5d9ac635b18232be172f45"}
# littlefs2 = { git = "https://github.com/Nitrokey/littlefs2", tag = "v0.3.2-nitrokey-1" }
[profile.dev.package.rsa]
opt-level = 2
[profile.dev.package.num-bigint-dig]
opt-level = 2
+9 -4
View File
@@ -4,6 +4,7 @@
.NOTPARALLEL:
export RUST_LOG ?= info,cargo_tarpaulin=off
TEST_FEATURES ?=virtual,pivy-tests,opensc-tests
.PHONY: build-cortex-m4
build-cortex-m4:
@@ -11,24 +12,28 @@ build-cortex-m4:
.PHONY: test
test:
cargo test --features virtual
cargo test --features $(TEST_FEATURES)
.PHONY: check
check:
RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets
.PHONY: lint
lint:
cargo fmt --check
cargo check --all-targets --all-features
RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets
cargo clippy --all-targets --all-features -- -Dwarnings
RUSTDOCFLAGS='-Dwarnings' cargo doc --all-features
reuse lint
.PHONY: tarpaulin
tarpaulin:
cargo tarpaulin --features virtual -o Html -o Xml
cargo tarpaulin --features $(TEST_FEATURES) -o Html -o Xml
.PHONY: example
example:
cargo run --example virtual --features virtual
.PHONY: ci
ci: check tarpaulin
ci: lint tarpaulin
+11 -1
View File
@@ -3,7 +3,15 @@
FROM docker.io/rust:latest
RUN apt update && apt install --yes scdaemon libclang-dev llvm python3-pip vsmartcard-vpcd pkg-config nettle-dev libpcsclite-dev
RUN apt update && apt install --yes libpcsclite-dev \
&& wget https://github.com/arekinath/pivy/releases/download/v0.10.0/pivy-0.10.0-src.tar.gz \
&& tar xvf pivy-0.10.0-src.tar.gz \
&& cd pivy-0.10.0 \
&& make pivy-tool
FROM docker.io/rust:latest
RUN apt update && apt install --yes scdaemon libclang-dev llvm python3-pip vsmartcard-vpcd pkg-config nettle-dev libpcsclite-dev opensc
RUN python3 -m pip install reuse
@@ -14,6 +22,8 @@ RUN cargo search
ENV CARGO_HOME=/app/.cache/cargo
COPY --from=0 pivy-0.10.0/pivy-tool /bin/pivy-tool
WORKDIR /app
COPY entrypoint.sh /entrypoint.sh
+54
View File
@@ -0,0 +1,54 @@
// Copyright (C) 2022 Nitrokey GmbH
// SPDX-License-Identifier: CC0-1.0
use trussed::virt::{self, Ram, UserInterface};
use trussed::{ClientImplementation, Platform};
use piv_authenticator as piv;
use trussed_usbip::Syscall;
const MANUFACTURER: &str = "Nitrokey";
const PRODUCT: &str = "Nitrokey 3";
const VID: u16 = 0x20a0;
const PID: u16 = 0x42b2;
struct PivApp {
piv: piv::Authenticator<ClientImplementation<Syscall<virt::Platform<Ram>>>>,
}
impl trussed_usbip::Apps<ClientImplementation<Syscall<virt::Platform<Ram>>>, ()> for PivApp {
fn new(
make_client: impl Fn(&str) -> ClientImplementation<Syscall<virt::Platform<Ram>>>,
_data: (),
) -> Self {
PivApp {
piv: piv::Authenticator::new(make_client("piv")),
}
}
fn with_ccid_apps<T>(
&mut self,
f: impl FnOnce(&mut [&mut dyn apdu_dispatch::App<7609, 7609>]) -> T,
) -> T {
f(&mut [&mut self.piv])
}
}
fn main() {
env_logger::init();
let options = trussed_usbip::Options {
manufacturer: Some(MANUFACTURER.to_owned()),
product: Some(PRODUCT.to_owned()),
serial_number: Some("TEST".into()),
vid: VID,
pid: PID,
};
trussed_usbip::Runner::new(virt::Ram::default(), options)
.init_platform(move |platform| {
let ui: Box<dyn trussed::platform::UserInterface + Send + Sync> =
Box::new(UserInterface::new());
platform.user_interface().set_inner(ui);
})
.exec::<PivApp, _, _>(|_platform| {});
}
+1 -1
View File
@@ -14,7 +14,7 @@
fn main() {
env_logger::init();
trussed::virt::with_ram_client("piv-authenticator", |client| {
trussed_rsa_alloc::virt::with_ram_client("piv-authenticator", |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");
+60 -23
View File
@@ -11,11 +11,13 @@ use core::convert::{TryFrom, TryInto};
// use flexiber::Decodable;
use iso7816::{Instruction, Status};
use crate::container::{Container, KeyReference};
use crate::state::TouchPolicy;
pub use crate::{
container::{
self as containers, AttestKeyReference, AuthenticateKeyReference,
ChangeReferenceKeyReference, GenerateAsymmetricKeyReference, VerifyKeyReference,
self as containers, AsymmetricKeyReference, AttestKeyReference, AuthenticateKeyReference,
ChangeReferenceKeyReference, GenerateKeyReference, VerifyKeyReference,
},
piv_types, Pin, Puk,
};
@@ -30,7 +32,7 @@ pub enum YubicoPivExtension {
SetPinRetries,
Attest(AttestKeyReference),
GetSerial, // also used via 0x01
GetMetadata,
GetMetadata(KeyReference),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -51,14 +53,14 @@ pub enum Command<'l> {
/// Change PIN or PUK
ChangeReference(ChangeReference),
/// If the PIN is blocked, reset it using the PUK
ResetPinRetries(ResetPinRetries),
ResetRetryCounter(ResetRetryCounter),
/// The most general purpose method, performing actual cryptographic operations
///
/// In particular, this can also decrypt or similar.
GeneralAuthenticate(GeneralAuthenticate),
/// Store a data object / container.
PutData(PutData),
GenerateAsymmetric(GenerateAsymmetricKeyReference),
PutData(PutData<'l>),
GenerateAsymmetric(GenerateKeyReference),
/* Yubico commands */
YkExtension(YubicoPivExtension),
@@ -66,8 +68,8 @@ pub enum Command<'l> {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GeneralAuthenticate {
algorithm: piv_types::Algorithms,
key_reference: AuthenticateKeyReference,
pub algorithm: piv_types::Algorithms,
pub key_reference: AuthenticateKeyReference,
}
impl<'l> Command<'l> {
@@ -111,8 +113,7 @@ impl TryFrom<&[u8]> for GetData {
if tagged_slice.tag() != flexiber::Tag::application(0x1C) {
return Err(Status::IncorrectDataParameter);
}
let container: containers::Container = containers::Tag::new(tagged_slice.as_bytes())
.try_into()
let container = containers::Container::try_from(tagged_slice.as_bytes())
.map_err(|_| Status::IncorrectDataParameter)?;
info!("request to GetData for container {:?}", container);
@@ -218,19 +219,19 @@ impl TryFrom<ChangeReferenceArguments<'_>> for ChangeReference {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResetPinRetries {
pub padded_pin: [u8; 8],
pub struct ResetRetryCounter {
pub pin: [u8; 8],
pub puk: [u8; 8],
}
impl TryFrom<&[u8]> for ResetPinRetries {
impl TryFrom<&[u8]> for ResetRetryCounter {
type Error = Status;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
if data.len() != 16 {
return Err(Status::IncorrectDataParameter);
}
Ok(Self {
padded_pin: data[..8].try_into().unwrap(),
pin: data[..8].try_into().unwrap(),
puk: data[8..].try_into().unwrap(),
})
}
@@ -246,12 +247,48 @@ pub struct AuthenticateArguments<'l> {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PutData {}
pub enum PutData<'data> {
DiscoveryObject(&'data [u8]),
BitGroupTemplate(&'data [u8]),
Any(Container, &'data [u8]),
}
impl TryFrom<&[u8]> for PutData {
impl<'data> TryFrom<&'data [u8]> for PutData<'data> {
type Error = Status;
fn try_from(_data: &[u8]) -> Result<Self, Self::Error> {
todo!();
fn try_from(data: &'data [u8]) -> Result<Self, Self::Error> {
use crate::tlv::take_do;
let (tag, inner, rem) = take_do(data).ok_or_else(|| {
warn!("Failed to parse PUT DATA: {:02x?}", data);
Status::IncorrectDataParameter
})?;
if matches!(tag, 0x7E | 0x7F61) && !rem.is_empty() {
warn!("Empty remainder expected, got: {:02x?}", rem);
}
let container: Container = match tag {
0x7E => return Ok(PutData::DiscoveryObject(inner)),
0x7F61 => return Ok(PutData::BitGroupTemplate(inner)),
0x5C => Container::try_from(inner).map_err(|_| Status::IncorrectDataParameter)?,
_ => return Err(Status::IncorrectDataParameter),
};
let (tag, inner, rem) = take_do(rem).ok_or_else(|| {
warn!(
"Failed to parse PUT DATA's second field: {:02x?}, {:02x?}",
data, rem
);
Status::IncorrectDataParameter
})?;
if !rem.is_empty() {
warn!("Empty second remainder expected, got: {:02x?}", rem);
}
if tag != 0x53 {
warn!("Expected 0x53 tag, got: 0x{:02x?}", rem);
}
Ok(PutData::Any(container, inner))
}
}
@@ -310,7 +347,7 @@ impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
}
(0x00, Instruction::ResetRetryCounter, 0x00, 0x80) => {
Self::ResetPinRetries(ResetPinRetries::try_from(data.as_slice())?)
Self::ResetRetryCounter(ResetRetryCounter::try_from(data.as_slice())?)
}
(0x00, Instruction::GeneralAuthenticate, p1, p2) => {
@@ -327,7 +364,7 @@ impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
}
(0x00, Instruction::GenerateAsymmetricKeyPair, 0x00, p2) => {
Self::GenerateAsymmetric(GenerateAsymmetricKeyReference::try_from(p2)?)
Self::GenerateAsymmetric(GenerateKeyReference::try_from(p2)?)
}
// (0x00, 0x01, 0x10, 0x00)
(0x00, Instruction::Unknown(0x01), 0x00, 0x00) => {
@@ -359,9 +396,9 @@ impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
(0x00, Instruction::Unknown(0xf8), _, _) => {
Self::YkExtension(YubicoPivExtension::GetSerial)
}
(0x00, Instruction::Unknown(0xf7), _, _) => {
Self::YkExtension(YubicoPivExtension::GetMetadata)
}
(0x00, Instruction::Unknown(0xf7), 0x00, reference) => Self::YkExtension(
YubicoPivExtension::GetMetadata(KeyReference::try_from(reference)?),
),
_ => return Err(Status::FunctionNotSupported),
})
+36 -2
View File
@@ -5,6 +5,8 @@
use hex_literal::hex;
use crate::state::AdministrationAlgorithm;
pub const RID_LENGTH: usize = 5;
// top nibble of first byte is "category", here "A" = International
@@ -269,6 +271,38 @@ pub const YUBICO_DEFAULT_MANAGEMENT_KEY: &[u8; 24] = &hex!(
"
);
pub const YUBICO_DEFAULT_MANAGEMENT_KEY_ALG: AdministrationAlgorithm =
AdministrationAlgorithm::Tdes;
// stolen from le yubico
pub const DISCOVERY_OBJECT: &[u8; 20] =
b"~\x12O\x0b\xa0\x00\x00\x03\x08\x00\x00\x10\x00\x01\x00_/\x02@\x00";
pub const DISCOVERY_OBJECT: [u8; 18] = hex!(
"
4f 0b // PIV AID
a000000308000010000100
5f2f 02 // PIN usage Policy
4000"
);
use crate::Container;
pub const RETIRED_CERTS: [Container; 20] = [
Container::RetiredCert01,
Container::RetiredCert02,
Container::RetiredCert03,
Container::RetiredCert04,
Container::RetiredCert05,
Container::RetiredCert06,
Container::RetiredCert07,
Container::RetiredCert08,
Container::RetiredCert09,
Container::RetiredCert10,
Container::RetiredCert11,
Container::RetiredCert12,
Container::RetiredCert13,
Container::RetiredCert14,
Container::RetiredCert15,
Container::RetiredCert16,
Container::RetiredCert17,
Container::RetiredCert18,
Container::RetiredCert19,
Container::RetiredCert20,
];
+195 -104
View File
@@ -7,6 +7,7 @@ use hex_literal::hex;
macro_rules! enum_subset {
(
$(#[$outer:meta])*
$vis:vis enum $name:ident: $sup:ident {
$($var:ident),+
@@ -15,6 +16,7 @@ macro_rules! enum_subset {
) => {
$(#[$outer])*
#[repr(u8)]
#[derive(Clone, Copy)]
$vis enum $name {
$(
$var,
@@ -45,6 +47,19 @@ macro_rules! enum_subset {
}
}
impl<T: Copy + Into<$sup>> PartialEq<T> for $name {
fn eq(&self, other: &T) -> bool {
match (self,(*other).into()) {
$(
| ($name::$var, $sup::$var)
)* => true,
_ => false
}
}
}
impl Eq for $name {}
impl TryFrom<u8> for $name {
type Error = ::iso7816::Status;
fn try_from(tag: u8) -> ::core::result::Result<Self, Self::Error> {
@@ -60,18 +75,19 @@ macro_rules! enum_subset {
}
}
pub struct Tag<'a>(&'a [u8]);
impl<'a> Tag<'a> {
pub fn new(slice: &'a [u8]) -> Self {
Self(slice)
}
pub(crate) use enum_subset;
/// Security condition for the use of a given key.
pub enum SecurityCondition {
Pin,
Always,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetiredIndex(u8);
crate::enum_u8! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Debug)]
pub enum KeyReference {
GlobalPin = 0x00,
SecureMessaging = 0x04,
@@ -110,17 +126,73 @@ crate::enum_u8! {
}
}
impl KeyReference {
pub fn use_security_condition(self) -> SecurityCondition {
match self {
Self::SecureMessaging
| Self::PivCardApplicationAdministration
| Self::KeyManagement => SecurityCondition::Always,
_ => SecurityCondition::Pin,
}
}
}
macro_rules! impl_use_security_condition {
($($name:ident),*) => {
$(
impl $name {
pub fn use_security_condition(self) -> SecurityCondition {
let tmp: KeyReference = self.into();
tmp.use_security_condition()
}
}
)*
};
}
enum_subset! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Debug)]
pub enum AttestKeyReference: KeyReference {
PivAuthentication,
}
}
enum_subset! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GenerateAsymmetricKeyReference: KeyReference {
SecureMessaging,
#[derive(Debug)]
pub enum AsymmetricKeyReference: KeyReference {
// SecureMessaging,
PivAuthentication,
DigitalSignature,
KeyManagement,
CardAuthentication,
Retired01,
Retired02,
Retired03,
Retired04,
Retired05,
Retired06,
Retired07,
Retired08,
Retired09,
Retired10,
Retired11,
Retired12,
Retired13,
Retired14,
Retired15,
Retired16,
Retired17,
Retired18,
Retired19,
Retired20,
}
}
enum_subset! {
#[derive(Debug)]
pub enum GenerateKeyReference: AsymmetricKeyReference {
// SecureMessaging,
PivAuthentication,
DigitalSignature,
KeyManagement,
@@ -129,7 +201,7 @@ enum_subset! {
}
enum_subset! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Debug)]
pub enum ChangeReferenceKeyReference: KeyReference {
GlobalPin,
ApplicationPin,
@@ -138,7 +210,7 @@ enum_subset! {
}
enum_subset! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Debug)]
pub enum VerifyKeyReference: KeyReference {
GlobalPin,
ApplicationPin,
@@ -151,7 +223,7 @@ enum_subset! {
enum_subset! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Debug)]
pub enum AuthenticateKeyReference: KeyReference {
SecureMessaging,
PivAuthentication,
@@ -182,11 +254,38 @@ enum_subset! {
}
}
impl_use_security_condition!(
AttestKeyReference,
AsymmetricKeyReference,
ChangeReferenceKeyReference,
VerifyKeyReference,
AuthenticateKeyReference
);
macro_rules! impl_try_from {
($(($left:ident, $right:ident)),*) => {
$(
impl TryFrom<$left> for $right {
type Error = ::iso7816::Status;
fn try_from(val: $left) -> Result<Self,Self::Error> {
let tmp = KeyReference::from(val);
tmp.try_into()
}
}
)*
};
}
impl_try_from!((AuthenticateKeyReference, AsymmetricKeyReference));
/// The 36 data objects defined by PIV (SP 800-37-4, Part 1).
///
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Container {
// static
CardCapabilityContainer,
// generated at card creation
CardHolderUniqueIdentifier,
X509CertificateFor9A,
CardholderFingerprints,
@@ -198,41 +297,32 @@ pub enum Container {
PrintedInformation,
DiscoveryObject,
KeyHistoryObject,
RetiredX509Certificate(RetiredIndex),
RetiredCert01,
RetiredCert02,
RetiredCert03,
RetiredCert04,
RetiredCert05,
RetiredCert06,
RetiredCert07,
RetiredCert08,
RetiredCert09,
RetiredCert10,
RetiredCert11,
RetiredCert12,
RetiredCert13,
RetiredCert14,
RetiredCert15,
RetiredCert16,
RetiredCert17,
RetiredCert18,
RetiredCert19,
RetiredCert20,
CardholderIrisImages,
BiometricInformationTemplatesGroupTemplate,
SecureMessagingCertificateSigner,
PairingCodeReferenceDataContainer,
}
pub struct ContainerId(u16);
impl From<Container> for ContainerId {
fn from(container: Container) -> Self {
use Container::*;
Self(match container {
CardCapabilityContainer => 0xDB00,
CardHolderUniqueIdentifier => 0x3000,
X509CertificateFor9A => 0x0101,
CardholderFingerprints => 0x6010,
SecurityObject => 0x9000,
CardholderFacialImage => 0x6030,
X509CertificateFor9E => 0x0500,
X509CertificateFor9C => 0x0100,
X509CertificateFor9D => 0x0102,
PrintedInformation => 0x3001,
DiscoveryObject => 0x6050,
KeyHistoryObject => 0x6060,
RetiredX509Certificate(RetiredIndex(i)) => 0x1000u16 + i as u16,
CardholderIrisImages => 0x1015,
BiometricInformationTemplatesGroupTemplate => 0x1016,
SecureMessagingCertificateSigner => 0x1017,
PairingCodeReferenceDataContainer => 0x1018,
})
}
}
// these are just the "contact" rules, need to model "contactless" also
pub enum ReadAccessRule {
Always,
@@ -240,46 +330,46 @@ pub enum ReadAccessRule {
PinOrOcc,
}
// impl Container {
// const fn minimum_capacity(self) -> usize {
// use Container::*;
// match self {
// CardCapabilityContainer => 287,
// CardHolderUniqueIdentifier => 2916,
// CardholderFingerprints => 4006,
// SecurityObject => 1336,
// CardholderFacialImage => 12710,
// PrintedInformation => 245,
// DiscoveryObject => 19,
// KeyHistoryObject => 128,
// CardholderIrisImages => 7106,
// BiometricInformationTemplate => 65,
// SecureMessagingCertificateSigner => 2471,
// PairingCodeReferenceDataContainer => 12,
// // the others are X509 certificates
// _ => 1905,
// }
// }
impl Container {
// const fn minimum_capacity(self) -> usize {
// use Container::*;
// match self {
// CardCapabilityContainer => 287,
// CardHolderUniqueIdentifier => 2916,
// CardholderFingerprints => 4006,
// SecurityObject => 1336,
// CardholderFacialImage => 12710,
// PrintedInformation => 245,
// DiscoveryObject => 19,
// KeyHistoryObject => 128,
// CardholderIrisImages => 7106,
// BiometricInformationTemplate => 65,
// SecureMessagingCertificateSigner => 2471,
// PairingCodeReferenceDataContainer => 12,
// // the others are X509 certificates
// _ => 1905,
// }
// }
// const fn contact_access_rule(self) -> {
// use Container::*;
// use ReadAccessRule::*;
// match self {
// CardholderFingerprints => Pin,
// CardholderFacialImage => Pin,
// PrintedInformation => PinOrOcc,
// CardholderIrisImages => Pin,
// PairingCodeReferenceDataContainer => PinOrOcc,
// _ => Always,
// }
// }
// }
impl TryFrom<Tag<'_>> for Container {
type Error = ();
fn try_from(tag: Tag<'_>) -> Result<Self, ()> {
pub const fn contact_access_rule(self) -> ReadAccessRule {
use Container::*;
Ok(match tag.0 {
use ReadAccessRule::*;
match self {
CardholderFingerprints => Pin,
CardholderFacialImage => Pin,
PrintedInformation => PinOrOcc,
CardholderIrisImages => Pin,
PairingCodeReferenceDataContainer => PinOrOcc,
_ => Always,
}
}
}
impl TryFrom<&[u8]> for Container {
type Error = ();
fn try_from(tag: &[u8]) -> Result<Self, ()> {
use Container::*;
Ok(match tag {
hex!("5FC107") => CardCapabilityContainer,
hex!("5FC102") => CardHolderUniqueIdentifier,
hex!("5FC105") => X509CertificateFor9A,
@@ -287,36 +377,37 @@ impl TryFrom<Tag<'_>> for Container {
hex!("5FC106") => SecurityObject,
hex!("5FC108") => CardholderFacialImage,
hex!("5FC101") => X509CertificateFor9E,
hex!("5FC109") => PrintedInformation,
hex!("5FC10A") => X509CertificateFor9C,
hex!("5FC10B") => X509CertificateFor9D,
hex!("5FC109") => PrintedInformation,
hex!("7E") => DiscoveryObject,
hex!("5FC10D") => RetiredX509Certificate(RetiredIndex(1)),
hex!("5FC10E") => RetiredX509Certificate(RetiredIndex(2)),
hex!("5FC10F") => RetiredX509Certificate(RetiredIndex(3)),
hex!("5FC110") => RetiredX509Certificate(RetiredIndex(4)),
hex!("5FC111") => RetiredX509Certificate(RetiredIndex(5)),
hex!("5FC112") => RetiredX509Certificate(RetiredIndex(6)),
hex!("5FC113") => RetiredX509Certificate(RetiredIndex(7)),
hex!("5FC114") => RetiredX509Certificate(RetiredIndex(8)),
hex!("5FC115") => RetiredX509Certificate(RetiredIndex(9)),
hex!("5FC116") => RetiredX509Certificate(RetiredIndex(10)),
hex!("5FC117") => RetiredX509Certificate(RetiredIndex(11)),
hex!("5FC118") => RetiredX509Certificate(RetiredIndex(12)),
hex!("5FC119") => RetiredX509Certificate(RetiredIndex(13)),
hex!("5FC11A") => RetiredX509Certificate(RetiredIndex(14)),
hex!("5FC11B") => RetiredX509Certificate(RetiredIndex(15)),
hex!("5FC11C") => RetiredX509Certificate(RetiredIndex(16)),
hex!("5FC11D") => RetiredX509Certificate(RetiredIndex(17)),
hex!("5FC11E") => RetiredX509Certificate(RetiredIndex(18)),
hex!("5FC11F") => RetiredX509Certificate(RetiredIndex(19)),
hex!("5FC120") => RetiredX509Certificate(RetiredIndex(20)),
hex!("5FC10C") => KeyHistoryObject,
hex!("5FC10D") => RetiredCert01,
hex!("5FC10E") => RetiredCert02,
hex!("5FC10F") => RetiredCert03,
hex!("5FC110") => RetiredCert04,
hex!("5FC111") => RetiredCert05,
hex!("5FC112") => RetiredCert06,
hex!("5FC113") => RetiredCert07,
hex!("5FC114") => RetiredCert08,
hex!("5FC115") => RetiredCert09,
hex!("5FC116") => RetiredCert10,
hex!("5FC117") => RetiredCert11,
hex!("5FC118") => RetiredCert12,
hex!("5FC119") => RetiredCert13,
hex!("5FC11A") => RetiredCert14,
hex!("5FC11B") => RetiredCert15,
hex!("5FC11C") => RetiredCert16,
hex!("5FC11D") => RetiredCert17,
hex!("5FC11E") => RetiredCert18,
hex!("5FC11F") => RetiredCert19,
hex!("5FC120") => RetiredCert20,
hex!("5FC121") => CardholderIrisImages,
hex!("7F61") => BiometricInformationTemplatesGroupTemplate,
hex!("5FC122") => SecureMessagingCertificateSigner,
hex!("5FC123") => PairingCodeReferenceDataContainer,
hex!("7E") => DiscoveryObject,
hex!("7F61") => BiometricInformationTemplatesGroupTemplate,
_ => return Err(()),
})
}
+11 -5
View File
@@ -23,12 +23,18 @@ impl From<untrusted::EndOfInput> for Error {
}
/// Return the value of the given tag and apply a decoding function to it.
pub fn nested<'a, F, R>(input: &mut Reader<'a>, tag: u8, decoder: F) -> Result<R>
pub fn nested<'a, F, R, E>(
input: &mut Reader<'a>,
incomplete_end: E,
bad_tag: E,
tag: u8,
decoder: F,
) -> core::result::Result<R, E>
where
F: FnOnce(&mut untrusted::Reader<'a>) -> Result<R>,
F: FnOnce(&mut untrusted::Reader<'a>) -> core::result::Result<R, E>,
{
let inner = expect_tag_and_get_value(input, tag)?;
inner.read_all(Error::Read, decoder)
let inner = expect_tag_and_get_value(input, tag).map_err(|_| bad_tag)?;
inner.read_all(incomplete_end, decoder)
}
/// Read a tag and return it's value. Errors when the expect and actual tag do not match.
@@ -41,7 +47,7 @@ pub fn expect_tag_and_get_value<'a>(input: &mut Reader<'a>, tag: u8) -> Result<I
}
/// Read a tag and its value. Errors when the expected and actual tag and values do not match.
pub fn expect_tag_and_value<'a>(input: &mut Reader<'a>, tag: u8, value: &[u8]) -> Result<()> {
pub fn expect_tag_and_value(input: &mut Reader, tag: u8, value: &[u8]) -> Result<()> {
let (actual_tag, inner) = read_tag_and_get_value(input)?;
if usize::from(tag) != usize::from(actual_tag) {
return Err(Error::WrongTag);
+3 -3
View File
@@ -1,7 +1,7 @@
// Copyright (C) 2022 Nicolas Stalder AND Nitrokey GmbH
// SPDX-License-Identifier: LGPL-3.0-only
use crate::{Authenticator, /*constants::PIV_AID,*/ Result};
use crate::{reply::Reply, Authenticator, /*constants::PIV_AID,*/ Result};
use apdu_dispatch::{app::App, command, response, Command};
use trussed::client;
@@ -12,7 +12,7 @@ where
T: client::Client + client::Ed255 + client::Tdes,
{
fn select(&mut self, _apdu: &Command, reply: &mut response::Data) -> Result {
self.select(reply)
self.select(Reply(reply))
}
fn deselect(&mut self) {
@@ -25,6 +25,6 @@ where
apdu: &Command,
reply: &mut response::Data,
) -> Result {
self.respond(apdu, reply)
self.respond(apdu, &mut Reply(reply))
}
}
+609 -335
View File
File diff suppressed because it is too large Load Diff
+171 -81
View File
@@ -6,6 +6,7 @@ use core::convert::{TryFrom, TryInto};
use flexiber::Encodable;
use hex_literal::hex;
use serde::{Deserialize, Serialize};
use trussed::types::Mechanism;
#[macro_export]
macro_rules! enum_u8 {
@@ -18,6 +19,7 @@ macro_rules! enum_u8 {
) => {
$(#[$outer])*
#[repr(u8)]
#[derive(Clone, Copy)]
$vis enum $name {
$(
$var = $num,
@@ -35,6 +37,23 @@ macro_rules! enum_u8 {
}
}
}
impl PartialEq<u8> for $name {
fn eq(&self, other: &u8) -> bool {
*self as u8 == *other
}
}
impl<T: Into<$name> + Copy> PartialEq<T> for $name {
fn eq(&self, other: &T) -> bool {
let other: $name = (*other).into();
matches!((self,other), $(
| ($name::$var, $name::$var)
)*)
}
}
impl Eq for $name {}
}
}
@@ -42,35 +61,12 @@ macro_rules! enum_u8 {
///
/// We are more lenient, and allow ASCII 0x20..=0x7E.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Pin {
padded_pin: [u8; 8],
len: usize,
}
pub struct Pin(pub [u8; 8]);
impl TryFrom<&[u8]> for Pin {
type Error = ();
fn try_from(padded_pin: &[u8]) -> Result<Self, Self::Error> {
let padded_pin: [u8; 8] = padded_pin.try_into().map_err(|_| ())?;
let first_pad_byte = padded_pin[..8].iter().position(|&b| b == 0xff);
let unpadded_pin = match first_pad_byte {
Some(l) => &padded_pin[..l],
None => &padded_pin,
};
match unpadded_pin.len() {
len @ 6..=8 => {
let verifier = if cfg!(feature = "strict-pin") {
|&byte| (b'0'..=b'9').contains(&byte)
} else {
|&byte| (32..=127).contains(&byte)
};
if unpadded_pin.iter().all(verifier) {
Ok(Pin { padded_pin, len })
} else {
Err(())
}
}
_ => Err(()),
}
Ok(Self(padded_pin.try_into().map_err(|_| ())?))
}
}
@@ -86,7 +82,7 @@ impl TryFrom<&[u8]> for Puk {
}
enum_u8! {
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
#[derive(Debug,Deserialize,Serialize)]
// As additional reference, see:
// https://globalplatform.org/wp-content/uploads/2014/03/GPC_ISO_Framework_v1.0.pdf#page=15
//
@@ -123,6 +119,101 @@ enum_u8! {
P384Sha384 = 0xF4,
}
}
crate::container::enum_subset! {
#[derive(Debug,Deserialize,Serialize)]
pub enum AsymmetricAlgorithms: Algorithms {
Rsa2048,
Rsa4096,
P256,
// Not supported
// Rsa1024 = 0x6,
// Rsa3072 = 0xE0,
// P384 = 0x14,
// P521 = 0x15,
// non-standard! in piv-go though!
// Ed255_prev = 0x22,
// https://globalplatform.org/wp-content/uploads/2014/03/GPC_ISO_Framework_v1.0.pdf#page=15
// non-standard!
// Ed25519 = 0xE2,
// X25519 = 0xE3,
// Ed448 = 0xE4,
// X448 = 0xE5,
}
}
impl AsymmetricAlgorithms {
pub fn key_mechanism(self) -> Mechanism {
match self {
Self::Rsa2048 => Mechanism::Rsa2048Raw,
Self::Rsa4096 => Mechanism::Rsa4096Raw,
Self::P256 => Mechanism::P256,
}
}
pub fn ecdh_mechanism(self) -> Option<Mechanism> {
use AsymmetricAlgorithms::*;
match self {
P256 => Some(Mechanism::P256),
/* P384 | P521 | X25519 | X448 */
_ => None,
}
}
pub fn sign_mechanism(self) -> Mechanism {
match self {
Self::Rsa2048 => Mechanism::Rsa2048Raw,
Self::Rsa4096 => Mechanism::Rsa4096Raw,
Self::P256 => Mechanism::P256Prehashed,
}
}
pub fn is_rsa(self) -> bool {
use AsymmetricAlgorithms::*;
matches!(self, Rsa2048 | Rsa4096)
}
}
macro_rules! impl_use_try_into {
($sup:ident => {$(($from:ident, $into:ident)),*}) => {
$(
impl TryFrom<$from> for $into {
type Error = iso7816::Status;
fn try_from(v: $from) -> core::result::Result<$into, iso7816::Status> {
let sup: $sup = v.into();
sup.try_into()
}
}
)*
};
}
crate::container::enum_subset! {
#[derive(Debug,Deserialize,Serialize)]
pub enum RsaAlgorithms: Algorithms {
Rsa2048,
Rsa4096,
}
}
impl RsaAlgorithms {
pub fn mechanism(self) -> Mechanism {
match self {
Self::Rsa2048 => Mechanism::Rsa2048Raw,
Self::Rsa4096 => Mechanism::Rsa4096Raw,
}
}
}
impl_use_try_into!(
Algorithms => {
(AsymmetricAlgorithms, RsaAlgorithms)
}
);
/// TODO:
#[derive(Clone, Copy, Default, Eq, PartialEq)]
pub struct CryptographicAlgorithmTemplate<'a> {
@@ -236,65 +327,64 @@ impl<'a> ApplicationPropertyTemplate<'a> {
}
}
}
// /// TODO: This should be an enum of sorts, maybe.
// ///
// /// The data objects that appear in the dynamic authentication template (tag '7C') in the data field
// /// of the GENERAL AUTHENTICATE card command depend on the authentication protocol being executed.
// ///
// /// Note that the empty tags (i.e., tags with no data) return the same tag with content
// /// (they can be seen as “requests for requests”):
// /// - '80 00' Returns '80 TL \<encrypted random\>' (as per definition)
// /// - '81 00' Returns '81 TL \<random\>' (as per external authenticate example)
// #[derive(Clone, Copy, Default, Encodable, Eq, PartialEq)]
// #[tlv(application, constructed, number = "0x1C")] // = 0x7C
// pub struct DynamicAuthenticationTemplate<'l> {
// /// The Witness (tag '80') contains encrypted data (unrevealed fact).
// /// This data is decrypted by the card.
// #[tlv(simple = "0x80")]
// witness: Option<&'l [u8]>,
/// TODO: This should be an enum of sorts, maybe.
///
/// The data objects that appear in the dynamic authentication template (tag '7C') in the data field
/// of the GENERAL AUTHENTICATE card command depend on the authentication protocol being executed.
///
/// Note that the empty tags (i.e., tags with no data) return the same tag with content
/// (they can be seen as “requests for requests”):
/// - '80 00' Returns '80 TL <encrypted random>' (as per definition)
/// - '81 00' Returns '81 TL <random>' (as per external authenticate example)
#[derive(Clone, Copy, Default, Encodable, Eq, PartialEq)]
#[tlv(application, constructed, number = "0x1C")] // = 0x7C
pub struct DynamicAuthenticationTemplate<'l> {
/// The Witness (tag '80') contains encrypted data (unrevealed fact).
/// This data is decrypted by the card.
#[tlv(simple = "0x80")]
witness: Option<&'l [u8]>,
// /// The Challenge (tag '81') contains clear data (byte sequence),
// /// which is encrypted by the card.
// #[tlv(simple = "0x81")]
// challenge: Option<&'l [u8]>,
/// The Challenge (tag '81') contains clear data (byte sequence),
/// which is encrypted by the card.
#[tlv(simple = "0x81")]
challenge: Option<&'l [u8]>,
// /// The Response (tag '82') contains either the decrypted data from tag '80'
// /// or the encrypted data from tag '81'.
// #[tlv(simple = "0x82")]
// response: Option<&'l [u8]>,
/// The Response (tag '82') contains either the decrypted data from tag '80'
/// or the encrypted data from tag '81'.
#[tlv(simple = "0x82")]
response: Option<&'l [u8]>,
// /// Not documented in SP-800-73-4
// #[tlv(simple = "0x85")]
// exponentiation: Option<&'l [u8]>,
// }
/// Not documented in SP-800-73-4
#[tlv(simple = "0x85")]
exponentiation: Option<&'l [u8]>,
}
impl<'a> DynamicAuthenticationTemplate<'a> {
pub fn with_challenge(challenge: &'a [u8]) -> Self {
Self {
challenge: Some(challenge),
..Default::default()
}
}
pub fn with_exponentiation(exponentiation: &'a [u8]) -> Self {
Self {
exponentiation: Some(exponentiation),
..Default::default()
}
}
pub fn with_response(response: &'a [u8]) -> Self {
Self {
response: Some(response),
..Default::default()
}
}
pub fn with_witness(witness: &'a [u8]) -> Self {
Self {
witness: Some(witness),
..Default::default()
}
}
}
// impl<'a> DynamicAuthenticationTemplate<'a> {
// pub fn with_challenge(challenge: &'a [u8]) -> Self {
// Self {
// challenge: Some(challenge),
// ..Default::default()
// }
// }
// pub fn with_exponentiation(exponentiation: &'a [u8]) -> Self {
// Self {
// exponentiation: Some(exponentiation),
// ..Default::default()
// }
// }
// pub fn with_response(response: &'a [u8]) -> Self {
// Self {
// response: Some(response),
// ..Default::default()
// }
// }
// pub fn with_witness(witness: &'a [u8]) -> Self {
// Self {
// witness: Some(witness),
// ..Default::default()
// }
// }
// }
/// The Card Holder Unique Identifier (CHUID) data object is defined in accordance with the Technical
/// Implementation Guidance: Smart Card Enabled Physical Access Control Systems (TIG SCEPACS)
+134
View File
@@ -0,0 +1,134 @@
// Copyright (C) 2022 Nitrokey GmbH
// SPDX-License-Identifier: LGPL-3.0-only
use iso7816::Status;
use core::ops::{Deref, DerefMut};
#[derive(Debug)]
pub struct Reply<'v, const R: usize>(pub &'v mut heapless::Vec<u8, R>);
impl<'v, const R: usize> Deref for Reply<'v, R> {
type Target = &'v mut heapless::Vec<u8, R>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'v, const R: usize> DerefMut for Reply<'v, R> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'v, const R: usize> Reply<'v, R> {
/// Extend the reply and return an error otherwise
/// The MoreAvailable and GET RESPONSE mechanisms are handled by adpu_dispatch
///
/// Named expand and not extend to avoid conflicts with Deref
pub fn expand(&mut self, data: &[u8]) -> Result<(), Status> {
self.0.extend_from_slice(data).map_err(|_| {
error!("Buffer full");
Status::NotEnoughMemory
})
}
fn serialize_len(len: usize) -> Result<heapless::Vec<u8, 3>, Status> {
let mut buf = heapless::Vec::new();
if let Ok(len) = u8::try_from(len) {
if len <= 0x7f {
buf.extend_from_slice(&[len]).ok();
} else {
buf.extend_from_slice(&[0x81, len]).ok();
}
} else if let Ok(len) = u16::try_from(len) {
let arr = len.to_be_bytes();
buf.extend_from_slice(&[0x82, arr[0], arr[1]]).ok();
} else {
error!("Length too long to be encoded");
return Err(Status::UnspecifiedNonpersistentExecutionError);
}
Ok(buf)
}
/// Prepend the length to some data.
///
/// Input:
/// AAAAAAAAAABBBBBBB
/// ↑
/// offset
///
/// Output:
///
/// AAAAAAAAAA 7 BBBBBBB
/// (There are seven Bs, the length is encoded as specified in § 4.4.4)
pub fn prepend_len(&mut self, offset: usize) -> Result<(), Status> {
if self.len() < offset {
error!("`prepend_len` called with offset lower than buffer length");
return Err(Status::UnspecifiedNonpersistentExecutionError);
}
let len = self.len() - offset;
let encoded = Self::serialize_len(len)?;
self.extend_from_slice(&encoded).map_err(|_| {
error!("Buffer full");
Status::UnspecifiedNonpersistentExecutionError
})?;
self[offset..].rotate_right(encoded.len());
Ok(())
}
pub fn append_len(&mut self, len: usize) -> Result<(), Status> {
let encoded = Self::serialize_len(len)?;
self.extend_from_slice(&encoded).map_err(|_| {
error!("Buffer full");
Status::UnspecifiedNonpersistentExecutionError
})
}
pub fn lend(&mut self) -> Reply<'_, R> {
Reply(self.0)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn prep_length() {
let mut tmp = heapless::Vec::<u8, 1000>::new();
let mut buf = Reply(&mut tmp);
let offset = buf.len();
buf.extend_from_slice(&[0; 0]).unwrap();
buf.prepend_len(offset).unwrap();
assert_eq!(&buf[offset..], [0]);
let offset = buf.len();
buf.extend_from_slice(&[0; 20]).unwrap();
buf.prepend_len(offset).unwrap();
let mut expected = vec![20];
expected.extend_from_slice(&[0; 20]);
assert_eq!(&buf[offset..], expected,);
let offset = buf.len();
buf.extend_from_slice(&[1; 127]).unwrap();
buf.prepend_len(offset).unwrap();
let mut expected = vec![127];
expected.extend_from_slice(&[1; 127]);
assert_eq!(&buf[offset..], expected);
let offset = buf.len();
buf.extend_from_slice(&[2; 128]).unwrap();
buf.prepend_len(offset).unwrap();
let mut expected = vec![0x81, 128];
expected.extend_from_slice(&[2; 128]);
assert_eq!(&buf[offset..], expected);
let offset = buf.len();
buf.extend_from_slice(&[3; 256]).unwrap();
buf.prepend_len(offset).unwrap();
let mut expected = vec![0x82, 0x01, 0x00];
expected.extend_from_slice(&[3; 256]);
assert_eq!(&buf[offset..], expected);
}
}
+385 -203
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
// Copyright (C) 2022 Nitrokey GmbH
// SPDX-License-Identifier: LGPL-3.0-only
//! Utilities for dealing with TLV (Tag-Length-Value) encoded data
#[allow(unused)]
pub fn get_do<'input>(tag_path: &[u16], data: &'input [u8]) -> Option<&'input [u8]> {
let mut to_ret = data;
let mut remainder = data;
for tag in tag_path {
loop {
let (cur_tag, cur_value, cur_remainder) = take_do(remainder)?;
remainder = cur_remainder;
if *tag == cur_tag {
to_ret = cur_value;
remainder = cur_value;
break;
}
}
}
Some(to_ret)
}
/// Returns (tag, data, remainder)
pub fn take_do(data: &[u8]) -> Option<(u16, &[u8], &[u8])> {
let (tag, remainder) = take_tag(data)?;
let (len, remainder) = take_len(remainder)?;
if remainder.len() < len {
warn!("Tried to parse TLV with data length shorter that the length data");
None
} else {
let (value, remainder) = remainder.split_at(len);
Some((tag, value, remainder))
}
}
// See
// https://www.emvco.com/wp-content/uploads/2017/05/EMV_v4.3_Book_3_Application_Specification_20120607062110791.pdf
// Annex B1
fn take_tag(data: &[u8]) -> Option<(u16, &[u8])> {
let b1 = *data.first()?;
if (b1 & 0x1f) == 0x1f {
let b2 = *data.get(1)?;
if (b2 & 0b10000000) != 0 {
// OpenPGP doesn't have any DO with a tag longer than 2 bytes
warn!("Got a tag larger than 2 bytes: {data:x?}");
return None;
}
Some((u16::from_be_bytes([b1, b2]), &data[2..]))
} else {
Some((u16::from_be_bytes([0, b1]), &data[1..]))
}
}
pub fn take_len(data: &[u8]) -> Option<(usize, &[u8])> {
let l1 = *data.first()?;
if l1 <= 0x7F {
Some((l1 as usize, &data[1..]))
} else if l1 == 0x81 {
Some((*data.get(1)? as usize, &data[2..]))
} else {
if l1 != 0x82 {
warn!(
"Got an unexpected length tag: {l1:x}, data: {:x?}",
&data[..3]
);
return None;
}
let l2 = *data.get(1)?;
let l3 = *data.get(2)?;
let len = u16::from_be_bytes([l2, l3]) as usize;
Some((len, &data[3..]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
use test_log::test;
#[test]
fn dos() {
assert_eq!(
get_do(&[0x02], &hex!("02 02 1DB9 02 02 1DB9")),
Some(hex!("1DB9").as_slice())
);
assert_eq!(
get_do(&[0xA6, 0x7F49, 0x86], &hex!("A6 26 7F49 23 86 21 04 2525252525252525252525252525252525252525252525252525252525252525")),
Some(hex!("04 2525252525252525252525252525252525252525252525252525252525252525").as_slice())
);
// Multiple nested
assert_eq!(
get_do(&[0xA6, 0x7F49, 0x86], &hex!("A6 2A 02 02 DEAD 7F49 23 86 21 04 2525252525252525252525252525252525252525252525252525252525252525")),
Some(hex!("04 2525252525252525252525252525252525252525252525252525252525252525").as_slice())
);
}
}
+2 -1
View File
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: LGPL-3.0-only
use iso7816::{command::FromSliceError, Command, Status};
use trussed::virt::{Client, Ram};
use trussed::virt::Ram;
use trussed_rsa_alloc::virt::Client;
use std::convert::{TryFrom, TryInto};
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2022 Nitrokey GmbH
// SPDX-License-Identifier: LGPL-3.0-only
use piv_authenticator::{vpicc::VirtualCard, Authenticator};
use std::{sync::mpsc, thread::sleep, time::Duration};
use stoppable_thread::spawn;
use std::sync::Mutex;
static VSC_MUTEX: Mutex<()> = Mutex::new(());
pub fn with_vsc<F: FnOnce() -> R, R>(f: F) -> R {
let _lock = VSC_MUTEX.lock().unwrap();
let mut vpicc = vpicc::connect().expect("failed to connect to vpcd");
let (tx, rx) = mpsc::channel();
let handle = spawn(move |stopped| {
trussed_rsa_alloc::virt::with_ram_client("opcard", |client| {
let card = Authenticator::new(client);
let mut virtual_card = VirtualCard::new(card);
let mut result = Ok(());
while !stopped.get() && result.is_ok() {
result = vpicc.poll(&mut virtual_card);
if result.is_ok() {
tx.send(()).expect("failed to send message");
}
}
result
})
});
rx.recv().expect("failed to read message");
sleep(Duration::from_millis(200));
let result = f();
handle
.stop()
.join()
.expect("failed to join vpicc thread")
.expect("failed to run virtual smartcard");
result
}
+112
View File
@@ -15,4 +15,116 @@
Select
]
),
IoTest(
name: "Default management key",
cmd_resp: [
AuthenticateManagement(
key: (
algorithm: Tdes,
key: "0102030405060708 0102030405060708 0102030405060708"
)
)
]
),
IoTest(
name: "Aes management key",
cmd_resp: [
AuthenticateManagement(
key: (
algorithm: Tdes,
key: "0102030405060708 0102030405060708 0102030405060708"
)
),
SetManagementKey(
key: (
algorithm: Aes256,
key: "0102030405060708 0102030405060708 0102030405060708 0102030405060708"
)
),
AuthenticateManagement(
key: (
algorithm: Aes256,
key: "0102030405060708 0102030405060708 0102030405060708 0102030405060708"
)
)
]
),
IoTest(
name: "unauthenticated set management key",
cmd_resp: [
SetManagementKey(
key: (
algorithm: Aes256,
key: "0102030405060708 0102030405060708 0102030405060708 0102030405060708"
),
expected_status: SecurityStatusNotSatisfied,
),
AuthenticateManagement(
key: (
algorithm: Aes256,
key: "0102030405060708 0102030405060708 0102030405060708 0102030405060708"
),
expected_status_challenge: IncorrectP1OrP2Parameter,
expected_status_response: IncorrectP1OrP2Parameter,
)
]
),
IoTest(
name: "Generate key",
cmd_resp: [
AuthenticateManagement(
key: (
algorithm: Tdes,
key: "0102030405060708 0102030405060708 0102030405060708"
)
),
IoData(
input: "00 47 009A 05
AC 03
80 01 11",
output: Len(70),
)
]
),
IoTest(
name: "PUT DATA",
cmd_resp: [
GetData(
input: "5C 01 7E",
output: Data("7e 12 4f 0b a000000308000010000100 5f2f 02 4000")
),
GetData(
input: "5C 03 5FC102",
output: Len(61)
),
PutData(
input: "5C 03 5FC102 53 10 000102030405060708090A0B0C0D0E0F",
expected_status: SecurityStatusNotSatisfied
),
GetData(
input: "5C 03 5FC102",
output: Len(61)
),
AuthenticateManagement(
key: (
algorithm: Tdes,
key: "0102030405060708 0102030405060708 0102030405060708"
)
),
PutData(
input: "5C 03 5FC102 53 10 000102030405060708090A0B0C0D0E0F",
),
GetData(
input: "5C 03 5FC102",
output: Data("53 10 000102030405060708090A0B0C0D0E0F")
),
PutData(
input: "5C 01 7E 53 10 000102030405060708090A0B0C0D0E0F",
),
GetData(
input: "5C 01 7E",
output: Data("7e 10 000102030405060708090A0B0C0D0E0F")
),
]
)
]
+214 -19
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2022 Nitrokey GmbH
// Copyright (C) 2022 Nicolas Stalder AND Nitrokey GmbH
// SPDX-License-Identifier: LGPL-3.0-only
#![cfg(feature = "virtual")]
@@ -6,8 +6,10 @@ mod setup;
use std::borrow::Cow;
use aes::Aes256Enc;
use hex_literal::hex;
use serde::Deserialize;
use trussed::types::GenericArray;
// iso7816::Status doesn't support serde
#[derive(Deserialize, Debug, PartialEq, Clone, Copy)]
@@ -36,6 +38,51 @@ enum Status {
UnspecifiedCheckingError,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize)]
pub enum Algorithm {
Tdes = 0x3,
Rsa1024 = 0x6,
Rsa2048 = 0x7,
Aes128 = 0x8,
Aes192 = 0xA,
Aes256 = 0xC,
P256 = 0x11,
P384 = 0x14,
P521 = 0x15,
// non-standard!
Rsa3072 = 0xE0,
Rsa4096 = 0xE1,
Ed25519 = 0xE2,
X25519 = 0xE3,
Ed448 = 0xE4,
X448 = 0xE5,
// non-standard! picked by Alex, but maybe due for removal
P256Sha1 = 0xF0,
P256Sha256 = 0xF1,
P384Sha1 = 0xF2,
P384Sha256 = 0xF3,
P384Sha384 = 0xF4,
}
impl Algorithm {
pub fn challenge_len(self) -> usize {
match self {
Self::Tdes => 8,
Self::Aes256 => 16,
_ => panic!(),
}
}
pub fn key_len(self) -> usize {
match self {
Self::Tdes => 24,
Self::Aes256 => 32,
_ => panic!(),
}
}
}
fn serialize_len(len: usize) -> heapless::Vec<u8, 3> {
let mut buf = heapless::Vec::new();
if let Ok(len) = u8::try_from(len) {
@@ -52,7 +99,6 @@ fn serialize_len(len: usize) -> heapless::Vec<u8, 3> {
buf
}
#[allow(unused)]
fn tlv(tag: &[u8], data: &[u8]) -> Vec<u8> {
let mut buf = Vec::from(tag);
buf.extend_from_slice(&serialize_len(data.len()));
@@ -60,7 +106,6 @@ fn tlv(tag: &[u8], data: &[u8]) -> Vec<u8> {
buf
}
#[allow(unused)]
fn build_command(cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8], le: u16) -> Vec<u8> {
let mut res = vec![cla, ins, p1, p2];
let lc = data.len();
@@ -152,8 +197,14 @@ enum OutputMatcher {
Len(usize),
// The () at the end are here to workaround a compiler bug. See:
// https://github.com/rust-lang/rust/issues/89940#issuecomment-1282321806
And(Cow<'static, [OutputMatcher]>, #[serde(default)] ()),
Or(Cow<'static, [OutputMatcher]>, #[serde(default)] ()),
All(
#[serde(default)] Cow<'static, [OutputMatcher]>,
#[serde(default)] (),
),
Any(
#[serde(default)] Cow<'static, [OutputMatcher]>,
#[serde(default)] (),
),
/// HEX data
Data(Cow<'static, str>),
Bytes(Cow<'static, [u8]>),
@@ -168,7 +219,7 @@ impl Default for OutputMatcher {
fn parse_hex(data: &str) -> Vec<u8> {
let tmp: String = data.split_whitespace().collect();
hex::decode(&tmp).unwrap()
hex::decode(tmp).unwrap()
}
impl OutputMatcher {
@@ -180,16 +231,23 @@ impl OutputMatcher {
data == parse_hex(expected)
}
Self::Bytes(expected) => {
println!("Validating output with {expected:x?}");
println!("Validating output with {expected:02x?}");
data == &**expected
}
Self::Len(len) => data.len() == *len,
Self::And(matchers, _) => matchers.iter().filter(|m| !m.validate(data)).count() == 0,
Self::Or(matchers, _) => matchers.iter().filter(|m| m.validate(data)).count() != 0,
Self::All(matchers, _) => matchers.iter().filter(|m| !m.validate(data)).count() == 0,
Self::Any(matchers, _) => matchers.iter().filter(|m| m.validate(data)).count() != 0,
}
}
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct ManagementKey {
algorithm: Algorithm,
key: String,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
enum IoCmd {
@@ -200,6 +258,20 @@ enum IoCmd {
#[serde(default)]
expected_status: Status,
},
GetData {
input: String,
#[serde(default)]
output: OutputMatcher,
#[serde(default)]
expected_status: Status,
},
PutData {
input: String,
#[serde(default)]
output: OutputMatcher,
#[serde(default)]
expected_status: Status,
},
VerifyDefaultApplicationPin {
#[serde(default)]
expected_status: Status,
@@ -208,10 +280,23 @@ enum IoCmd {
#[serde(default)]
expected_status: Status,
},
SetManagementKey {
key: ManagementKey,
#[serde(default)]
expected_status: Status,
},
AuthenticateManagement {
key: ManagementKey,
#[serde(default)]
expected_status_challenge: Status,
#[serde(default)]
expected_status_response: Status,
},
Select,
}
const MATCH_EMPTY: OutputMatcher = OutputMatcher::Len(0);
const MATCH_ANY: OutputMatcher = OutputMatcher::All(Cow::Borrowed(&[]), ());
impl IoCmd {
fn run(&self, card: &mut setup::Piv) {
@@ -221,23 +306,66 @@ impl IoCmd {
output,
expected_status,
} => Self::run_iodata(input, output, *expected_status, card),
Self::GetData {
input,
output,
expected_status,
} => Self::run_get_data(input, output, *expected_status, card),
Self::PutData {
input,
output,
expected_status,
} => Self::run_put_data(input, output, *expected_status, card),
Self::VerifyDefaultApplicationPin { expected_status } => {
Self::run_verify_default_application_pin(*expected_status, card)
}
Self::VerifyDefaultGlobalPin { expected_status } => {
Self::run_verify_default_global_pin(*expected_status, card)
}
Self::AuthenticateManagement {
key,
expected_status_challenge,
expected_status_response,
} => Self::run_authenticate_management(
key.algorithm,
&key.key,
*expected_status_challenge,
*expected_status_response,
card,
),
Self::SetManagementKey {
key,
expected_status,
} => Self::run_set_administration_key(key.algorithm, &key.key, *expected_status, card),
Self::Select => Self::run_select(card),
}
}
fn run_set_administration_key(
alg: Algorithm,
key: &str,
expected_status: Status,
card: &mut setup::Piv,
) {
let mut key_data = parse_hex(key);
let mut data = vec![alg as u8, 0x9b, key_data.len() as u8];
data.append(&mut key_data);
Self::run_bytes(
&build_command(0x00, 0xff, 0xff, 0xff, &data, 0),
&MATCH_ANY,
expected_status,
card,
);
}
fn run_bytes(
input: &[u8],
output: &OutputMatcher,
expected_status: Status,
card: &mut setup::Piv,
) {
println!("Command: {:x?}", input);
) -> heapless::Vec<u8, 1024> {
println!("Command: {input:x?}");
let mut rep: heapless::Vec<u8, 1024> = heapless::Vec::new();
let cmd: iso7816::Command<{ setup::COMMAND_SIZE }> = iso7816::Command::try_from(input)
.unwrap_or_else(|err| {
@@ -252,11 +380,12 @@ impl IoCmd {
println!("Output: {:?}\nStatus: {status:?}", hex::encode(&rep));
if !output.validate(&rep) {
panic!("Bad output. Expected {:?}", output);
panic!("Bad output. Expected {output:02x?}");
}
if status != expected_status {
panic!("Bad status. Expected {:?}", expected_status);
panic!("Bad status. Expected {expected_status:?}");
}
rep
}
fn run_iodata(
@@ -265,7 +394,71 @@ impl IoCmd {
expected_status: Status,
card: &mut setup::Piv,
) {
Self::run_bytes(&parse_hex(input), output, expected_status, card)
Self::run_bytes(&parse_hex(input), output, expected_status, card);
}
fn run_get_data(
input: &str,
output: &OutputMatcher,
expected_status: Status,
card: &mut setup::Piv,
) {
Self::run_bytes(
&build_command(0x00, 0xCB, 0x3F, 0xFF, &parse_hex(input), 0),
output,
expected_status,
card,
);
}
fn run_put_data(
input: &str,
output: &OutputMatcher,
expected_status: Status,
card: &mut setup::Piv,
) {
Self::run_bytes(
&build_command(0x00, 0xDB, 0x3F, 0xFF, &parse_hex(input), 0),
output,
expected_status,
card,
);
}
fn run_authenticate_management(
alg: Algorithm,
key: &str,
expected_status_challenge: Status,
expected_status_response: Status,
card: &mut setup::Piv,
) {
use des::{
cipher::{BlockEncrypt, KeyInit},
TdesEde3,
};
let command = build_command(0x00, 0x87, alg as u8, 0x9B, &hex!("7C 02 81 00"), 0);
let mut res = Self::run_bytes(&command, &MATCH_ANY, expected_status_challenge, card);
let key = parse_hex(key);
if expected_status_challenge != Status::Success {
res = heapless::Vec::from_slice(&vec![0; alg.challenge_len() + 6]).unwrap();
}
// Remove header
let challenge = &mut res[4..][..alg.challenge_len()];
match alg {
Algorithm::Tdes => {
let cipher = TdesEde3::new(GenericArray::from_slice(&key));
cipher.encrypt_block(GenericArray::from_mut_slice(challenge));
}
Algorithm::Aes256 => {
let cipher = Aes256Enc::new(GenericArray::from_slice(&key));
cipher.encrypt_block(GenericArray::from_mut_slice(challenge));
}
_ => panic!(),
}
let second_data = tlv(&[0x7C], &tlv(&[0x82], challenge));
let command = build_command(0x00, 0x87, alg as u8, 0x9B, &second_data, 0);
Self::run_bytes(&command, &MATCH_ANY, expected_status_response, card);
}
fn run_verify_default_global_pin(expected_status: Status, card: &mut setup::Piv) {
@@ -274,21 +467,22 @@ impl IoCmd {
&MATCH_EMPTY,
expected_status,
card,
)
);
}
fn run_verify_default_application_pin(expected_status: Status, card: &mut setup::Piv) {
Self::run_bytes(
&hex!("00 20 00 80 08 313233343536FFFF"),
&MATCH_EMPTY,
expected_status,
card,
)
);
}
fn run_select(card: &mut setup::Piv) {
let matcher = OutputMatcher::Bytes(Cow::Borrowed(&hex!(
"
61 63 // Card application property template
61 66 // Card application property template
4f 06 000010000100 // Application identifier
50 0c 536f6c6f4b65797320504956 // Application label = b\"Solokeys PIV\"
@@ -296,12 +490,13 @@ impl IoCmd {
5f50 2d 68747470733a2f2f6769746875622e636f6d2f736f6c6f6b6579732f7069762d61757468656e74696361746f72
// Cryptographic Algorithm Identifier Template
ac 12
ac 15
80 01 03 // TDES - ECB
80 01 0c // AES256 - ECB
80 01 11 // P-256
80 01 e2 // Ed25519
80 01 e3 // X25519
80 01 07 // RSA 2048
06 01 00
// Coexistent Tag Allocation Authority Template
79 07
@@ -313,7 +508,7 @@ impl IoCmd {
&matcher,
Status::Success,
card,
)
);
}
}

Some files were not shown because too many files have changed in this diff Show More