diff --git a/.gitignore b/.gitignore index 1b320462..611b2781 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Cargo.lock /reproducible/binaries.sha256sum /reproducible/elf2tab.txt /reproducible/reproduced.tar +__pycache__ diff --git a/Cargo.toml b/Cargo.toml index ed293cc8..15984a0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ctap2" -version = "0.1.0" +version = "1.0.0" authors = [ "Fabian Kaczmarczyck ", "Guillaume Endignoux ", @@ -35,7 +35,6 @@ elf2tab = "0.6.0" enum-iterator = "0.6.0" [build-dependencies] -openssl = "0.10" uuid = { version = "0.8", features = ["v4"] } [profile.dev] diff --git a/README.md b/README.md index ccb6fc73..68e9f717 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # OpenSK logo -[![Build Status](https://travis-ci.org/google/OpenSK.svg?branch=master)](https://travis-ci.org/google/OpenSK) ![markdownlint](https://github.com/google/OpenSK/workflows/markdownlint/badge.svg?branch=master) ![pylint](https://github.com/google/OpenSK/workflows/pylint/badge.svg?branch=master) ![Cargo check](https://github.com/google/OpenSK/workflows/Cargo%20check/badge.svg?branch=master) @@ -31,7 +30,8 @@ our implementation was not reviewed nor officially tested and doesn't claim to be FIDO Certified. We started adding features of the upcoming next version of the [CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). -The development is currently between 2.0 and 2.1, with updates hidden behind a feature flag. +The development is currently between 2.0 and 2.1, with updates hidden behind +a feature flag. Please add the flag `--ctap2.1` to the deploy command to include them. ### Cryptography @@ -58,8 +58,8 @@ For a more detailed guide, please refer to our ./setup.sh ``` -2. Next step is to install Tock OS as well as the OpenSK application on your - board (**Warning**: it will erase the locally stored credentials). Run: +1. Next step is to install Tock OS as well as the OpenSK application on your + board. Run: ```shell # Nordic nRF52840-DK board @@ -68,7 +68,17 @@ For a more detailed guide, please refer to our ./deploy.py --board=nrf52840_dongle --opensk ``` -3. On Linux, you may want to avoid the need for `root` privileges to interact +1. Finally you need to inject the cryptographic material if you enabled + batch attestation or CTAP1/U2F compatibility (which is the case by + default): + + ```shell + ./tools/configure.py \ + --certificate=crypto_data/opensk_cert.pem \ + --private-key=crypto_data/opensk.key + ``` + +1. On Linux, you may want to avoid the need for `root` privileges to interact with the key. For that purpose we provide a udev rule file that can be installed with the following command: @@ -148,7 +158,7 @@ operation. The additional output looks like the following. -``` +```text # Allocation of 256 byte(s), aligned on 1 byte(s). The allocated address is # 0x2002401c. After this operation, 2 pointers have been allocated, totalling # 384 bytes (the total heap usage may be larger, due to alignment and @@ -163,12 +173,12 @@ A tool is provided to analyze such reports, in `tools/heapviz`. This tool parses the console output, identifies the lines corresponding to (de)allocation operations, and first computes some statistics: -- Address range used by the heap over this run of the program, -- Peak heap usage (how many useful bytes are allocated), -- Peak heap consumption (how many bytes are used by the heap, including - unavailable bytes between allocated blocks, due to alignment constraints and - memory fragmentation), -- Fragmentation overhead (difference between heap consumption and usage). +* Address range used by the heap over this run of the program, +* Peak heap usage (how many useful bytes are allocated), +* Peak heap consumption (how many bytes are used by the heap, including + unavailable bytes between allocated blocks, due to alignment constraints and + memory fragmentation), +* Fragmentation overhead (difference between heap consumption and usage). Then, the `heapviz` tool displays an animated "movie" of the allocated bytes in heap memory. Each frame in this "movie" shows bytes that are currently @@ -177,10 +187,11 @@ allocated. A new frame is generated for each (de)allocation operation. This tool uses the `ncurses` library, that you may have to install beforehand. You can control the tool with the following parameters: -- `--logfile` (required) to provide the file which contains the console output - to parse, -- `--fps` (optional) to customize the number of frames per second in the movie - animation. + +* `--logfile` (required) to provide the file which contains the console output + to parse, +* `--fps` (optional) to customize the number of frames per second in the movie + animation. ```shell cargo run --manifest-path tools/heapviz/Cargo.toml -- --logfile console.log --fps 50 diff --git a/boards/nordic/nrf52840_mdk_dfu/src/main.rs b/boards/nordic/nrf52840_mdk_dfu/src/main.rs index a346da51..1eccb0e7 100644 --- a/boards/nordic/nrf52840_mdk_dfu/src/main.rs +++ b/boards/nordic/nrf52840_mdk_dfu/src/main.rs @@ -48,7 +48,7 @@ static STRINGS: &'static [&'static str] = &[ // Product "OpenSK", // Serial number - "v0.1", + "v1.0", ]; // State for loading and holding applications. diff --git a/build.rs b/build.rs index e9815556..b581d8eb 100644 --- a/build.rs +++ b/build.rs @@ -12,11 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use openssl::asn1; -use openssl::ec; -use openssl::nid::Nid; -use openssl::pkey::PKey; -use openssl::x509; use std::env; use std::fs::File; use std::io::Read; @@ -25,65 +20,11 @@ use std::path::Path; use uuid::Uuid; fn main() { - println!("cargo:rerun-if-changed=crypto_data/opensk.key"); - println!("cargo:rerun-if-changed=crypto_data/opensk_cert.pem"); println!("cargo:rerun-if-changed=crypto_data/aaguid.txt"); let out_dir = env::var_os("OUT_DIR").unwrap(); - let priv_key_bin_path = Path::new(&out_dir).join("opensk_pkey.bin"); - let cert_bin_path = Path::new(&out_dir).join("opensk_cert.bin"); let aaguid_bin_path = Path::new(&out_dir).join("opensk_aaguid.bin"); - // Load the OpenSSL PEM ECC key - let ecc_data = include_bytes!("crypto_data/opensk.key"); - let pkey = - ec::EcKey::private_key_from_pem(ecc_data).expect("Failed to load OpenSK private key file"); - - // Check key validity - pkey.check_key().unwrap(); - assert_eq!(pkey.group().curve_name(), Some(Nid::X9_62_PRIME256V1)); - - // Private keys generated by OpenSSL have variable size but we only handle - // constant size. Serialization is done in big endian so if the size is less - // than 32 bytes, we need to prepend with null bytes. - // If the size is 33 bytes, this means the serialized BigInt is negative. - // Any other size is invalid. - let priv_key_hex = pkey.private_key().to_hex_str().unwrap(); - let priv_key_vec = pkey.private_key().to_vec(); - let key_len = priv_key_vec.len(); - - assert!( - key_len <= 33, - "Invalid private key (too big): {} ({:#?})", - priv_key_hex, - priv_key_vec, - ); - - // Copy OpenSSL generated key to our vec, starting from the end - let mut output_vec = [0u8; 32]; - let min_key_len = std::cmp::min(key_len, 32); - output_vec[32 - min_key_len..].copy_from_slice(&priv_key_vec[key_len - min_key_len..]); - - // Create the raw private key out of the OpenSSL data - let mut priv_key_bin_file = File::create(&priv_key_bin_path).unwrap(); - priv_key_bin_file.write_all(&output_vec).unwrap(); - - // Convert the PEM certificate to DER and extract the serial for AAGUID - let input_pem_cert = include_bytes!("crypto_data/opensk_cert.pem"); - let cert = x509::X509::from_pem(input_pem_cert).expect("Failed to load OpenSK certificate"); - - // Do some sanity check on the certificate - assert!(cert - .public_key() - .unwrap() - .public_eq(&PKey::from_ec_key(pkey).unwrap())); - let now = asn1::Asn1Time::days_from_now(0).unwrap(); - assert!(cert.not_after() > now); - assert!(cert.not_before() <= now); - - let mut cert_bin_file = File::create(&cert_bin_path).unwrap(); - cert_bin_file.write_all(&cert.to_der().unwrap()).unwrap(); - let mut aaguid_bin_file = File::create(&aaguid_bin_path).unwrap(); let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap(); let mut content = String::new(); diff --git a/deploy.py b/deploy.py index 42579a2b..b3daa6fa 100755 --- a/deploy.py +++ b/deploy.py @@ -710,6 +710,22 @@ def run(self): check=False, timeout=None, ).returncode + + # Configure OpenSK through vendor specific command if needed + if any([ + self.args.lock_device, + self.args.config_cert, + self.args.config_pkey, + ]): + # pylint: disable=g-import-not-at-top,import-outside-toplevel + import tools.configure + tools.configure.main( + argparse.Namespace( + batch=False, + certificate=self.args.config_cert, + priv_key=self.args.config_pkey, + lock=self.args.lock_device, + )) return 0 @@ -770,6 +786,33 @@ def main(args): help=("Erases the persistent storage when installing an application. " "All stored data will be permanently lost."), ) + main_parser.add_argument( + "--lock-device", + action="store_true", + default=False, + dest="lock_device", + help=("Try to disable JTAG at the end of the operations. This " + "operation may fail if the device is already locked or if " + "the certificate/private key are not programmed."), + ) + main_parser.add_argument( + "--inject-certificate", + default=None, + metavar="PEM_FILE", + type=argparse.FileType("rb"), + dest="config_cert", + help=("If this option is set, the corresponding certificate " + "will be programmed into the key as the last operation."), + ) + main_parser.add_argument( + "--inject-private-key", + default=None, + metavar="PEM_FILE", + type=argparse.FileType("rb"), + dest="config_pkey", + help=("If this option is set, the corresponding private key " + "will be programmed into the key as the last operation."), + ) main_parser.add_argument( "--programmer", metavar="METHOD", diff --git a/docs/install.md b/docs/install.md index cf355fac..d00b991a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -125,6 +125,7 @@ This is the expected content after running our `setup.sh` script: File | Purpose ----------------- | -------------------------------------------------------- +`aaguid.txt` | Text file containaing the AAGUID value `opensk_ca.csr` | Certificate sign request for the Root CA `opensk_ca.key` | ECC secp256r1 private key used for the Root CA `opensk_ca.pem` | PEM encoded certificate of the Root CA @@ -136,9 +137,11 @@ File | Purpose If you want to use your own attestation certificate and private key, simply replace `opensk_cert.pem` and `opensk.key` files. -Our build script `build.rs` is responsible for converting `opensk_cert.pem` and -`opensk.key` files into raw data that is then used by the Rust file: -`src/ctap/key_material.rs`. +Our build script `build.rs` is responsible for converting the `aaguid.txt` file +into raw data that is then used by the Rust file `src/ctap/key_material.rs`. + +Our configuration script `tools/configure.py` is responsible for configuring +an OpenSK device with the correct certificate and private key. ### Flashing a firmware diff --git a/patches/tock/02-usb.patch b/patches/tock/02-usb.patch index 135369d2..6220f6a1 100644 --- a/patches/tock/02-usb.patch +++ b/patches/tock/02-usb.patch @@ -117,7 +117,7 @@ index d72d20482..118ea6d68 100644 + // Product + "OpenSK", + // Serial number -+ "v0.1", ++ "v1.0", +]; + // State for loading and holding applications. @@ -189,7 +189,7 @@ index 2ebb384d8..4a7bfffdd 100644 + // Product + "OpenSK", + // Serial number -+ "v0.1", ++ "v1.0", +]; + // State for loading and holding applications. diff --git a/patches/tock/06-update-uicr.patch b/patches/tock/06-update-uicr.patch new file mode 100644 index 00000000..53ee945b --- /dev/null +++ b/patches/tock/06-update-uicr.patch @@ -0,0 +1,100 @@ +diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs +index 6bb6c86..3bb8b5a 100644 +--- a/chips/nrf52/src/uicr.rs ++++ b/chips/nrf52/src/uicr.rs +@@ -1,38 +1,45 @@ + //! User information configuration registers +-//! +-//! Minimal implementation to support activation of the reset button on +-//! nRF52-DK. ++ + + use enum_primitive::cast::FromPrimitive; +-use kernel::common::registers::{register_bitfields, ReadWrite}; ++use kernel::common::registers::{register_bitfields, register_structs, ReadWrite}; + use kernel::common::StaticRef; ++use kernel::hil; ++use kernel::ReturnCode; + + use crate::gpio::Pin; + + const UICR_BASE: StaticRef = +- unsafe { StaticRef::new(0x10001200 as *const UicrRegisters) }; +- +-#[repr(C)] +-struct UicrRegisters { +- /// Mapping of the nRESET function (see POWER chapter for details) +- /// - Address: 0x200 - 0x204 +- pselreset0: ReadWrite, +- /// Mapping of the nRESET function (see POWER chapter for details) +- /// - Address: 0x204 - 0x208 +- pselreset1: ReadWrite, +- /// Access Port protection +- /// - Address: 0x208 - 0x20c +- approtect: ReadWrite, +- /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO +- /// - Address: 0x20c - 0x210 +- nfcpins: ReadWrite, +- _reserved1: [u32; 60], +- /// External circuitry to be supplied from VDD pin. +- /// - Address: 0x300 - 0x304 +- extsupply: ReadWrite, +- /// GPIO reference voltage +- /// - Address: 0x304 - 0x308 +- regout0: ReadWrite, ++ unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; ++ ++register_structs! { ++ UicrRegisters { ++ (0x000 => _reserved1), ++ /// Reserved for Nordic firmware design ++ (0x014 => nrffw: [ReadWrite; 13]), ++ (0x048 => _reserved2), ++ /// Reserved for Nordic hardware design ++ (0x050 => nrfhw: [ReadWrite; 12]), ++ /// Reserved for customer ++ (0x080 => customer: [ReadWrite; 32]), ++ (0x100 => _reserved3), ++ /// Mapping of the nRESET function (see POWER chapter for details) ++ (0x200 => pselreset0: ReadWrite), ++ /// Mapping of the nRESET function (see POWER chapter for details) ++ (0x204 => pselreset1: ReadWrite), ++ /// Access Port protection ++ (0x208 => approtect: ReadWrite), ++ /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO ++ /// - Address: 0x20c - 0x210 ++ (0x20c => nfcpins: ReadWrite), ++ (0x210 => debugctrl: ReadWrite), ++ (0x214 => _reserved4), ++ /// External circuitry to be supplied from VDD pin. ++ (0x300 => extsupply: ReadWrite), ++ /// GPIO reference voltage ++ (0x304 => regout0: ReadWrite), ++ (0x308 => @END), ++ } + } + + register_bitfields! [u32, +@@ -58,6 +65,21 @@ register_bitfields! [u32, + DISABLED = 0xff + ] + ], ++ /// Processor debug control ++ DebugControl [ ++ CPUNIDEN OFFSET(0) NUMBITS(8) [ ++ /// Enable ++ ENABLED = 0xff, ++ /// Disable ++ DISABLED = 0x00 ++ ], ++ CPUFPBEN OFFSET(8) NUMBITS(8) [ ++ /// Enable ++ ENABLED = 0xff, ++ /// Disable ++ DISABLED = 0x00 ++ ] ++ ], + /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO + NfcPins [ + /// Setting pins dedicated to NFC functionality + diff --git a/patches/tock/07-firmware-protect.patch b/patches/tock/07-firmware-protect.patch new file mode 100644 index 00000000..365b20a4 --- /dev/null +++ b/patches/tock/07-firmware-protect.patch @@ -0,0 +1,426 @@ +diff --git a/boards/components/src/firmware_protection.rs b/boards/components/src/firmware_protection.rs +new file mode 100644 +index 0000000..58695af +--- /dev/null ++++ b/boards/components/src/firmware_protection.rs +@@ -0,0 +1,70 @@ ++//! Component for firmware protection syscall interface. ++//! ++//! This provides one Component, `FirmwareProtectionComponent`, which implements a ++//! userspace syscall interface to enable the code readout protection. ++//! ++//! Usage ++//! ----- ++//! ```rust ++//! let crp = components::firmware_protection::FirmwareProtectionComponent::new( ++//! board_kernel, ++//! nrf52840::uicr::Uicr::new() ++//! ) ++//! .finalize( ++//! components::firmware_protection_component_helper!(uicr)); ++//! ``` ++ ++use core::mem::MaybeUninit; ++ ++use capsules::firmware_protection; ++use kernel::capabilities; ++use kernel::component::Component; ++use kernel::create_capability; ++use kernel::hil; ++use kernel::static_init_half; ++ ++// Setup static space for the objects. ++#[macro_export] ++macro_rules! firmware_protection_component_helper { ++ ($C:ty) => {{ ++ use capsules::firmware_protection; ++ use core::mem::MaybeUninit; ++ static mut BUF: MaybeUninit> = ++ MaybeUninit::uninit(); ++ &mut BUF ++ };}; ++} ++ ++pub struct FirmwareProtectionComponent { ++ board_kernel: &'static kernel::Kernel, ++ crp: C, ++} ++ ++impl FirmwareProtectionComponent { ++ pub fn new(board_kernel: &'static kernel::Kernel, crp: C) -> FirmwareProtectionComponent { ++ FirmwareProtectionComponent { ++ board_kernel: board_kernel, ++ crp: crp, ++ } ++ } ++} ++ ++impl Component ++ for FirmwareProtectionComponent ++{ ++ type StaticInput = &'static mut MaybeUninit>; ++ type Output = &'static firmware_protection::FirmwareProtection; ++ ++ unsafe fn finalize(self, static_buffer: Self::StaticInput) -> Self::Output { ++ let grant_cap = create_capability!(capabilities::MemoryAllocationCapability); ++ ++ static_init_half!( ++ static_buffer, ++ firmware_protection::FirmwareProtection, ++ firmware_protection::FirmwareProtection::new( ++ self.crp, ++ self.board_kernel.create_grant(&grant_cap), ++ ) ++ ) ++ } ++} +diff --git a/boards/components/src/lib.rs b/boards/components/src/lib.rs +index 917497a..520408f 100644 +--- a/boards/components/src/lib.rs ++++ b/boards/components/src/lib.rs +@@ -9,6 +9,7 @@ pub mod console; + pub mod crc; + pub mod debug_queue; + pub mod debug_writer; ++pub mod firmware_protection; + pub mod ft6x06; + pub mod gpio; + pub mod hd44780; +diff --git a/boards/nordic/nrf52840_dongle/src/main.rs b/boards/nordic/nrf52840_dongle/src/main.rs +index 118ea6d..76436f3 100644 +--- a/boards/nordic/nrf52840_dongle/src/main.rs ++++ b/boards/nordic/nrf52840_dongle/src/main.rs +@@ -112,6 +112,7 @@ pub struct Platform { + 'static, + nrf52840::usbd::Usbd<'static>, + >, ++ crp: &'static capsules::firmware_protection::FirmwareProtection, + } + + impl kernel::Platform for Platform { +@@ -132,6 +133,7 @@ impl kernel::Platform for Platform { + capsules::analog_comparator::DRIVER_NUM => f(Some(self.analog_comparator)), + nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)), + capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)), ++ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)), + kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)), + _ => f(None), + } +@@ -355,6 +357,14 @@ pub unsafe fn reset_handler() { + ) + .finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd)); + ++ let crp = components::firmware_protection::FirmwareProtectionComponent::new( ++ board_kernel, ++ nrf52840::uicr::Uicr::new(), ++ ) ++ .finalize(components::firmware_protection_component_helper!( ++ nrf52840::uicr::Uicr ++ )); ++ + nrf52_components::NrfClockComponent::new().finalize(()); + + let platform = Platform { +@@ -371,6 +381,7 @@ pub unsafe fn reset_handler() { + analog_comparator, + nvmc, + usb, ++ crp, + ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability), + }; + +diff --git a/boards/nordic/nrf52840dk/src/main.rs b/boards/nordic/nrf52840dk/src/main.rs +index b1d0d3c..3cfb38d 100644 +--- a/boards/nordic/nrf52840dk/src/main.rs ++++ b/boards/nordic/nrf52840dk/src/main.rs +@@ -180,6 +180,7 @@ pub struct Platform { + 'static, + nrf52840::usbd::Usbd<'static>, + >, ++ crp: &'static capsules::firmware_protection::FirmwareProtection, + } + + impl kernel::Platform for Platform { +@@ -201,6 +202,7 @@ impl kernel::Platform for Platform { + capsules::nonvolatile_storage_driver::DRIVER_NUM => f(Some(self.nonvolatile_storage)), + nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)), + capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)), ++ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)), + kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)), + _ => f(None), + } +@@ -480,6 +482,14 @@ pub unsafe fn reset_handler() { + ) + .finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd)); + ++ let crp = components::firmware_protection::FirmwareProtectionComponent::new( ++ board_kernel, ++ nrf52840::uicr::Uicr::new(), ++ ) ++ .finalize(components::firmware_protection_component_helper!( ++ nrf52840::uicr::Uicr ++ )); ++ + nrf52_components::NrfClockComponent::new().finalize(()); + + let platform = Platform { +@@ -497,6 +507,7 @@ pub unsafe fn reset_handler() { + nonvolatile_storage, + nvmc, + usb, ++ crp, + ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability), + }; + +diff --git a/capsules/src/driver.rs b/capsules/src/driver.rs +index ae458b3..f536dad 100644 +--- a/capsules/src/driver.rs ++++ b/capsules/src/driver.rs +@@ -16,6 +16,7 @@ pub enum NUM { + Adc = 0x00005, + Dac = 0x00006, + AnalogComparator = 0x00007, ++ FirmwareProtection = 0x00008, + + // Kernel + Ipc = 0x10000, +diff --git a/capsules/src/firmware_protection.rs b/capsules/src/firmware_protection.rs +new file mode 100644 +index 0000000..8cf63d6 +--- /dev/null ++++ b/capsules/src/firmware_protection.rs +@@ -0,0 +1,85 @@ ++//! Provides userspace control of firmware protection on a board. ++//! ++//! This allows an application to enable firware readout protection, ++//! disabling JTAG interface and other ways to read/tamper the firmware. ++//! Of course, outside of a hardware bug, once set, the only way to enable ++//! programming/debugging is by fully erasing the flash. ++//! ++//! Usage ++//! ----- ++//! ++//! ```rust ++//! # use kernel::static_init; ++//! ++//! let crp = static_init!( ++//! capsules::firmware_protection::FirmwareProtection, ++//! capsules::firmware_protection::FirmwareProtection::new( ++//! nrf52840::uicr::Uicr, ++//! board_kernel.create_grant(&grant_cap), ++//! ); ++//! ``` ++//! ++//! Syscall Interface ++//! ----------------- ++//! ++//! - Stability: 0 - Draft ++//! ++//! ### Command ++//! ++//! Enable code readout protection on the current board. ++//! ++//! #### `command_num` ++//! ++//! - `0`: Driver check. ++//! - `1`: Get current firmware readout protection (aka CRP) state. ++//! - `2`: Set current firmware readout protection (aka CRP) state. ++//! ++ ++use kernel::hil; ++use kernel::{AppId, Callback, Driver, Grant, ReturnCode}; ++ ++/// Syscall driver number. ++use crate::driver; ++pub const DRIVER_NUM: usize = driver::NUM::FirmwareProtection as usize; ++ ++pub struct FirmwareProtection { ++ crp_unit: C, ++ apps: Grant>, ++} ++ ++impl FirmwareProtection { ++ pub fn new(crp_unit: C, apps: Grant>) -> Self { ++ Self { crp_unit, apps } ++ } ++} ++ ++impl Driver for FirmwareProtection { ++ /// ++ /// ### Command numbers ++ /// ++ /// * `0`: Returns non-zero to indicate the driver is present. ++ /// * `1`: Gets firmware protection state. ++ /// * `2`: Sets firmware protection state. ++ fn command(&self, command_num: usize, data: usize, _: usize, appid: AppId) -> ReturnCode { ++ match command_num { ++ // return if driver is available ++ 0 => ReturnCode::SUCCESS, ++ ++ 1 => self ++ .apps ++ .enter(appid, |_, _| ReturnCode::SuccessWithValue { ++ value: self.crp_unit.get_protection() as usize, ++ }) ++ .unwrap_or_else(|err| err.into()), ++ ++ // sets firmware protection ++ 2 => self ++ .apps ++ .enter(appid, |_, _| self.crp_unit.set_protection(data.into())) ++ .unwrap_or_else(|err| err.into()), ++ ++ // default ++ _ => ReturnCode::ENOSUPPORT, ++ } ++ } ++} +diff --git a/capsules/src/lib.rs b/capsules/src/lib.rs +index e4423fe..7538aad 100644 +--- a/capsules/src/lib.rs ++++ b/capsules/src/lib.rs +@@ -22,6 +22,7 @@ pub mod crc; + pub mod dac; + pub mod debug_process_restart; + pub mod driver; ++pub mod firmware_protection; + pub mod fm25cl; + pub mod ft6x06; + pub mod fxos8700cq; +diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs +index 3bb8b5a..ea96cb2 100644 +--- a/chips/nrf52/src/uicr.rs ++++ b/chips/nrf52/src/uicr.rs +@@ -1,13 +1,14 @@ + //! User information configuration registers + +- + use enum_primitive::cast::FromPrimitive; ++use hil::firmware_protection::ProtectionLevel; + use kernel::common::registers::{register_bitfields, register_structs, ReadWrite}; + use kernel::common::StaticRef; + use kernel::hil; + use kernel::ReturnCode; + + use crate::gpio::Pin; ++use crate::nvmc::NVMC; + + const UICR_BASE: StaticRef = + unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; +@@ -210,3 +211,49 @@ impl Uicr { + self.registers.approtect.write(ApProtect::PALL::ENABLED); + } + } ++ ++impl hil::firmware_protection::FirmwareProtection for Uicr { ++ fn get_protection(&self) -> ProtectionLevel { ++ let ap_protect_state = self.is_ap_protect_enabled(); ++ let cpu_debug_state = self ++ .registers ++ .debugctrl ++ .matches_all(DebugControl::CPUNIDEN::ENABLED + DebugControl::CPUFPBEN::ENABLED); ++ match (ap_protect_state, cpu_debug_state) { ++ (false, _) => ProtectionLevel::NoProtection, ++ (true, true) => ProtectionLevel::JtagDisabled, ++ (true, false) => ProtectionLevel::FullyLocked, ++ } ++ } ++ ++ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode { ++ let current_level = self.get_protection(); ++ if current_level > level || level == ProtectionLevel::Unknown { ++ return ReturnCode::EINVAL; ++ } ++ if current_level == level { ++ return ReturnCode::EALREADY; ++ } ++ ++ unsafe { NVMC.configure_writeable() }; ++ if level >= ProtectionLevel::JtagDisabled { ++ self.set_ap_protect(); ++ } ++ ++ if level >= ProtectionLevel::FullyLocked { ++ // Prevent CPU debug and flash patching. Leaving these enabled could ++ // allow to circumvent protection. ++ self.registers ++ .debugctrl ++ .write(DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED); ++ // TODO(jmichel): prevent returning into bootloader if present ++ } ++ unsafe { NVMC.configure_readonly() }; ++ ++ if self.get_protection() == level { ++ ReturnCode::SUCCESS ++ } else { ++ ReturnCode::FAIL ++ } ++ } ++} +diff --git a/kernel/src/hil/firmware_protection.rs b/kernel/src/hil/firmware_protection.rs +new file mode 100644 +index 0000000..de08246 +--- /dev/null ++++ b/kernel/src/hil/firmware_protection.rs +@@ -0,0 +1,48 @@ ++//! Interface for Firmware Protection, also called Code Readout Protection. ++ ++use crate::returncode::ReturnCode; ++ ++#[derive(PartialOrd, PartialEq)] ++pub enum ProtectionLevel { ++ /// Unsupported feature ++ Unknown = 0, ++ /// This should be the factory default for the chip. ++ NoProtection = 1, ++ /// At this level, only JTAG/SWD are disabled but other debugging ++ /// features may still be enabled. ++ JtagDisabled = 2, ++ /// This is the maximum level of protection the chip supports. ++ /// At this level, JTAG and all other features are expected to be ++ /// disabled and only a full chip erase may allow to recover from ++ /// that state. ++ FullyLocked = 0xff, ++} ++ ++impl From for ProtectionLevel { ++ fn from(value: usize) -> Self { ++ match value { ++ 1 => ProtectionLevel::NoProtection, ++ 2 => ProtectionLevel::JtagDisabled, ++ 0xff => ProtectionLevel::FullyLocked, ++ _ => ProtectionLevel::Unknown, ++ } ++ } ++} ++ ++pub trait FirmwareProtection { ++ /// Gets the current firmware protection level. ++ /// This doesn't fail and always returns a value. ++ fn get_protection(&self) -> ProtectionLevel; ++ ++ /// Sets the firmware protection level. ++ /// There are four valid return values: ++ /// - SUCCESS: protection level has been set to `level` ++ /// - FAIL: something went wrong while setting the protection ++ /// level and the effective protection level is not the one ++ /// that was requested. ++ /// - EALREADY: the requested protection level is already the ++ /// level that is set. ++ /// - EINVAL: unsupported protection level or the requested ++ /// protection level is lower than the currently set one. ++ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode; ++} +diff --git a/kernel/src/hil/mod.rs b/kernel/src/hil/mod.rs +index 4f42afa..83e7702 100644 +--- a/kernel/src/hil/mod.rs ++++ b/kernel/src/hil/mod.rs +@@ -8,6 +8,7 @@ pub mod dac; + pub mod digest; + pub mod eic; + pub mod entropy; ++pub mod firmware_protection; + pub mod flash; + pub mod gpio; + pub mod gpio_async; + diff --git a/setup.sh b/setup.sh index 903e0e3d..13ad9b04 100755 --- a/setup.sh +++ b/setup.sh @@ -44,3 +44,6 @@ rustup target add thumbv7em-none-eabi # Install dependency to create applications. mkdir -p elf2tab cargo install elf2tab --version 0.6.0 --root elf2tab/ + +# Install python dependencies to factory configure OpenSK (crypto, JTAG lockdown) +pip3 install --user --upgrade colorama tqdm cryptography fido2 diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 1b4b96a3..5dbd9308 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -13,14 +13,17 @@ // limitations under the License. use super::data_formats::{ - extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned, - ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, + extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, + GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; +use super::key_material; use super::status_code::Ctap2StatusCode; use alloc::string::String; use alloc::vec::Vec; +use arrayref::array_ref; use cbor::destructure_cbor_map; use core::convert::TryFrom; @@ -41,6 +44,8 @@ pub enum Command { #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) + // Vendor specific commands + AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } impl From for Ctap2StatusCode { @@ -63,7 +68,8 @@ impl Command { const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_SELECTION: u8 = 0xB0; const AUTHENTICATOR_CONFIG: u8 = 0xC0; - const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; + const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40; + const AUTHENTICATOR_VENDOR_FIRST_UNUSED: u8 = 0x41; const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; pub fn deserialize(bytes: &[u8]) -> Result { @@ -109,6 +115,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_VENDOR_CONFIGURE => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters::try_from(decoded_cbor)?, + )) + } _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND), } } @@ -372,6 +384,62 @@ impl TryFrom for AuthenticatorClientPinParameters { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorAttestationMaterial { + pub certificate: Vec, + pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], +} + +impl TryFrom for AuthenticatorAttestationMaterial { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => certificate, + 2 => private_key, + } = extract_map(cbor_value)?; + } + let certificate = extract_byte_string(ok_or_missing(certificate)?)?; + let private_key = extract_byte_string(ok_or_missing(private_key)?)?; + if private_key.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let private_key = array_ref!(private_key, 0, key_material::ATTESTATION_PRIVATE_KEY_LENGTH); + Ok(AuthenticatorAttestationMaterial { + certificate, + private_key: *private_key, + }) + } +} + +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorVendorConfigureParameters { + pub lockdown: bool, + pub attestation_material: Option, +} + +impl TryFrom for AuthenticatorVendorConfigureParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => lockdown, + 2 => attestation_material, + } = extract_map(cbor_value)?; + } + let lockdown = lockdown.map_or(Ok(false), extract_bool)?; + let attestation_material = attestation_material + .map(AuthenticatorAttestationMaterial::try_from) + .transpose()?; + Ok(AuthenticatorVendorConfigureParameters { + lockdown, + attestation_material, + }) + } +} + #[cfg(test)] mod test { use super::super::data_formats::{ @@ -570,4 +638,83 @@ mod test { let command = Command::deserialize(&cbor_bytes); assert_eq!(command, Ok(Command::AuthenticatorSelection)); } + + #[test] + fn test_vendor_configure() { + // Incomplete command + let mut cbor_bytes = vec![Command::AUTHENTICATOR_VENDOR_CONFIGURE]; + let command = Command::deserialize(&cbor_bytes); + assert_eq!(command, Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)); + + cbor_bytes.extend(&[0xA1, 0x01, 0xF5]); + let command = Command::deserialize(&cbor_bytes); + assert_eq!( + command, + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None + } + )) + ); + + let dummy_cert = [0xddu8; 20]; + let dummy_pkey = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + + // Attestation key is too short. + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 1 => dummy_cert, + 2 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // Missing private key + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 1 => dummy_cert + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + ); + + // Missing certificate + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 2 => dummy_pkey + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + ); + + // Valid + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 1 => dummy_cert, + 2 => dummy_pkey + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Ok(AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_pkey + }) + }) + ); + } } diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 1ae83346..ef96eefa 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -117,9 +117,8 @@ impl CtapHid { // CTAP specification (version 20190130) section 8.1.9.1.3 const PROTOCOL_VERSION: u8 = 2; - // The device version number is vendor-defined. For now we define them to be zero. - // TODO: Update with device version? - const DEVICE_VERSION_MAJOR: u8 = 0; + // The device version number is vendor-defined. + const DEVICE_VERSION_MAJOR: u8 = 1; const DEVICE_VERSION_MINOR: u8 = 0; const DEVICE_VERSION_BUILD: u8 = 0; @@ -574,7 +573,7 @@ mod test { 0x00, 0x01, 0x02, // Protocol version - 0x00, // Device version + 0x01, // Device version 0x00, 0x00, CtapHid::CAPABILITIES @@ -634,7 +633,7 @@ mod test { cid[2], cid[3], 0x02, // Protocol version - 0x00, // Device version + 0x01, // Device version 0x00, 0x00, CtapHid::CAPABILITIES diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index 2563798e..eec54569 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -17,9 +17,3 @@ pub const AAGUID_LENGTH: usize = 16; pub const AAGUID: &[u8; AAGUID_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); - -pub const ATTESTATION_CERTIFICATE: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin")); - -pub const ATTESTATION_PRIVATE_KEY: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH] = - include_bytes!(concat!(env!("OUT_DIR"), "/opensk_pkey.bin")); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4c5687a2..dfa2d9bd 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -29,7 +29,7 @@ mod timed_permission; use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, - AuthenticatorMakeCredentialParameters, Command, + AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, }; #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; @@ -44,7 +44,7 @@ use self::pin_protocol_v1::PinPermission; use self::pin_protocol_v1::PinProtocolV1; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, - AuthenticatorMakeCredentialResponse, ResponseData, + AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; @@ -67,6 +67,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock_drivers::console::Console; +use libtock_drivers::crp; use libtock_drivers::timer::{ClockValue, Duration}; // This flag enables or disables basic attestation for FIDO2. U2F is unaffected by @@ -358,6 +359,10 @@ where #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands + // Vendor specific commands + Command::AuthenticatorVendorConfigure(params) => { + self.process_vendor_configure(params, cid) + } }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -919,6 +924,74 @@ where Ok(ResponseData::AuthenticatorSelection) } + fn process_vendor_configure( + &mut self, + params: AuthenticatorVendorConfigureParameters, + cid: ChannelID, + ) -> Result { + (self.check_user_presence)(cid)?; + + // Sanity checks + let current_priv_key = self.persistent_store.attestation_private_key()?; + let current_cert = self.persistent_store.attestation_certificate()?; + + let response = match params.attestation_material { + // Only reading values. + None => AuthenticatorVendorResponse { + cert_programmed: current_cert.is_some(), + pkey_programmed: current_priv_key.is_some(), + }, + // Device is already fully programmed. We don't leak information. + Some(_) if current_cert.is_some() && current_priv_key.is_some() => { + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + // Device is partially or not programmed. We complete the process. + Some(data) => { + if let Some(current_cert) = ¤t_cert { + if current_cert != &data.certificate { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + } + if let Some(current_priv_key) = ¤t_priv_key { + if current_priv_key != &data.private_key { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + } + if current_cert.is_none() { + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + } + if current_priv_key.is_none() { + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + } + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + }; + if params.lockdown { + // To avoid bricking the authenticator, we only allow lockdown + // to happen if both values are programmed or if both U2F/CTAP1 and + // batch attestation are disabled. + #[cfg(feature = "with_ctap1")] + let need_certificate = true; + #[cfg(not(feature = "with_ctap1"))] + let need_certificate = USE_BATCH_ATTESTATION; + + if (need_certificate && !(response.pkey_programmed && response.cert_programmed)) + || crp::set_protection(crp::ProtectionLevel::FullyLocked).is_err() + { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + Ok(ResponseData::AuthenticatorVendor(response)) + } + pub fn generate_auth_data( &self, rp_id_hash: &[u8], @@ -941,6 +1014,7 @@ where #[cfg(test)] mod test { + use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, @@ -2052,4 +2126,124 @@ mod test { last_counter = next_counter; } } + + #[test] + fn test_vendor_configure() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // Nothing should be configured at the beginning + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: false, + } + )) + ); + + // Inject dummy values + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Try to inject other dummy values and check that initial values are retained. + let other_dummy_key = [0x44u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: other_dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Now try to lock the device + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + )) + ); + } } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 47e1d548..64229596 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -34,6 +34,7 @@ pub enum ResponseData { AuthenticatorReset, #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, + AuthenticatorVendor(AuthenticatorVendorResponse), } impl From for Option { @@ -48,6 +49,7 @@ impl From for Option { ResponseData::AuthenticatorReset => None, #[cfg(feature = "with_ctap2_1")] ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorVendor(data) => Some(data.into()), } } } @@ -231,6 +233,27 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorVendorResponse { + pub cert_programmed: bool, + pub pkey_programmed: bool, +} + +impl From for cbor::Value { + fn from(vendor_response: AuthenticatorVendorResponse) -> Self { + let AuthenticatorVendorResponse { + cert_programmed, + pkey_programmed, + } = vendor_response; + + cbor_map_options! { + 1 => cert_programmed, + 2 => pkey_programmed, + } + } +} + #[cfg(test)] mod test { use super::super::data_formats::PackedAttestationStatement; @@ -401,4 +424,34 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); assert_eq!(response_cbor, None); } + + #[test] + fn test_vendor_response_into_cbor() { + let response_cbor: Option = + ResponseData::AuthenticatorVendor(AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: false, + }) + .into(); + assert_eq!( + response_cbor, + Some(cbor_map_options! { + 1 => true, + 2 => false, + }) + ); + let response_cbor: Option = + ResponseData::AuthenticatorVendor(AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: true, + }) + .into(); + assert_eq!( + response_cbor, + Some(cbor_map_options! { + 1 => false, + 2 => true, + }) + ); + } } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5f170523..a7013251 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -115,36 +115,12 @@ impl PersistentStore { self.store.insert(key::CRED_RANDOM_SECRET, &cred_random)?; } - // TODO(jmichel): remove this when vendor command is in place - #[cfg(not(test))] - self.load_attestation_data_from_firmware()?; if self.store.find_handle(key::AAGUID)?.is_none() { self.set_aaguid(key_material::AAGUID)?; } Ok(()) } - // TODO(jmichel): remove this function when vendor command is in place. - #[cfg(not(test))] - fn load_attestation_data_from_firmware(&mut self) -> Result<(), Ctap2StatusCode> { - // The following 2 entries are meant to be written by vendor-specific commands. - if self - .store - .find_handle(key::ATTESTATION_PRIVATE_KEY)? - .is_none() - { - self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY)?; - } - if self - .store - .find_handle(key::ATTESTATION_CERTIFICATE)? - .is_none() - { - self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE)?; - } - Ok(()) - } - /// Returns the first matching credential. /// /// Returns `None` if no credentials are matched or if `check_cred_protect` is set and the first @@ -988,12 +964,14 @@ mod test { .unwrap() .is_none()); - // Make sure the persistent keys are initialized. + // Make sure the persistent keys are initialized to dummy values. + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; persistent_store - .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .set_attestation_private_key(&dummy_key) .unwrap(); persistent_store - .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .set_attestation_certificate(&dummy_cert) .unwrap(); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); @@ -1001,11 +979,11 @@ mod test { persistent_store.reset(&mut rng).unwrap(); assert_eq!( &persistent_store.attestation_private_key().unwrap().unwrap(), - key_material::ATTESTATION_PRIVATE_KEY + &dummy_key ); assert_eq!( persistent_store.attestation_certificate().unwrap().unwrap(), - key_material::ATTESTATION_CERTIFICATE + &dummy_cert ); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); } diff --git a/third_party/libtock-drivers/src/crp.rs b/third_party/libtock-drivers/src/crp.rs new file mode 100644 index 00000000..3b686caf --- /dev/null +++ b/third_party/libtock-drivers/src/crp.rs @@ -0,0 +1,52 @@ +use crate::result::TockResult; +use libtock_core::syscalls; + +const DRIVER_NUMBER: usize = 0x00008; + +mod command_nr { + pub const AVAILABLE: usize = 0; + pub const GET_PROTECTION: usize = 1; + pub const SET_PROTECTION: usize = 2; +} + +#[derive(PartialOrd, PartialEq)] +pub enum ProtectionLevel { + /// Unsupported feature + Unknown = 0, + /// This should be the factory default for the chip. + NoProtection = 1, + /// At this level, only JTAG/SWD are disabled but other debugging + /// features may still be enabled. + JtagDisabled = 2, + /// This is the maximum level of protection the chip supports. + /// At this level, JTAG and all other features are expected to be + /// disabled and only a full chip erase may allow to recover from + /// that state. + FullyLocked = 0xff, +} + +impl From for ProtectionLevel { + fn from(value: usize) -> Self { + match value { + 1 => ProtectionLevel::NoProtection, + 2 => ProtectionLevel::JtagDisabled, + 0xff => ProtectionLevel::FullyLocked, + _ => ProtectionLevel::Unknown, + } + } +} + +pub fn is_available() -> TockResult<()> { + syscalls::command(DRIVER_NUMBER, command_nr::AVAILABLE, 0, 0)?; + Ok(()) +} + +pub fn get_protection() -> TockResult { + let current_level = syscalls::command(DRIVER_NUMBER, command_nr::GET_PROTECTION, 0, 0)?; + Ok(current_level.into()) +} + +pub fn set_protection(level: ProtectionLevel) -> TockResult<()> { + syscalls::command(DRIVER_NUMBER, command_nr::SET_PROTECTION, level as usize, 0)?; + Ok(()) +} diff --git a/third_party/libtock-drivers/src/lib.rs b/third_party/libtock-drivers/src/lib.rs index 8b8983c7..f0149966 100644 --- a/third_party/libtock-drivers/src/lib.rs +++ b/third_party/libtock-drivers/src/lib.rs @@ -2,6 +2,7 @@ pub mod buttons; pub mod console; +pub mod crp; pub mod led; #[cfg(feature = "with_nfc")] pub mod nfc; diff --git a/tools/configure.py b/tools/configure.py new file mode 100755 index 00000000..cc00a1ab --- /dev/null +++ b/tools/configure.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Lint as: python3 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import getpass +import datetime +import sys +import uuid + +import colorama +from tqdm.auto import tqdm + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +from fido2 import ctap +from fido2 import ctap2 +from fido2 import hid + +OPENSK_VID_PID = (0x1915, 0x521F) +OPENSK_VENDOR_CONFIGURE = 0x40 + + +def fatal(msg): + tqdm.write("{style_begin}fatal:{style_end} {message}".format( + style_begin=colorama.Fore.RED + colorama.Style.BRIGHT, + style_end=colorama.Style.RESET_ALL, + message=msg)) + sys.exit(1) + + +def error(msg): + tqdm.write("{style_begin}error:{style_end} {message}".format( + style_begin=colorama.Fore.RED, + style_end=colorama.Style.RESET_ALL, + message=msg)) + + +def info(msg): + tqdm.write("{style_begin}info:{style_end} {message}".format( + style_begin=colorama.Fore.GREEN + colorama.Style.BRIGHT, + style_end=colorama.Style.RESET_ALL, + message=msg)) + + +def get_opensk_devices(batch_mode): + devices = [] + for dev in hid.CtapHidDevice.list_devices(): + if (dev.descriptor["vendor_id"], + dev.descriptor["product_id"]) == OPENSK_VID_PID: + if dev.capabilities & hid.CAPABILITY.CBOR: + if batch_mode: + devices.append(ctap2.CTAP2(dev)) + else: + return [ctap2.CTAP2(dev)] + return devices + + +def get_private_key(data, password=None): + # First we try without password. + try: + return serialization.load_pem_private_key(data, password=None) + except TypeError: + # Maybe we need a password then. + if sys.stdin.isatty(): + password = getpass.getpass(prompt="Private key password: ") + else: + password = sys.stdin.readline().rstrip() + return get_private_key(data, password=password.encode(sys.stdin.encoding)) + + +def main(args): + colorama.init() + # We need either both the certificate and the key or none + if bool(args.priv_key) ^ bool(args.certificate): + fatal("Certificate and private key must be set together or both omitted.") + + cbor_data = {1: args.lock} + + if args.priv_key: + cbor_data[1] = args.lock + priv_key = get_private_key(args.priv_key.read()) + if not isinstance(priv_key, ec.EllipticCurvePrivateKey): + fatal("Private key must be an Elliptic Curve one.") + if not isinstance(priv_key.curve, ec.SECP256R1): + fatal("Private key must use Secp256r1 curve.") + if priv_key.key_size != 256: + fatal("Private key must be 256 bits long.") + info("Private key is valid.") + + cert = x509.load_pem_x509_certificate(args.certificate.read()) + # Some sanity/validity checks + now = datetime.datetime.now() + if cert.not_valid_before > now: + fatal("Certificate validity starts in the future.") + if cert.not_valid_after <= now: + fatal("Certificate expired.") + pub_key = cert.public_key() + if not isinstance(pub_key, ec.EllipticCurvePublicKey): + fatal("Certificate public key must be an Elliptic Curve one.") + if not isinstance(pub_key.curve, ec.SECP256R1): + fatal("Certificate public key must use Secp256r1 curve.") + if pub_key.key_size != 256: + fatal("Certificate public key must be 256 bits long.") + if pub_key.public_numbers() != priv_key.public_key().public_numbers(): + fatal("Certificate public doesn't match with the private key.") + info("Certificate is valid.") + + cbor_data[2] = { + 1: + cert.public_bytes(serialization.Encoding.DER), + 2: + priv_key.private_numbers().private_value.to_bytes( + length=32, byteorder='big', signed=False) + } + + for authenticator in tqdm(get_opensk_devices(args.batch)): + # If the device supports it, wink to show which device + # we're going to program. + if authenticator.device.capabilities & hid.CAPABILITY.WINK: + authenticator.device.wink() + aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid) + info(("Programming device {} AAGUID {} ({}). " + "Please touch the device to confirm...").format( + authenticator.device.descriptor.get("product_string", "Unknown"), + aaguid, authenticator.device)) + try: + result = authenticator.send_cbor( + OPENSK_VENDOR_CONFIGURE, + data=cbor_data, + ) + info("Certificate: {}".format("Present" if result[1] else "Missing")) + info("Private Key: {}".format("Present" if result[2] else "Missing")) + if args.lock: + info("Device is now locked down!") + except ctap.CtapError as ex: + if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: + error("Failed to configure OpenSK (unsupported command).") + elif ex.code.value == 0xF2: # VENDOR_INTERNAL_ERROR + error(("Failed to configure OpenSK (lockdown conditions not met " + "or hardware error).")) + elif ex.code.value == ctap.CtapError.ERR.INVALID_PARAMETER: + error( + ("Failed to configure OpenSK (device is partially programmed but " + "the given cert/key don't match the ones currently programmed).")) + else: + error("Failed to configure OpenSK (unknown error: {}".format(ex)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--batch", + default=False, + action="store_true", + help=( + "When batch processing is used, all plugged OpenSK devices will " + "be programmed the same way. Otherwise (default) only the first seen " + "device will be programmed."), + ) + parser.add_argument( + "--certificate", + type=argparse.FileType("rb"), + default=None, + metavar="PEM_FILE", + dest="certificate", + help=("PEM file containing the certificate to inject into " + "the OpenSK authenticator."), + ) + parser.add_argument( + "--private-key", + type=argparse.FileType("rb"), + default=None, + metavar="PEM_FILE", + dest="priv_key", + help=("PEM file containing the private key associated " + "with the certificate."), + ) + parser.add_argument( + "--lock-device", + default=False, + action="store_true", + dest="lock", + help=("Locks the device (i.e. bootloader and JTAG access). " + "This command can fail if the certificate or the private key " + "haven't been both programmed yet."), + ) + main(parser.parse_args())