diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..a7e771b --- /dev/null +++ b/.reuse/dep5 @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 9c5d2b8..ba9609b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/Makefile b/Makefile index ac78149..1050eb3 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/ci/Dockerfile b/ci/Dockerfile index d0b6828..7896c87 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -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 diff --git a/examples/usbip.rs b/examples/usbip.rs new file mode 100644 index 0000000..7fe20f9 --- /dev/null +++ b/examples/usbip.rs @@ -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>>>, +} + +impl trussed_usbip::Apps>>, ()> for PivApp { + fn new( + make_client: impl Fn(&str) -> ClientImplementation>>, + _data: (), + ) -> Self { + PivApp { + piv: piv::Authenticator::new(make_client("piv")), + } + } + + fn with_ccid_apps( + &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 = + Box::new(UserInterface::new()); + platform.user_interface().set_inner(ui); + }) + .exec::(|_platform| {}); +} diff --git a/examples/virtual.rs b/examples/virtual.rs index b5cd4f7..4f20b27 100644 --- a/examples/virtual.rs +++ b/examples/virtual.rs @@ -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"); diff --git a/src/commands.rs b/src/commands.rs index 7aa0bdd..8594fc1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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> 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 { 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 { - todo!(); + fn try_from(data: &'data [u8]) -> Result { + 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> 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> 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> 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), }) diff --git a/src/constants.rs b/src/constants.rs index 5eba22d..93f77a4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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, +]; diff --git a/src/container.rs b/src/container.rs index d727d2b..0a477a3 100644 --- a/src/container.rs +++ b/src/container.rs @@ -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> PartialEq for $name { + fn eq(&self, other: &T) -> bool { + match (self,(*other).into()) { + $( + | ($name::$var, $sup::$var) + )* => true, + _ => false + } + } + } + + impl Eq for $name {} + impl TryFrom for $name { type Error = ::iso7816::Status; fn try_from(tag: u8) -> ::core::result::Result { @@ -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 { + 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 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> for Container { - type Error = (); - fn try_from(tag: Tag<'_>) -> Result { + 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 { + use Container::*; + Ok(match tag { hex!("5FC107") => CardCapabilityContainer, hex!("5FC102") => CardHolderUniqueIdentifier, hex!("5FC105") => X509CertificateFor9A, @@ -287,36 +377,37 @@ impl TryFrom> 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(()), }) } diff --git a/src/derp.rs b/src/derp.rs index daa4cc5..0906780 100644 --- a/src/derp.rs +++ b/src/derp.rs @@ -23,12 +23,18 @@ impl From 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 +pub fn nested<'a, F, R, E>( + input: &mut Reader<'a>, + incomplete_end: E, + bad_tag: E, + tag: u8, + decoder: F, +) -> core::result::Result where - F: FnOnce(&mut untrusted::Reader<'a>) -> Result, + F: FnOnce(&mut untrusted::Reader<'a>) -> core::result::Result, { - 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(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); diff --git a/src/dispatch.rs b/src/dispatch.rs index fe03113..d1d8142 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -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)) } } diff --git a/src/lib.rs b/src/lib.rs index b4ed68c..96330e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,18 +11,22 @@ extern crate log; delog::generate_macros!(); pub mod commands; -use commands::GeneralAuthenticate; pub use commands::{Command, YubicoPivExtension}; +use commands::{GeneralAuthenticate, PutData, ResetRetryCounter}; pub mod constants; pub mod container; -use container::AttestKeyReference; +use container::{ + AttestKeyReference, AuthenticateKeyReference, Container, GenerateKeyReference, KeyReference, +}; pub mod derp; #[cfg(feature = "apdu-dispatch")] mod dispatch; pub mod piv_types; +mod reply; pub mod state; +mod tlv; -pub use piv_types::{Pin, Puk}; +pub use piv_types::{AsymmetricAlgorithms, Pin, Puk}; #[cfg(feature = "virtual")] pub mod vpicc; @@ -30,14 +34,16 @@ pub mod vpicc; use core::convert::TryInto; use flexiber::EncodableHeapless; +use heapless_bytes::Bytes; use iso7816::{Data, Status}; -use trussed::client; -use trussed::{syscall, try_syscall}; +use trussed::types::{KeySerialization, Location, StorageAttributes}; +use trussed::{client, syscall, try_syscall}; use constants::*; -pub type Result = iso7816::Result<()>; -use state::{LoadedState, State}; +pub type Result = iso7816::Result; +use reply::Reply; +use state::{AdministrationAlgorithm, CommandCache, KeyWithAlg, LoadedState, State, TouchPolicy}; /// PIV authenticator Trussed app. /// @@ -86,17 +92,19 @@ where // The way apdu-dispatch currently works, this would deselect, resetting security indicators. pub fn deselect(&mut self) {} - pub fn select(&mut self, reply: &mut Data) -> Result { + pub fn select(&mut self, mut reply: Reply<'_, R>) -> Result { use piv_types::Algorithms::*; info!("selecting PIV maybe"); let application_property_template = piv_types::ApplicationPropertyTemplate::default() .with_application_label(APPLICATION_LABEL) .with_application_url(APPLICATION_URL) - .with_supported_cryptographic_algorithms(&[Tdes, Aes256, P256, Ed25519, X25519]); + .with_supported_cryptographic_algorithms(&[ + Tdes, Aes256, P256, Ed25519, X25519, Rsa2048, + ]); application_property_template - .encode_to_heapless_vec(reply) + .encode_to_heapless_vec(*reply) .unwrap(); info!("returning: {:02X?}", reply); Ok(()) @@ -107,110 +115,39 @@ where command: &iso7816::Command, reply: &mut Data, ) -> Result { - info!("PIV responding to {:?}", command); + info!("PIV responding to {:02x?}", command); let parsed_command: Command = command.try_into()?; info!("parsed: {:?}", &parsed_command); + let reply = Reply(reply); match parsed_command { Command::Verify(verify) => self.load()?.verify(verify), Command::ChangeReference(change_reference) => { self.load()?.change_reference(change_reference) } - Command::GetData(container) => self.get_data(container, reply), + Command::GetData(container) => self.load()?.get_data(container, reply), + Command::PutData(put_data) => self.load()?.put_data(put_data), Command::Select(_aid) => self.select(reply), Command::GeneralAuthenticate(authenticate) => { self.load()? .general_authenticate(authenticate, command.data(), reply) } + Command::GenerateAsymmetric(reference) => { + self.load()? + .generate_asymmetric_keypair(reference, command.data(), reply) + } Command::YkExtension(yk_command) => { self.yubico_piv_extension(command.data(), yk_command, reply) } - _ => todo!(), + Command::ResetRetryCounter(reset) => self.load()?.reset_retry_counter(reset), } } - fn get_data( - &mut self, - container: container::Container, - reply: &mut Data, - ) -> Result { - // TODO: check security status, else return Status::SecurityStatusNotSatisfied - - // Table 3, Part 1, SP 800-73-4 - // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=30 - use crate::container::Container; - match container { - Container::DiscoveryObject => { - // Err(Status::InstructionNotSupportedOrInvalid) - reply.extend_from_slice(DISCOVERY_OBJECT).ok(); - // todo!("discovery object"), - } - - Container::BiometricInformationTemplatesGroupTemplate => { - return Err(Status::InstructionNotSupportedOrInvalid); - // todo!("biometric information template"), - } - - // '5FC1 07' (351B) - Container::CardCapabilityContainer => { - piv_types::CardCapabilityContainer::default() - .encode_to_heapless_vec(reply) - .unwrap(); - info!("returning CCC {:02X?}", reply); - } - - // '5FC1 02' (351B) - Container::CardHolderUniqueIdentifier => { - let guid = self.state.persistent(&mut self.trussed)?.guid(); - piv_types::CardHolderUniqueIdentifier::default() - .with_guid(guid) - .encode_to_heapless_vec(reply) - .unwrap(); - info!("returning CHUID {:02X?}", reply); - } - - // // '5FC1 05' (351B) - // Container::X509CertificateForPivAuthentication => { - // // return Err(Status::NotFound); - - // // 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 - // let data = try_syscall!(self.trussed.read_file( - // trussed::types::Location::Internal, - // trussed::types::PathBuf::from(b"authentication-key.x5c"), - // )).map_err(|_| { - // // info!("error loading: {:?}", &e); - // Status::NotFound - // } )?.data; - - // // todo: cleanup - // let tag = flexiber::Tag::application(0x13); // 0x53 - // flexiber::TaggedSlice::from(tag, &data) - // .unwrap() - // .encode_to_heapless_vec(reply) - // .unwrap(); - // } - - // // '5F FF01' (754B) - // YubicoObjects::AttestationCertificate => { - // let data = Data::from_slice(YUBICO_ATTESTATION_CERTIFICATE).unwrap(); - // reply.extend_from_slice(&data).ok(); - // } - _ => { - warn!("Unimplemented GET DATA object: {container:?}"); - return Err(Status::FunctionNotSupported); - } - } - Ok(()) - } - pub fn yubico_piv_extension( &mut self, data: &[u8], instruction: YubicoPivExtension, - reply: &mut Data, + mut reply: Reply<'_, R>, ) -> Result { info!("yubico extension: {:?}", &instruction); match instruction { @@ -238,10 +175,13 @@ where // TODO: find out what all needs resetting :) persistent_state.reset_pin(&mut self.trussed); persistent_state.reset_puk(&mut self.trussed); - persistent_state.reset_management_key(&mut self.trussed); - self.state.runtime.app_security_status.pin_verified = false; - self.state.runtime.app_security_status.puk_verified = false; - self.state.runtime.app_security_status.management_verified = false; + persistent_state.reset_administration_key(&mut self.trussed); + self.state.volatile.app_security_status.pin_verified = false; + self.state.volatile.app_security_status.puk_verified = false; + self.state + .volatile + .app_security_status + .administrator_verified = false; try_syscall!(self.trussed.remove_file( trussed::types::Location::Internal, @@ -256,32 +196,12 @@ where .ok(); } - YubicoPivExtension::SetManagementKey(_touch_policy) => { - // cmd := apdu{ - // instruction: insSetMGMKey, - // param1: 0xff, - // param2: 0xff, - // data: append([]byte{ - // alg3DES, keyCardManagement, 24, - // }, key[:]...), - // } - // TODO check we are authenticated with old management key - - // example: 03 9B 18 - // B0 20 7A 20 DC 39 0B 1B A5 56 CC EB 8D CE 7A 8A C8 23 E6 F5 0D 89 17 AA - if data.len() != 3 + 24 { - return Err(Status::IncorrectDataParameter); - } - let (prefix, new_management_key) = data.split_at(3); - if prefix != [0x03, 0x9b, 0x18] { - return Err(Status::IncorrectDataParameter); - } - let new_management_key: [u8; 24] = new_management_key.try_into().unwrap(); - self.state - .persistent(&mut self.trussed)? - .set_management_key(&new_management_key, &mut self.trussed); + YubicoPivExtension::SetManagementKey(touch_policy) => { + self.load()? + .yubico_set_administration_key(data, touch_policy, reply)?; } + YubicoPivExtension::GetMetadata(_reference) => { /* TODO */ } _ => return Err(Status::FunctionNotSupported), } Ok(()) @@ -289,6 +209,66 @@ where } impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> { + pub fn yubico_set_administration_key( + &mut self, + data: &[u8], + _touch_policy: TouchPolicy, + _reply: Reply<'_, R>, + ) -> Result { + // cmd := apdu{ + // instruction: insSetMGMKey, + // param1: 0xff, + // param2: 0xff, + // data: append([]byte{ + // alg3DES, keyCardManagement, 24, + // }, key[:]...), + // } + + // TODO _touch_policy + + if !self + .state + .volatile + .app_security_status + .administrator_verified + { + return Err(Status::SecurityStatusNotSatisfied); + } + + // example: 03 9B 18 + // B0 20 7A 20 DC 39 0B 1B A5 56 CC EB 8D CE 7A 8A C8 23 E6 F5 0D 89 17 AA + if data.len() < 4 { + warn!("Set management key with incorrect data"); + return Err(Status::IncorrectDataParameter); + } + + let key_data = &data[3..]; + + let Ok(alg) = AdministrationAlgorithm::try_from(data[0]) else { + warn!("Set management key with incorrect alg: {:x}", data[0]); + return Err(Status::IncorrectDataParameter); + }; + + if KeyReference::PivCardApplicationAdministration != data[1] { + warn!( + "Set management key with incorrect reference: {:x}, expected: {:x}", + data[1], + KeyReference::PivCardApplicationAdministration as u8 + ); + return Err(Status::IncorrectDataParameter); + } + + if data[2] as usize != key_data.len() || alg.key_len() != key_data.len() { + warn!("Set management key with incorrect data length: claimed: {}, required by algorithm: {}, real: {}", data[2], alg.key_len(), key_data.len()); + return Err(Status::IncorrectDataParameter); + } + + self.state + .persistent + .set_administration_key(key_data, alg, self.trussed); + Ok(()) + } + // maybe reserve this for the case VerifyLogin::PivPin? pub fn login(&mut self, login: commands::VerifyLogin) -> Result { if let commands::VerifyLogin::PivPin(pin) = login { @@ -297,11 +277,11 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> return Err(Status::OperationBlocked); } - if self.state.persistent.verify_pin(&pin) { + if self.state.persistent.verify_pin(&pin, self.trussed) { self.state .persistent .reset_consecutive_pin_mismatches(self.trussed); - self.state.runtime.app_security_status.pin_verified = true; + self.state.volatile.app_security_status.pin_verified = true; Ok(()) } else { let remaining = self @@ -309,7 +289,7 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> .persistent .increment_consecutive_pin_mismatches(self.trussed); // should we logout here? - self.state.runtime.app_security_status.pin_verified = false; + self.state.volatile.app_security_status.pin_verified = false; Err(Status::RemainingRetries(remaining)) } } else { @@ -323,7 +303,7 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> Verify::Login(login) => self.login(login), Verify::Logout(_) => { - self.state.runtime.app_security_status.pin_verified = false; + self.state.volatile.app_security_status.pin_verified = false; Ok(()) } @@ -331,7 +311,7 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> if key_reference != commands::VerifyKeyReference::ApplicationPin { return Err(Status::FunctionNotSupported); } - if self.state.runtime.app_security_status.pin_verified { + if self.state.volatile.app_security_status.pin_verified { Ok(()) } else { let retries = self.state.persistent.remaining_pin_retries(); @@ -354,12 +334,12 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> return Err(Status::OperationBlocked); } - if !self.state.persistent.verify_pin(&old_pin) { + if !self.state.persistent.verify_pin(&old_pin, self.trussed) { let remaining = self .state .persistent .increment_consecutive_pin_mismatches(self.trussed); - self.state.runtime.app_security_status.pin_verified = false; + self.state.volatile.app_security_status.pin_verified = false; return Err(Status::RemainingRetries(remaining)); } @@ -367,7 +347,7 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> .persistent .reset_consecutive_pin_mismatches(self.trussed); self.state.persistent.set_pin(new_pin, self.trussed); - self.state.runtime.app_security_status.pin_verified = true; + self.state.volatile.app_security_status.pin_verified = true; Ok(()) } @@ -376,12 +356,12 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> return Err(Status::OperationBlocked); } - if !self.state.persistent.verify_puk(&old_puk) { + if !self.state.persistent.verify_puk(&old_puk, self.trussed) { let remaining = self .state .persistent .increment_consecutive_puk_mismatches(self.trussed); - self.state.runtime.app_security_status.puk_verified = false; + self.state.volatile.app_security_status.puk_verified = false; return Err(Status::RemainingRetries(remaining)); } @@ -389,7 +369,7 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> .persistent .reset_consecutive_puk_mismatches(self.trussed); self.state.persistent.set_puk(new_puk, self.trussed); - self.state.runtime.app_security_status.puk_verified = true; + self.state.volatile.app_security_status.puk_verified = true; Ok(()) } @@ -425,7 +405,7 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> &mut self, auth: GeneralAuthenticate, data: &[u8], - reply: &mut Data, + mut reply: Reply<'_, R>, ) -> Result { // For "SSH", we need implement A.4.2 in SP-800-73-4 Part 2, ECDSA signatures // @@ -445,65 +425,358 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> // expected response: "7C L1 82 L2 SEQ(INT r, INT s)" // refine as we gain more capability - let mut input = derp::Reader::new(derp::Input::from(data)); - let Ok((tag,data)) = derp::read_tag_and_get_value(&mut input) else { + if !self + .state + .volatile + .security_valid(auth.key_reference.use_security_condition()) + { + warn!( + "Security condition not satisfied for key {:?}", + auth.key_reference + ); + return Err(Status::SecurityStatusNotSatisfied); + } + + /// struct + struct Auth<'i> { + witness: Option<&'i [u8]>, + challenge: Option<&'i [u8]>, + response: Option<&'i [u8]>, + exponentiation: Option<&'i [u8]>, + } + + let input = tlv::get_do(&[0x007C], data).ok_or_else(|| { + warn!("No 0x7C do in GENERAL AUTHENTICATE"); + Status::IncorrectDataParameter + })?; + + let parsed = Auth { + witness: tlv::get_do(&[0x80], input), + challenge: tlv::get_do(&[0x81], input), + response: tlv::get_do(&[0x82], input), + exponentiation: tlv::get_do(&[0x85], input), + }; + match parsed { + Auth { + witness: None, + challenge: Some(c), + response: Some([]), + exponentiation: None, + } => self.sign(auth, c, reply.lend())?, + Auth { + witness: None, + challenge: None, + response: Some([]), + exponentiation: Some(p), + } => self.key_agreement(auth, p, reply.lend())?, + Auth { + witness: None, + challenge: Some([]), + response: None, + exponentiation: None, + } => self.single_auth_1(auth, reply.lend())?, + Auth { + witness: None, + challenge: None, + response: Some(c), + exponentiation: None, + } => self.single_auth_2(auth, c)?, + Auth { + witness: Some([]), + challenge: None, + response: None, + exponentiation: None, + } => self.mutual_auth_1(auth, reply.lend())?, + Auth { + witness: Some(r), + challenge: Some(c), + response: None, + exponentiation: None, + } => self.mutual_auth_2(auth, r, c, reply.lend())?, + _ => todo!(), + } + Ok(()) + } + + /// Validate the auth parameters for managememt authentication operations + fn validate_auth_management( + &self, + auth: GeneralAuthenticate, + ) -> Result> { + if auth.key_reference != AuthenticateKeyReference::PivCardApplicationAdministration { + warn!("Attempt to authenticate with an invalid key"); + return Err(Status::IncorrectP1OrP2Parameter); + } + if auth.algorithm != self.state.persistent.keys.administration.alg { + warn!("Attempt to authenticate with an invalid algo"); + return Err(Status::IncorrectP1OrP2Parameter); + } + Ok(self.state.persistent.keys.administration) + } + + fn single_auth_1( + &mut self, + auth: GeneralAuthenticate, + mut reply: Reply<'_, R>, + ) -> Result { + info!("Single auth 1"); + let key = self.validate_auth_management(auth)?; + let pl = syscall!(self.trussed.random_bytes(key.alg.challenge_length())).bytes; + self.state.volatile.command_cache = Some(CommandCache::SingleAuthChallenge( + Bytes::from_slice(&pl).unwrap(), + )); + let data = syscall!(self + .trussed + .encrypt(key.alg.mechanism(), key.id, &pl, &[], None)) + .ciphertext; + + reply.expand(&[0x7C])?; + let offset = reply.len(); + { + reply.expand(&[0x81])?; + reply.append_len(data.len())?; + reply.expand(&data)?; + } + reply.prepend_len(offset)?; + Ok(()) + } + + fn single_auth_2(&mut self, auth: GeneralAuthenticate, response: &[u8]) -> Result { + info!("Single auth 2"); + use subtle::ConstantTimeEq; + + let key = self.validate_auth_management(auth)?; + if response.len() != key.alg.challenge_length() { + warn!("Incorrect challenge length"); return Err(Status::IncorrectDataParameter); + } + + let Some(plaintext_challenge) = self.state.volatile.take_single_challenge() else { + warn!("Missing cached challenge for auth"); + return Err(Status::ConditionsOfUseNotSatisfied); }; - // part 2 table 7 - match tag { - 0x80 => self.request_for_witness(auth, data, reply), - 0x81 => self.request_for_challenge(auth, data, reply), - 0x82 => self.request_for_response(auth, data, reply), - 0x85 => self.request_for_exponentiation(auth, data, reply), - _ => Err(Status::IncorrectDataParameter), + if response.ct_eq(&plaintext_challenge).into() { + warn!("Bad auth challenge"); + return Err(Status::IncorrectDataParameter); } - } - pub fn request_for_response( + self.state + .volatile + .app_security_status + .administrator_verified = true; + Ok(()) + } + fn mutual_auth_1( &mut self, - _auth: GeneralAuthenticate, - _data: derp::Input<'_>, - _reply: &mut Data, + auth: GeneralAuthenticate, + mut reply: Reply<'_, R>, ) -> Result { - todo!() + info!("Mutual auth 1"); + let key = self.validate_auth_management(auth)?; + let pl = syscall!(self.trussed.random_bytes(key.alg.challenge_length())).bytes; + self.state.volatile.command_cache = Some(CommandCache::MutualAuthChallenge( + Bytes::from_slice(&pl).unwrap(), + )); + let data = syscall!(self + .trussed + .encrypt(key.alg.mechanism(), key.id, &pl, &[], None)) + .ciphertext; + reply.expand(&[0x7C])?; + let offset = reply.len(); + { + reply.expand(&[0x80])?; + reply.append_len(data.len())?; + reply.expand(&data)?; + } + reply.prepend_len(offset)?; + Ok(()) } - - pub fn request_for_exponentiation( + fn mutual_auth_2( &mut self, - _auth: GeneralAuthenticate, - _data: derp::Input<'_>, - _reply: &mut Data, + auth: GeneralAuthenticate, + response: &[u8], + challenge: &[u8], + mut reply: Reply<'_, R>, ) -> Result { - todo!() + use subtle::ConstantTimeEq; + + info!("Mutual auth 2"); + let key = self.validate_auth_management(auth)?; + if challenge.len() != key.alg.challenge_length() { + warn!("Incorrect challenge length"); + return Err(Status::IncorrectDataParameter); + } + if response.len() != key.alg.challenge_length() { + warn!("Incorrect response length"); + return Err(Status::IncorrectDataParameter); + } + + let Some(plaintext_challenge) = self.state.volatile.take_mutual_challenge() else { + warn!("Missing cached challenge for auth"); + return Err(Status::ConditionsOfUseNotSatisfied); + }; + + if challenge.ct_eq(&plaintext_challenge).into() { + warn!("Bad auth challenge"); + return Err(Status::IncorrectDataParameter); + } + + let challenge_response = + syscall!(self + .trussed + .encrypt(key.alg.mechanism(), key.id, challenge, &[], None)) + .ciphertext; + + reply.expand(&[0x7C])?; + let offset = reply.len(); + { + reply.expand(&[0x82])?; + reply.append_len(challenge_response.len())?; + reply.expand(&challenge_response)?; + } + reply.prepend_len(offset)?; + + self.state + .volatile + .app_security_status + .administrator_verified = true; + Ok(()) } - pub fn request_for_challenge( + // Sign a message. For RSA, since the key is exposed as a raw key, so it can also be used for decryption + fn sign( &mut self, - _auth: GeneralAuthenticate, - _data: derp::Input<'_>, - _reply: &mut Data, + auth: GeneralAuthenticate, + message: &[u8], + mut reply: Reply<'_, R>, ) -> Result { - todo!() + info!("Request for sign"); + + let Ok(key_ref) = auth.key_reference.try_into() else { + warn!("Attempt to sign with an incorrect key"); + return Err(Status::IncorrectP1OrP2Parameter); + }; + let Some(KeyWithAlg { alg, id }) = self.state.persistent.keys.asymetric_for_reference(key_ref) else { + warn!("Attempt to use unset key"); + return Err(Status::ConditionsOfUseNotSatisfied); + }; + + if alg != auth.algorithm { + warn!("Bad algorithm: {:?}", auth.algorithm); + return Err(Status::IncorrectP1OrP2Parameter); + } + if !self.state.volatile.app_security_status.pin_verified { + warn!("Authenticate challenge without pin validated"); + return Err(Status::SecurityStatusNotSatisfied); + } + + let response = syscall!(self.trussed.sign( + alg.sign_mechanism(), + id, + message, + trussed::types::SignatureSerialization::Raw, + )) + .signature; + reply.expand(&[0x7C])?; + let offset = reply.len(); + { + reply.expand(&[0x82])?; + reply.append_len(response.len())?; + reply.expand(&response)?; + } + reply.prepend_len(offset)?; + Ok(()) } - pub fn request_for_witness( - &mut self, - _auth: GeneralAuthenticate, - _data: derp::Input<'_>, - _reply: &mut Data, - ) -> Result { - todo!() - } - - #[allow(unused)] - pub fn generate_asymmetric_keypair( + fn key_agreement( &mut self, + auth: GeneralAuthenticate, data: &[u8], - reply: &mut Data, + mut reply: Reply<'_, R>, ) -> Result { - if !self.state.runtime.app_security_status.management_verified { + info!("Request for exponentiation"); + let key_reference = auth.key_reference.try_into().map_err(|_| { + warn!( + "Attempt to use non asymetric key for exponentiation: {:?}", + auth.key_reference + ); + Status::IncorrectP1OrP2Parameter + })?; + let Some(KeyWithAlg { alg, id }) = self.state.persistent.keys.asymetric_for_reference(key_reference) else { + warn!("Attempt to use unset key"); + return Err(Status::ConditionsOfUseNotSatisfied); + }; + + if alg != auth.algorithm { + warn!("Attempt to exponentiate with incorrect algorithm"); + return Err(Status::IncorrectP1OrP2Parameter); + } + + let Some(mechanism) = alg.ecdh_mechanism() else { + warn!("Attempt to exponentiate with non ECDH algorithm"); + return Err(Status::ConditionsOfUseNotSatisfied); + }; + + if data.first() != Some(&0x04) { + warn!("Bad data format for ECDH"); + return Err(Status::IncorrectDataParameter); + } + + let public_key = try_syscall!(self.trussed.deserialize_key( + mechanism, + &data[1..], + KeySerialization::Raw, + StorageAttributes::default().set_persistence(Location::Volatile) + )) + .map_err(|_err| { + warn!("Failed to load public key: {:?}", _err); + Status::IncorrectDataParameter + })? + .key; + let shared_secret = syscall!(self.trussed.agree( + mechanism, + id, + public_key, + StorageAttributes::default() + .set_persistence(Location::Volatile) + .set_serializable(true) + )) + .shared_secret; + + let serialized_secret = syscall!(self.trussed.serialize_key( + trussed::types::Mechanism::SharedSecret, + shared_secret, + KeySerialization::Raw + )) + .serialized_key; + syscall!(self.trussed.delete(public_key)); + syscall!(self.trussed.delete(shared_secret)); + + reply.expand(&[0x7C])?; + let offset = reply.len(); + { + reply.expand(&[0x82])?; + reply.append_len(serialized_secret.len())?; + reply.expand(&serialized_secret)?; + } + reply.prepend_len(offset)?; + Ok(()) + } + + pub fn generate_asymmetric_keypair( + &mut self, + reference: GenerateKeyReference, + data: &[u8], + mut reply: Reply<'_, R>, + ) -> Result { + if !self + .state + .volatile + .app_security_status + .administrator_verified + { return Err(Status::SecurityStatusNotSatisfied); } @@ -531,187 +804,188 @@ impl<'a, T: trussed::Client + trussed::client::Ed255> LoadedAuthenticator<'a, T> // TODO: iterate on this, don't expect tags.. let input = derp::Input::from(data); // let (mechanism, parameter) = input.read_all(derp::Error::Read, |input| { - let (mechanism, _pin_policy, _touch_policy) = input - .read_all(derp::Error::Read, |input| { - derp::nested(input, 0xac, |input| { - let mechanism = derp::expect_tag_and_get_value(input, 0x80)?; - // let parameter = derp::expect_tag_and_get_value(input, 0x81)?; - let pin_policy = derp::expect_tag_and_get_value(input, 0xaa)?; - let touch_policy = derp::expect_tag_and_get_value(input, 0xab)?; - // Ok((mechanism.as_slice_less_safe(), parameter.as_slice_less_safe())) - Ok(( - mechanism.as_slice_less_safe(), - pin_policy.as_slice_less_safe(), - touch_policy.as_slice_less_safe(), - )) - }) - }) - .map_err(|_e| { - info!("error parsing GenerateAsymmetricKeypair: {:?}", &_e); - Status::IncorrectDataParameter - })?; + let mechanism_data = input.read_all(Status::IncorrectDataParameter, |input| { + derp::nested( + input, + Status::IncorrectDataParameter, + Status::IncorrectDataParameter, + 0xac, + |input| { + derp::expect_tag_and_get_value(input, 0x80) + .map(|input| input.as_slice_less_safe()) + .map_err(|_e| { + warn!("error parsing GenerateAsymmetricKeypair: {:?}", &_e); + Status::IncorrectDataParameter + }) + }, + ) + })?; - // if mechanism != &[0x11] { - // HA! patch in Ed255 - if mechanism != [0x22] { - return Err(Status::InstructionNotSupportedOrInvalid); - } + let [mechanism] = mechanism_data else { + warn!("Mechanism of len not 1: {mechanism_data:02x?}"); + return Err(Status::IncorrectDataParameter); + }; - // ble policy + let parsed_mechanism: AsymmetricAlgorithms = (*mechanism).try_into().map_err(|_| { + warn!("Unknown mechanism: {mechanism:x}"); + Status::IncorrectDataParameter + })?; - if let Some(key) = self.state.persistent.keys.authentication_key { - syscall!(self.trussed.delete(key)); - } + let secret_key = self.state.persistent.generate_asymmetric_key( + reference.into(), + parsed_mechanism, + self.trussed, + ); - // let key = syscall!(self.trussed.generate_p256_private_key( - // let key = syscall!(self.trussed.generate_p256_private_key( - let key = syscall!(self - .trussed - .generate_ed255_private_key(trussed::types::Location::Internal,)) - .key; - - // // TEMP - // let mechanism = trussed::types::Mechanism::P256Prehashed; - // let mechanism = trussed::types::Mechanism::P256; - // let commitment = &[37u8; 32]; - // // blocking::dbg!(commitment); - // let serialization = trussed::types::SignatureSerialization::Asn1Der; - // // blocking::dbg!(&key); - // let signature = block!(self.trussed.sign(mechanism, key.clone(), commitment, serialization).map_err(|e| { - // blocking::dbg!(e); - // e - // }).unwrap()) - // .map_err(|error| { - // // NoSuchKey - // blocking::dbg!(error); - // Status::UnspecifiedNonpersistentExecutionError } - // )? - // .signature; - // blocking::dbg!(&signature); - self.state.persistent.keys.authentication_key = Some(key); - self.state.persistent.save(self.trussed); - - // let public_key = syscall!(self.trussed.derive_p256_public_key( - let public_key = syscall!(self - .trussed - .derive_ed255_public_key(key, trussed::types::Location::Volatile,)) - .key; - - let serialized_public_key = syscall!(self.trussed.serialize_key( - // trussed::types::Mechanism::P256, - trussed::types::Mechanism::Ed255, - public_key, - trussed::types::KeySerialization::Raw, + let public_key = syscall!(self.trussed.derive_key( + parsed_mechanism.key_mechanism(), + secret_key, + None, + StorageAttributes::default().set_persistence(Location::Volatile) )) - .serialized_key; + .key; - // info!("supposed SEC1 pubkey, len {}: {:X?}", serialized_public_key.len(), &serialized_public_key); + match parsed_mechanism { + AsymmetricAlgorithms::P256 => { + let serialized_key = syscall!(self.trussed.serialize_key( + parsed_mechanism.key_mechanism(), + public_key, + trussed::types::KeySerialization::Raw + )) + .serialized_key; + reply.expand(&[0x7F, 0x49])?; + let offset = reply.len(); + reply.expand(&[0x86])?; + reply.append_len(serialized_key.len() + 1)?; + reply.expand(&[0x04])?; + reply.expand(&serialized_key)?; + reply.prepend_len(offset)?; + } + AsymmetricAlgorithms::Rsa2048 | AsymmetricAlgorithms::Rsa4096 => { + use trussed_rsa_alloc::RsaPublicParts; + reply.expand(&[0x7F, 0x49])?; + let offset = reply.len(); + let tmp = syscall!(self.trussed.serialize_key( + parsed_mechanism.key_mechanism(), + public_key, + trussed::types::KeySerialization::RsaParts + )) + .serialized_key; + let serialized: RsaPublicParts = + trussed::postcard_deserialize(&tmp).map_err(|_err| { + error!("Failed to parse RSA parts: {:?}", _err); + Status::UnspecifiedNonpersistentExecutionError + })?; + reply.expand(&[0x81])?; + reply.append_len(serialized.n.len())?; + reply.expand(serialized.n)?; - // P256 SEC1 has 65 bytes, Ed255 pubkeys have 32 - // let l2 = 65; - let l2 = 32; - let l1 = l2 + 2; + reply.expand(&[0x82])?; + reply.append_len(serialized.e.len())?; + reply.expand(serialized.e)?; - reply - .extend_from_slice(&[0x7f, 0x49, l1, 0x86, l2]) - .unwrap(); - reply.extend_from_slice(&serialized_public_key).unwrap(); + reply.prepend_len(offset)?; + } + }; + syscall!(self.trussed.delete(public_key)); Ok(()) } - #[allow(unused)] - pub fn put_data(&mut self, data: &[u8]) -> Result { - info!("PutData"); - - // if !self.state.runtime.app_security_status.management_verified { - // return Err(Status::SecurityStatusNotSatisfied); - // } - - // # PutData - // 00 DB 3F FF 23 - // # data object: 5FC109 - // 5C 03 5F C1 09 - // # data: - // 53 1C - // # actual data - // 88 1A 89 18 AA 81 D5 48 A5 EC 26 01 60 BA 06 F6 EC 3B B6 05 00 2E B6 3D 4B 28 7F 86 - // - - let input = derp::Input::from(data); - let (data_object, data) = input - .read_all(derp::Error::Read, |input| { - let data_object = derp::expect_tag_and_get_value(input, 0x5c)?; - let data = derp::expect_tag_and_get_value(input, 0x53)?; - Ok((data_object.as_slice_less_safe(), data.as_slice_less_safe())) - // }).unwrap(); - }) - .map_err(|_e| { - info!("error parsing PutData: {:?}", &_e); - Status::IncorrectDataParameter - })?; - - // info!("PutData in {:?}: {:?}", data_object, data); - - if data_object == [0x5f, 0xc1, 0x09] { - // "Printed Information", supposedly - // Yubico uses this to store its "Metadata" - // - // 88 1A - // 89 18 - // # we see here the raw management key? amazing XD - // AA 81 D5 48 A5 EC 26 01 60 BA 06 F6 EC 3B B6 05 00 2E B6 3D 4B 28 7F 86 - - // TODO: use smarter quota rule, actual data sent is 28B - if data.len() >= 512 { - return Err(Status::UnspecifiedCheckingError); - } - - try_syscall!(self.trussed.write_file( - trussed::types::Location::Internal, - trussed::types::PathBuf::from(b"printed-information"), - trussed::types::Message::from_slice(data).unwrap(), - None, - )) - .map_err(|_| Status::NotEnoughMemory)?; - - return Ok(()); + fn get_data( + &mut self, + container: Container, + mut reply: Reply<'_, R>, + ) -> Result { + if !self + .state + .volatile + .read_valid(container.contact_access_rule()) + { + warn!("Unauthorized attempt to access: {:?}", container); + return Err(Status::SecurityStatusNotSatisfied); } - if data_object == [0x5f, 0xc1, 0x05] { - // "X.509 Certificate for PIV Authentication", supposedly - // IOW, the cert for "authentication key" - // Yubico uses this to store its "Metadata" - // - // 88 1A - // 89 18 - // # we see here the raw management key? amazing XD - // AA 81 D5 48 A5 EC 26 01 60 BA 06 F6 EC 3B B6 05 00 2E B6 3D 4B 28 7F 86 - - // TODO: use smarter quota rule, actual data sent is 28B - if data.len() >= 512 { - return Err(Status::UnspecifiedCheckingError); - } - - try_syscall!(self.trussed.write_file( - trussed::types::Location::Internal, - trussed::types::PathBuf::from(b"authentication-key.x5c"), - trussed::types::Message::from_slice(data).unwrap(), - None, - )) - .map_err(|_| Status::NotEnoughMemory)?; - - return Ok(()); + use state::ContainerStorage; + let tag = match container { + Container::DiscoveryObject => [0x7E].as_slice(), + Container::BiometricInformationTemplatesGroupTemplate => &[0x7F, 0x61], + _ => &[0x53], + }; + reply.expand(tag)?; + let offset = reply.len(); + match container { + Container::KeyHistoryObject => self.get_key_history_object(reply.lend())?, + _ => match ContainerStorage(container).load(self.trussed)? { + Some(data) => reply.expand(&data)?, + None => return Err(Status::NotFound), + }, } + reply.prepend_len(offset)?; - Err(Status::IncorrectDataParameter) + Ok(()) } - // match container { - // containers::Container::CardHolderUniqueIdentifier => - // piv_types::CardHolderUniqueIdentifier::default() - // .encode - // _ => todo!(), - // } - // todo!(); + fn put_data(&mut self, put_data: PutData<'_>) -> Result { + if !self + .state + .volatile + .app_security_status + .administrator_verified + { + warn!("Unauthorized attempt at PUT DATA: {:?}", put_data); + return Err(Status::SecurityStatusNotSatisfied); + } + + let (container, data) = match put_data { + PutData::Any(container, data) => (container, data), + PutData::BitGroupTemplate(data) => { + (Container::BiometricInformationTemplatesGroupTemplate, data) + } + PutData::DiscoveryObject(data) => (Container::DiscoveryObject, data), + }; + + use state::ContainerStorage; + ContainerStorage(container).save(self.trussed, data) + } + + fn reset_retry_counter(&mut self, data: ResetRetryCounter) -> Result { + if !self + .state + .persistent + .verify_puk(&Puk(data.puk), self.trussed) + { + return Err(Status::VerificationFailed); + } + self.state.persistent.set_pin(Pin(data.pin), self.trussed); + + Ok(()) + } + + fn get_key_history_object(&mut self, mut reply: Reply<'_, R>) -> Result { + let num_keys = self + .state + .persistent + .keys + .retired_keys + .iter() + .filter(|k| k.is_some()) + .count() as u8; + let mut num_certs = 0u8; + + use state::ContainerStorage; + + for c in RETIRED_CERTS { + if ContainerStorage(c).exists(self.trussed)? { + num_certs += 1; + } + } + + reply.expand(&[0xC1, 0x01])?; + reply.expand(&[num_certs])?; + reply.expand(&[0xC2, 0x01])?; + reply.expand(&[num_keys.saturating_sub(num_certs)])?; + reply.expand(&[0xFE, 0x00])?; + Ok(()) + } } diff --git a/src/piv_types.rs b/src/piv_types.rs index be1975c..595abe1 100644 --- a/src/piv_types.rs +++ b/src/piv_types.rs @@ -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 for $name { + fn eq(&self, other: &u8) -> bool { + *self as u8 == *other + } + } + + impl + Copy> PartialEq 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 { - 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 { + 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 \' (as per definition) +// /// - '81 00' Returns '81 TL \' (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 ' (as per definition) -/// - '81 00' Returns '81 TL ' (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) diff --git a/src/reply.rs b/src/reply.rs new file mode 100644 index 0000000..4ecdcd0 --- /dev/null +++ b/src/reply.rs @@ -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); + +impl<'v, const R: usize> Deref for Reply<'v, R> { + type Target = &'v mut heapless::Vec; + 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, 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::::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); + } +} diff --git a/src/state.rs b/src/state.rs index 62d890b..5b8d64d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,25 +2,28 @@ // SPDX-License-Identifier: LGPL-3.0-only use core::convert::{TryFrom, TryInto}; +use core::mem::replace; +use flexiber::EncodableHeapless; +use heapless::Vec; use heapless_bytes::Bytes; use iso7816::Status; use trussed::{ api::reply::Metadata, config::MAX_MESSAGE_LENGTH, syscall, try_syscall, - types::{KeyId, Location, PathBuf}, + types::{KeyId, KeySerialization, Location, Mechanism, PathBuf, StorageAttributes}, }; -use crate::constants::*; +use crate::piv_types::CardHolderUniqueIdentifier; +use crate::{constants::*, piv_types::AsymmetricAlgorithms}; +use crate::{ + container::{AsymmetricKeyReference, Container, ReadAccessRule, SecurityCondition}, + piv_types::Algorithms, +}; use crate::{Pin, Puk}; -pub enum Key { - Ed25519(KeyId), - P256(KeyId), - X25519(KeyId), -} pub enum PinPolicy { Never, Once, @@ -34,128 +37,153 @@ pub enum TouchPolicy { Cached, } -pub struct Slot { - pub key: Option, - pub pin_policy: PinPolicy, - // touch_policy: TouchPolicy, +crate::container::enum_subset! { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub enum AdministrationAlgorithm: Algorithms { + Tdes, + Aes256 + } } -impl Default for Slot { - fn default() -> Self { - Self { - key: None, - pin_policy: PinPolicy::Once, /*touch_policy: TouchPolicy::Never*/ +impl AdministrationAlgorithm { + pub fn challenge_length(self) -> usize { + match self { + Self::Tdes => 8, + Self::Aes256 => 16, + } + } + + pub fn mechanism(self) -> Mechanism { + match self { + Self::Tdes => Mechanism::Tdes, + Self::Aes256 => Mechanism::Aes256Cbc, + } + } + + pub fn key_len(self) -> usize { + match self { + Self::Tdes => 24, + Self::Aes256 => 32, } } } -impl Slot { - pub fn default(name: SlotName) -> Self { - use SlotName::*; - match name { - // Management => Slot { pin_policy: PinPolicy::Never, ..Default::default() }, - Signature => Slot { - pin_policy: PinPolicy::Always, - ..Default::default() - }, - Pinless => Slot { - pin_policy: PinPolicy::Never, - ..Default::default() - }, - _ => Default::default(), - } - } +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct KeyWithAlg { + pub id: KeyId, + pub alg: A, } -pub struct RetiredSlotIndex(u8); - -impl core::convert::TryFrom for RetiredSlotIndex { - type Error = u8; - fn try_from(i: u8) -> core::result::Result { - if (1..=20).contains(&i) { - Ok(Self(i)) - } else { - Err(i) - } - } -} -pub enum SlotName { - Identity, - Management, // Personalization? Administration? - Signature, - Decryption, // Management after all? - Pinless, - Retired(RetiredSlotIndex), - Attestation, +macro_rules! generate_into_key_with_alg { + ($($name:ident),*) => { + $( + impl From> for KeyWithAlg { + fn from(other: KeyWithAlg<$name>) -> Self { + KeyWithAlg { + id: other.id, + alg: other.alg.into() + } + } + } + )* + }; } -impl SlotName { - pub fn default_pin_policy(&self) -> PinPolicy { - use PinPolicy::*; - use SlotName::*; - match *self { - Signature => Always, - Pinless | Management | Attestation => Never, - _ => Once, - } - } - - pub fn default_slot(&self) -> Slot { - Slot { - key: None, - pin_policy: self.default_pin_policy(), - } - } - - pub fn reference(&self) -> u8 { - use SlotName::*; - match *self { - Identity => 0x9a, - Management => 0x9b, - Signature => 0x9c, - Decryption => 0x9d, - Pinless => 0x9e, - Retired(RetiredSlotIndex(i)) => 0x81 + i, - Attestation => 0xf9, - } - } - pub fn tag(&self) -> u32 { - use SlotName::*; - match *self { - Identity => 0x5fc105, - Management => 0, - Signature => 0x5fc10a, - Decryption => 0x5fc10b, - Pinless => 0x5fc101, - Retired(RetiredSlotIndex(i)) => 0x5fc10c + i as u32, - Attestation => 0x5fff01, - } - } -} +generate_into_key_with_alg!(AsymmetricAlgorithms, AdministrationAlgorithm); #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Keys { // 9a "PIV Authentication Key" (YK: PIV Authentication) - #[serde(skip_serializing_if = "Option::is_none")] - pub authentication_key: Option, + pub authentication: KeyWithAlg, // 9b "PIV Card Application Administration Key" (YK: PIV Management) - pub management_key: KeyId, + pub administration: KeyWithAlg, + pub is_admin_default: bool, // 9c "Digital Signature Key" (YK: Digital Signature) #[serde(skip_serializing_if = "Option::is_none")] - pub signature_key: Option, + pub signature: Option>, // 9d "Key Management Key" (YK: Key Management) #[serde(skip_serializing_if = "Option::is_none")] - pub encryption_key: Option, + pub key_management: Option>, // 9e "Card Authentication Key" (YK: Card Authentication) #[serde(skip_serializing_if = "Option::is_none")] - pub pinless_authentication_key: Option, + pub card_authentication: Option>, // 0x82..=0x95 (130-149) - pub retired_keys: [Option; 20], + pub retired_keys: [Option>; 20], + // pub secure_messaging +} + +impl Keys { + pub fn asymetric_for_reference( + &self, + key: AsymmetricKeyReference, + ) -> Option> { + match key { + AsymmetricKeyReference::PivAuthentication => Some(self.authentication), + AsymmetricKeyReference::DigitalSignature => self.signature, + AsymmetricKeyReference::KeyManagement => self.key_management, + AsymmetricKeyReference::CardAuthentication => self.card_authentication, + AsymmetricKeyReference::Retired01 => self.retired_keys[1], + AsymmetricKeyReference::Retired02 => self.retired_keys[2], + AsymmetricKeyReference::Retired03 => self.retired_keys[3], + AsymmetricKeyReference::Retired04 => self.retired_keys[4], + AsymmetricKeyReference::Retired05 => self.retired_keys[5], + AsymmetricKeyReference::Retired06 => self.retired_keys[6], + AsymmetricKeyReference::Retired07 => self.retired_keys[7], + AsymmetricKeyReference::Retired08 => self.retired_keys[8], + AsymmetricKeyReference::Retired09 => self.retired_keys[9], + AsymmetricKeyReference::Retired10 => self.retired_keys[10], + AsymmetricKeyReference::Retired11 => self.retired_keys[11], + AsymmetricKeyReference::Retired12 => self.retired_keys[12], + AsymmetricKeyReference::Retired13 => self.retired_keys[13], + AsymmetricKeyReference::Retired14 => self.retired_keys[14], + AsymmetricKeyReference::Retired15 => self.retired_keys[15], + AsymmetricKeyReference::Retired16 => self.retired_keys[16], + AsymmetricKeyReference::Retired17 => self.retired_keys[17], + AsymmetricKeyReference::Retired18 => self.retired_keys[18], + AsymmetricKeyReference::Retired19 => self.retired_keys[19], + AsymmetricKeyReference::Retired20 => self.retired_keys[20], + } + } + + pub fn set_asymetric_for_reference( + &mut self, + key: AsymmetricKeyReference, + new: KeyWithAlg, + ) -> Option> { + match key { + AsymmetricKeyReference::PivAuthentication => { + Some(replace(&mut self.authentication, new)) + } + AsymmetricKeyReference::DigitalSignature => self.signature.replace(new), + AsymmetricKeyReference::KeyManagement => self.key_management.replace(new), + AsymmetricKeyReference::CardAuthentication => self.card_authentication.replace(new), + AsymmetricKeyReference::Retired01 => self.retired_keys[1].replace(new), + AsymmetricKeyReference::Retired02 => self.retired_keys[2].replace(new), + AsymmetricKeyReference::Retired03 => self.retired_keys[3].replace(new), + AsymmetricKeyReference::Retired04 => self.retired_keys[4].replace(new), + AsymmetricKeyReference::Retired05 => self.retired_keys[5].replace(new), + AsymmetricKeyReference::Retired06 => self.retired_keys[6].replace(new), + AsymmetricKeyReference::Retired07 => self.retired_keys[7].replace(new), + AsymmetricKeyReference::Retired08 => self.retired_keys[8].replace(new), + AsymmetricKeyReference::Retired09 => self.retired_keys[9].replace(new), + AsymmetricKeyReference::Retired10 => self.retired_keys[10].replace(new), + AsymmetricKeyReference::Retired11 => self.retired_keys[11].replace(new), + AsymmetricKeyReference::Retired12 => self.retired_keys[12].replace(new), + AsymmetricKeyReference::Retired13 => self.retired_keys[13].replace(new), + AsymmetricKeyReference::Retired14 => self.retired_keys[14].replace(new), + AsymmetricKeyReference::Retired15 => self.retired_keys[15].replace(new), + AsymmetricKeyReference::Retired16 => self.retired_keys[16].replace(new), + AsymmetricKeyReference::Retired17 => self.retired_keys[17].replace(new), + AsymmetricKeyReference::Retired18 => self.retired_keys[18].replace(new), + AsymmetricKeyReference::Retired19 => self.retired_keys[19].replace(new), + AsymmetricKeyReference::Retired20 => self.retired_keys[20].replace(new), + } + } } #[derive(Debug, Default, Eq, PartialEq)] pub struct State { - pub runtime: Runtime, + pub volatile: Volatile, pub persistent: Option, } @@ -165,7 +193,7 @@ impl State { self.persistent = Some(Persistent::load_or_initialize(client)?); } Ok(LoadedState { - runtime: &mut self.runtime, + volatile: &mut self.volatile, persistent: self.persistent.as_mut().unwrap(), }) } @@ -184,7 +212,7 @@ impl State { #[derive(Debug, Eq, PartialEq)] pub struct LoadedState<'t> { - pub runtime: &'t mut Runtime, + pub volatile: &'t mut Volatile, pub persistent: &'t mut Persistent, } @@ -202,12 +230,10 @@ pub struct Persistent { // pin_hash: Option<[u8; 16]>, // Ideally, we'd dogfood a "Monotonic Counter" from `trussed`. timestamp: u32, - // must be a valid RFC 4122 UUID 1, 2 or 4 - guid: [u8; 16], } #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Runtime { +pub struct Volatile { // aid: Option< // consecutive_pin_mismatches: u8, pub global_security_status: GlobalSecurityStatus, @@ -216,59 +242,6 @@ pub struct Runtime { pub command_cache: Option, } -// pub trait Aid { -// const AID: &'static [u8]; -// const RIGHT_TRUNCATED_LENGTH: usize; - -// fn len() -> usize { -// Self::AID.len() -// } - -// fn full() -> &'static [u8] { -// Self::AID -// } - -// fn right_truncated() -> &'static [u8] { -// &Self::AID[..Self::RIGHT_TRUNCATED_LENGTH] -// } - -// fn pix() -> &'static [u8] { -// &Self::AID[5..] -// } - -// fn rid() -> &'static [u8] { -// &Self::AID[..5] -// } -// } - -// #[derive(Copy, Clone, Debug, Eq, PartialEq)] -// pub enum SelectableAid { -// Piv(PivAid), -// YubicoOtp(YubicoOtpAid), -// } - -// impl Default for SelectableAid { -// fn default() -> Self { -// Self::Piv(Default::default()) -// } -// } - -// #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -// pub struct PivAid {} - -// impl Aid for PivAid { -// const AID: &'static [u8] = &PIV_AID; -// const RIGHT_TRUNCATED_LENGTH: usize = 9; -// } - -// #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -// pub struct YubicoOtpAid {} - -// impl Aid for YubicoOtpAid { -// const AID: &'static [u8] = &YUBICO_OTP_AID; -// const RIGHT_TRUNCATED_LENGTH: usize = 8; -// } - #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GlobalSecurityStatus {} @@ -279,6 +252,40 @@ pub enum SecurityStatus { NotVerified, } +impl Volatile { + pub fn security_valid(&self, condition: SecurityCondition) -> bool { + use SecurityCondition::*; + match condition { + Pin => self.app_security_status.pin_verified, + Always => true, + } + } + + pub fn read_valid(&self, condition: ReadAccessRule) -> bool { + use ReadAccessRule::*; + match condition { + Pin | PinOrOcc => self.app_security_status.pin_verified, + Always => true, + } + } + + pub fn take_single_challenge(&mut self) -> Option> { + match self.command_cache.take() { + Some(CommandCache::SingleAuthChallenge(b)) => return Some(b), + old => self.command_cache = old, + }; + None + } + + pub fn take_mutual_challenge(&mut self) -> Option> { + match self.command_cache.take() { + Some(CommandCache::MutualAuthChallenge(b)) => return Some(b), + old => self.command_cache = old, + }; + None + } +} + impl Default for SecurityStatus { fn default() -> Self { Self::NotVerified @@ -289,21 +296,13 @@ impl Default for SecurityStatus { pub struct AppSecurityStatus { pub pin_verified: bool, pub puk_verified: bool, - pub management_verified: bool, + pub administrator_verified: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum CommandCache { - GetData(GetData), - AuthenticateManagement(AuthenticateManagement), -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct GetData {} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AuthenticateManagement { - pub challenge: [u8; 8], + SingleAuthChallenge(Bytes<16>), + MutualAuthChallenge(Bytes<16>), } impl Persistent { @@ -314,10 +313,6 @@ impl Persistent { const DEFAULT_PIN: &'static [u8] = b"123456\xff\xff"; const DEFAULT_PUK: &'static [u8] = b"12345678"; - pub fn guid(&self) -> [u8; 16] { - self.guid - } - pub fn remaining_pin_retries(&self) -> u8 { if self.consecutive_pin_mismatches >= Self::PIN_RETRIES_DEFAULT { 0 @@ -335,24 +330,44 @@ impl Persistent { } // FIXME: revisit with trussed pin management - pub fn verify_pin(&self, other_pin: &Pin) -> bool { - // hprintln!("verifying pin {:?} against {:?}", other_pin, &self.pin).ok(); - self.pin == *other_pin + pub fn verify_pin(&mut self, other_pin: &Pin, client: &mut impl trussed::Client) -> bool { + if self.remaining_pin_retries() == 0 { + return false; + } + self.consecutive_pin_mismatches += 1; + self.save(client); + if self.pin == *other_pin { + self.consecutive_pin_mismatches = 0; + true + } else { + false + } } // FIXME: revisit with trussed pin management - pub fn verify_puk(&self, other_puk: &Puk) -> bool { - // hprintln!("verifying puk {:?} against {:?}", other_puk, &self.puk).ok(); - self.puk == *other_puk + pub fn verify_puk(&mut self, other_puk: &Puk, client: &mut impl trussed::Client) -> bool { + if self.remaining_puk_retries() == 0 { + return false; + } + self.consecutive_puk_mismatches += 1; + self.save(client); + if self.puk == *other_puk { + self.consecutive_puk_mismatches = 0; + true + } else { + false + } } pub fn set_pin(&mut self, new_pin: Pin, client: &mut impl trussed::Client) { self.pin = new_pin; + self.consecutive_pin_mismatches = 0; self.save(client); } pub fn set_puk(&mut self, new_puk: Puk, client: &mut impl trussed::Client) { self.puk = new_puk; + self.consecutive_puk_mismatches = 0; self.save(client); } @@ -410,33 +425,84 @@ impl Persistent { Self::PUK_RETRIES_DEFAULT } - pub fn reset_management_key(&mut self, client: &mut impl trussed::Client) { - self.set_management_key(YUBICO_DEFAULT_MANAGEMENT_KEY, client); + pub fn reset_administration_key(&mut self, client: &mut impl trussed::Client) { + self.set_administration_key( + YUBICO_DEFAULT_MANAGEMENT_KEY, + YUBICO_DEFAULT_MANAGEMENT_KEY_ALG, + client, + ); } - pub fn set_management_key( + pub fn set_administration_key( &mut self, - management_key: &[u8; 24], + management_key: &[u8], + alg: AdministrationAlgorithm, client: &mut impl trussed::Client, ) { // let new_management_key = syscall!(self.trussed.unsafe_inject_tdes_key( - let new_management_key = - syscall!(client - .unsafe_inject_shared_key(management_key, trussed::types::Location::Internal,)) - .key; - let old_management_key = self.keys.management_key; - self.keys.management_key = new_management_key; + let id = syscall!(client.unsafe_inject_key( + alg.mechanism(), + management_key, + trussed::types::Location::Internal, + KeySerialization::Raw + )) + .key; + let old_management_key = self.keys.administration.id; + self.keys.administration = KeyWithAlg { id, alg }; self.save(client); syscall!(client.delete(old_management_key)); } - pub fn initialize(client: &mut impl trussed::Client) -> Self { - info!("initializing PIV state"); - let management_key = syscall!(client.unsafe_inject_shared_key( - YUBICO_DEFAULT_MANAGEMENT_KEY, - trussed::types::Location::Internal, + fn set_asymmetric_key( + &mut self, + key: AsymmetricKeyReference, + id: KeyId, + alg: AsymmetricAlgorithms, + ) -> Option> { + self.keys + .set_asymetric_for_reference(key, KeyWithAlg { id, alg }) + } + + pub fn generate_asymmetric_key( + &mut self, + key: AsymmetricKeyReference, + alg: AsymmetricAlgorithms, + client: &mut impl trussed::Client, + ) -> KeyId { + let id = syscall!(client.generate_key( + alg.key_mechanism(), + StorageAttributes::default().set_persistence(Location::Internal) )) .key; + let old = self.set_asymmetric_key(key, id, alg); + self.save(client); + if let Some(old) = old { + syscall!(client.delete(old.id)); + } + id + } + + pub fn initialize(client: &mut impl trussed::Client) -> Self { + info!("initializing PIV state"); + let administration = KeyWithAlg { + id: syscall!(client.unsafe_inject_key( + YUBICO_DEFAULT_MANAGEMENT_KEY_ALG.mechanism(), + YUBICO_DEFAULT_MANAGEMENT_KEY, + trussed::types::Location::Internal, + KeySerialization::Raw + )) + .key, + alg: YUBICO_DEFAULT_MANAGEMENT_KEY_ALG, + }; + + let authentication = KeyWithAlg { + id: syscall!(client.generate_key( + Mechanism::P256, + StorageAttributes::new().set_persistence(Location::Internal) + )) + .key, + alg: AsymmetricAlgorithms::P256, + }; let mut guid: [u8; 16] = syscall!(client.random_bytes(16)) .bytes @@ -447,12 +513,24 @@ impl Persistent { guid[6] = (guid[6] & 0xf) | 0x40; guid[8] = (guid[8] & 0x3f) | 0x80; + let guid_file: Vec = CardHolderUniqueIdentifier::default() + .with_guid(guid) + .to_heapless_vec() + .unwrap(); + ContainerStorage(Container::CardHolderUniqueIdentifier) + .save( + client, + &guid_file[2..], // Remove the unnecessary 53 tag + ) + .ok(); + let keys = Keys { - authentication_key: None, - management_key, - signature_key: None, - encryption_key: None, - pinless_authentication_key: None, + authentication, + administration, + is_admin_default: true, + signature: None, + key_management: None, + card_authentication: None, retired_keys: Default::default(), }; @@ -463,7 +541,6 @@ impl Persistent { pin: Pin::try_from(Self::DEFAULT_PIN).unwrap(), puk: Puk::try_from(Self::DEFAULT_PUK).unwrap(), timestamp: 0, - guid, }; state.save(client); state @@ -523,3 +600,108 @@ fn load_if_exists( }, } } + +#[derive(Clone, Copy, Debug)] +pub struct ContainerStorage(pub Container); + +impl ContainerStorage { + fn path(self) -> PathBuf { + PathBuf::from(match self.0 { + Container::CardCapabilityContainer => "CardCapabilityContainer", + Container::CardHolderUniqueIdentifier => "CardHolderUniqueIdentifier", + Container::X509CertificateFor9A => "X509CertificateFor9A", + Container::CardholderFingerprints => "CardholderFingerprints", + Container::SecurityObject => "SecurityObject", + Container::CardholderFacialImage => "CardholderFacialImage", + Container::X509CertificateFor9E => "X509CertificateFor9E", + Container::X509CertificateFor9C => "X509CertificateFor9C", + Container::X509CertificateFor9D => "X509CertificateFor9D", + Container::PrintedInformation => "PrintedInformation", + Container::DiscoveryObject => "DiscoveryObject", + Container::KeyHistoryObject => "KeyHistoryObject", + Container::RetiredCert01 => "RetiredCert01", + Container::RetiredCert02 => "RetiredCert02", + Container::RetiredCert03 => "RetiredCert03", + Container::RetiredCert04 => "RetiredCert04", + Container::RetiredCert05 => "RetiredCert05", + Container::RetiredCert06 => "RetiredCert06", + Container::RetiredCert07 => "RetiredCert07", + Container::RetiredCert08 => "RetiredCert08", + Container::RetiredCert09 => "RetiredCert09", + Container::RetiredCert10 => "RetiredCert10", + Container::RetiredCert11 => "RetiredCert11", + Container::RetiredCert12 => "RetiredCert12", + Container::RetiredCert13 => "RetiredCert13", + Container::RetiredCert14 => "RetiredCert14", + Container::RetiredCert15 => "RetiredCert15", + Container::RetiredCert16 => "RetiredCert16", + Container::RetiredCert17 => "RetiredCert17", + Container::RetiredCert18 => "RetiredCert18", + Container::RetiredCert19 => "RetiredCert19", + Container::RetiredCert20 => "RetiredCert20", + Container::CardholderIrisImages => "CardholderIrisImages", + Container::BiometricInformationTemplatesGroupTemplate => { + "BiometricInformationTemplatesGroupTemplate" + } + Container::SecureMessagingCertificateSigner => "SecureMessagingCertificateSigner", + Container::PairingCodeReferenceDataContainer => "PairingCodeReferenceDataContainer", + }) + } + + fn default(self) -> Option> { + match self.0 { + Container::CardHolderUniqueIdentifier => panic!("CHUID should alway be set"), + Container::CardCapabilityContainer => Some( + crate::piv_types::CardCapabilityContainer::default() + .to_heapless_vec() + .unwrap(), + ), + Container::DiscoveryObject => Some(Vec::from_slice(&DISCOVERY_OBJECT).unwrap()), + _ => None, + } + } + + pub fn exists(self, client: &mut impl trussed::Client) -> Result { + match try_syscall!(client.entry_metadata(Location::Internal, self.path())) { + Ok(Metadata { metadata: None }) => Ok(false), + Ok(Metadata { + metadata: Some(metadata), + }) if metadata.is_file() => Ok(true), + Ok(Metadata { + metadata: Some(_metadata), + }) => { + error!( + "File {} exists but isn't a file: {_metadata:?}", + self.path() + ); + Err(Status::UnspecifiedPersistentExecutionError) + } + Err(_err) => { + error!("File {} couldn't be read: {_err:?}", self.path()); + Err(Status::UnspecifiedPersistentExecutionError) + } + } + } + + pub fn load( + self, + client: &mut impl trussed::Client, + ) -> Result>, Status> { + load_if_exists(client, Location::Internal, &self.path()) + .map(|data| data.or_else(|| self.default().map(Bytes::from))) + } + + pub fn save(self, client: &mut impl trussed::Client, bytes: &[u8]) -> Result<(), Status> { + let msg = Bytes::from(heapless::Vec::try_from(bytes).map_err(|_| { + error!("Buffer full"); + Status::IncorrectDataParameter + })?); + try_syscall!(client.write_file(Location::Internal, self.path(), msg, None)).map_err( + |_err| { + error!("Failed to store data: {_err:?}"); + Status::UnspecifiedNonpersistentExecutionError + }, + )?; + Ok(()) + } +} diff --git a/src/tlv.rs b/src/tlv.rs new file mode 100644 index 0000000..07158a3 --- /dev/null +++ b/src/tlv.rs @@ -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()) + ); + } +} diff --git a/src/vpicc.rs b/src/vpicc.rs index 989a18d..e7b82bb 100644 --- a/src/vpicc.rs +++ b/src/vpicc.rs @@ -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}; diff --git a/tests/card/mod.rs b/tests/card/mod.rs new file mode 100644 index 0000000..de845ae --- /dev/null +++ b/tests/card/mod.rs @@ -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 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 +} diff --git a/tests/command_response.ron b/tests/command_response.ron index bcee492..d1489dd 100644 --- a/tests/command_response.ron +++ b/tests/command_response.ron @@ -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") + ), + ] + ) ] diff --git a/tests/command_response.rs b/tests/command_response.rs index 868976e..9c3334d 100644 --- a/tests/command_response.rs +++ b/tests/command_response.rs @@ -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 { 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 { buf } -#[allow(unused)] fn tlv(tag: &[u8], data: &[u8]) -> Vec { 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 { buf } -#[allow(unused)] fn build_command(cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8], le: u16) -> Vec { 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 { 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 { + println!("Command: {input:x?}"); let mut rep: heapless::Vec = 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, - ) + ); } } diff --git a/tests/default_admin_key b/tests/default_admin_key new file mode 100644 index 0000000..e4c53ad --- /dev/null +++ b/tests/default_admin_key @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/generate_asymmetric_keypair.rs b/tests/generate_asymmetric_keypair.rs index 8f787f2..03b244c 100644 --- a/tests/generate_asymmetric_keypair.rs +++ b/tests/generate_asymmetric_keypair.rs @@ -12,7 +12,7 @@ mod setup; // # 0xAB = Yubico extension (of course...), TouchPolicy, 0x2 = // AB 01 02 -#[test] +#[test_log::test] fn gen_keypair() { let _cmd = cmd!("00 47 00 9A 0B AC 09 80 01 11 AA 01 02 AB 01 02"); diff --git a/tests/get_data.rs b/tests/get_data.rs index 2b94b0a..fac73fc 100644 --- a/tests/get_data.rs +++ b/tests/get_data.rs @@ -6,7 +6,7 @@ mod setup; // use delog::hex_str; // use iso7816::Status::*; -#[test] +#[test_log::test] fn get_data() { // let cmd = cmd!("00 47 00 9A 0B AC 09 80 01 11 AA 01 02 AB 01 02"); // let cmd = cmd!("00 47 00 9A 0B AC 09 80 01 11 AA 01 02 AB 01 02"); diff --git a/tests/opensc.rs b/tests/opensc.rs new file mode 100644 index 0000000..86dd036 --- /dev/null +++ b/tests/opensc.rs @@ -0,0 +1,86 @@ +// Copyright (C) 2022 Nitrokey GmbH +// SPDX-License-Identifier: LGPL-3.0-only + +#![cfg(all(feature = "virtual", feature = "opensc-tests"))] + +mod card; + +use std::process::Command; + +use card::with_vsc; + +use expectrl::{spawn, Eof, WaitStatus}; + +#[test_log::test] +fn list() { + with_vsc(|| { + let mut p = spawn("piv-tool -n").unwrap(); + p.expect("Using reader with a card: Virtual PCD 00 00") + .unwrap(); + p.expect("Personal Identity Verification Card").unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + }); +} + +#[test_log::test] +fn admin_mutual() { + with_vsc(|| { + let mut command = Command::new("piv-tool"); + command + .env("PIV_EXT_AUTH_KEY", "tests/default_admin_key") + .args(["-A", "M:9B:03"]); + let mut p = expectrl::session::Session::spawn(command).unwrap(); + p.expect("Using reader with a card: Virtual PCD 00 00") + .unwrap(); + // p.expect("Personal Identity Verification Card").unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + }); +} + +/// Fails because of https://github.com/OpenSC/OpenSC/issues/2658 +#[test_log::test] +#[ignore] +fn admin_card() { + with_vsc(|| { + let mut command = Command::new("piv-tool"); + command + .env("PIV_EXT_AUTH_KEY", "tests/default_admin_key") + .args(["-A", "A:9B:03"]); + let mut p = expectrl::session::Session::spawn(command).unwrap(); + p.expect("Using reader with a card: Virtual PCD 00 00") + .unwrap(); + p.expect("Personal Identity Verification Card").unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + }); +} + +#[test_log::test] +fn generate_key() { + // with_vsc(|| { + // let mut command = Command::new("piv-tool"); + // command + // .env("PIV_EXT_AUTH_KEY", "tests/default_admin_key") + // .args(["-A", "M:9B:03", "-G", "9A:11"]); + // let mut p = expectrl::session::Session::spawn(command).unwrap(); + // p.expect("Using reader with a card: Virtual PCD 00 00") + // .unwrap(); + // p.expect(Eof).unwrap(); + // // Non zero exit code? + // assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 1)); + // }); + // with_vsc(|| { + // let mut command = Command::new("piv-tool"); + // command + // .env("PIV_EXT_AUTH_KEY", "tests/default_admin_key") + // .args(["-A", "M:9B:03", "-G", "9A:07"]); + // let mut p = expectrl::session::Session::spawn(command).unwrap(); + // p.expect("Using reader with a card: Virtual PCD 00 00") + // .unwrap(); + // p.expect(Eof).unwrap(); + // // Non zero exit code? + // assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 1)); + // }); +} diff --git a/tests/pivy.rs b/tests/pivy.rs new file mode 100644 index 0000000..5544eb0 --- /dev/null +++ b/tests/pivy.rs @@ -0,0 +1,78 @@ +// Copyright (C) 2022 Nitrokey GmbH +// SPDX-License-Identifier: LGPL-3.0-only + +#![cfg(all(feature = "virtual", feature = "pivy-tests"))] + +mod card; + +use card::with_vsc; + +use expectrl::{spawn, Eof, Regex, WaitStatus}; + +use std::io::Write; +use std::process::{Command, Stdio}; + +#[test_log::test] +fn list() { + with_vsc(|| { + let mut p = spawn("pivy-tool list").unwrap(); + p.expect(Regex("card: [0-9A-Z]*")).unwrap(); + p.expect("device: Virtual PCD 00 00").unwrap(); + p.expect("chuid: ok").unwrap(); + p.expect(Regex("guid: [0-9A-Z]*")).unwrap(); + p.expect("algos: 3DES AES256 ECCP256 (null) (null)") + .unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + }); +} + +#[test_log::test] +fn generate() { + with_vsc(|| { + let mut p = spawn("pivy-tool -A 3des -K 010203040506070801020304050607080102030405060708 generate 9A -a eccp256 -P 123456").unwrap(); + p.expect(Regex( + "ecdsa-sha2-nistp256 (?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)? PIV_slot_9A@[A-F0-9]{20}", + )) + .unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + }); + with_vsc(|| { + let mut p = spawn("pivy-tool -A 3des -K 010203040506070801020304050607080102030405060708 generate 9A -a rsa2048 -P 123456").unwrap(); + p.expect(Regex( + "ssh-rsa (?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)? PIV_slot_9A@[A-F0-9]{20}", + )) + .unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + }); +} + +#[test_log::test] +fn ecdh() { + with_vsc(|| { + let mut p = spawn("pivy-tool -A 3des -K 010203040506070801020304050607080102030405060708 generate 9A -a eccp256 -P 123456").unwrap(); + p.expect(Regex( + "ecdsa-sha2-nistp256 (?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)? PIV_slot_9A@[A-F0-9]{20}", + )) + .unwrap(); + p.expect(Eof).unwrap(); + assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); + + let mut p = Command::new("pivy-tool") + .args(["ecdh", "9A", "-P", "123456"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let mut stdin = p.stdin.take().unwrap(); + write!(stdin, + "ecdsa-sha2-nistp256 \ + AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIK+WUxBiBEwHgT4ykw3FDC1kRRMZCQo2+iM9+8WQgz7eFhEcU78eVweIrqG0nyJaZeWhgcYTSDP+VisDftiQgo= \ + PIV_slot_9A@6E9BCA45D8AF4B9D95AA2E8C8C23BA49" ).unwrap(); + drop(stdin); + + assert_eq!(p.wait().unwrap().code(), Some(0)); + }); +} diff --git a/tests/put_data.rs b/tests/put_data.rs index a92e501..e498aed 100644 --- a/tests/put_data.rs +++ b/tests/put_data.rs @@ -17,7 +17,7 @@ mod setup; // # actual data // 88 1A 89 18 AA 81 D5 48 A5 EC 26 01 60 BA 06 F6 EC 3B B6 05 00 2E B6 3D 4B 28 7F 86 -#[test] +#[test_log::test] fn put_data() { setup::piv(|_piv| { diff --git a/tests/setup/mod.rs b/tests/setup/mod.rs index 91b9391..d559ced 100644 --- a/tests/setup/mod.rs +++ b/tests/setup/mod.rs @@ -11,12 +11,13 @@ macro_rules! cmd { }; } -use trussed::virt::{Client, Ram}; +use trussed::virt::Ram; +use trussed_rsa_alloc::virt::Client; pub type Piv = piv_authenticator::Authenticator>; pub fn piv(test: impl FnOnce(&mut Piv) -> R) -> R { - trussed::virt::with_ram_client("test", |client| { + trussed_rsa_alloc::virt::with_ram_client("test", |client| { let mut piv_app = piv_authenticator::Authenticator::new(client); test(&mut piv_app) })