Merge pull request #239 from jmichelp/transparency

Add vendor commands to inject crypto materials
This commit is contained in:
Jean-Michel Picod
2020-12-16 22:06:15 +01:00
committed by GitHub
20 changed files with 1281 additions and 130 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ Cargo.lock
/reproducible/binaries.sha256sum
/reproducible/elf2tab.txt
/reproducible/reproduced.tar
__pycache__

View File

@@ -1,6 +1,6 @@
[package]
name = "ctap2"
version = "0.1.0"
version = "1.0.0"
authors = [
"Fabian Kaczmarczyck <kaczmarczyck@google.com>",
"Guillaume Endignoux <guillaumee@google.com>",
@@ -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]

View File

@@ -1,6 +1,5 @@
# <img alt="OpenSK logo" src="docs/img/OpenSK.svg" width="200px">
[![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
* 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).
* 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,9 +187,10 @@ 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
* `--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
* `--fps` (optional) to customize the number of frames per second in the movie
animation.
```shell

View File

@@ -48,7 +48,7 @@ static STRINGS: &'static [&'static str] = &[
// Product
"OpenSK",
// Serial number
"v0.1",
"v1.0",
];
// State for loading and holding applications.

View File

@@ -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();

View File

@@ -710,6 +710,22 @@ class OpenSKInstaller:
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 @@ if __name__ == "__main__":
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",

View File

@@ -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

View File

@@ -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.

View File

@@ -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<UicrRegisters> =
- 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<u32, Pselreset::Register>,
- /// Mapping of the nRESET function (see POWER chapter for details)
- /// - Address: 0x204 - 0x208
- pselreset1: ReadWrite<u32, Pselreset::Register>,
- /// Access Port protection
- /// - Address: 0x208 - 0x20c
- approtect: ReadWrite<u32, ApProtect::Register>,
- /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
- /// - Address: 0x20c - 0x210
- nfcpins: ReadWrite<u32, NfcPins::Register>,
- _reserved1: [u32; 60],
- /// External circuitry to be supplied from VDD pin.
- /// - Address: 0x300 - 0x304
- extsupply: ReadWrite<u32, ExtSupply::Register>,
- /// GPIO reference voltage
- /// - Address: 0x304 - 0x308
- regout0: ReadWrite<u32, RegOut::Register>,
+ unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) };
+
+register_structs! {
+ UicrRegisters {
+ (0x000 => _reserved1),
+ /// Reserved for Nordic firmware design
+ (0x014 => nrffw: [ReadWrite<u32>; 13]),
+ (0x048 => _reserved2),
+ /// Reserved for Nordic hardware design
+ (0x050 => nrfhw: [ReadWrite<u32>; 12]),
+ /// Reserved for customer
+ (0x080 => customer: [ReadWrite<u32>; 32]),
+ (0x100 => _reserved3),
+ /// Mapping of the nRESET function (see POWER chapter for details)
+ (0x200 => pselreset0: ReadWrite<u32, Pselreset::Register>),
+ /// Mapping of the nRESET function (see POWER chapter for details)
+ (0x204 => pselreset1: ReadWrite<u32, Pselreset::Register>),
+ /// Access Port protection
+ (0x208 => approtect: ReadWrite<u32, ApProtect::Register>),
+ /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
+ /// - Address: 0x20c - 0x210
+ (0x20c => nfcpins: ReadWrite<u32, NfcPins::Register>),
+ (0x210 => debugctrl: ReadWrite<u32, DebugControl::Register>),
+ (0x214 => _reserved4),
+ /// External circuitry to be supplied from VDD pin.
+ (0x300 => extsupply: ReadWrite<u32, ExtSupply::Register>),
+ /// GPIO reference voltage
+ (0x304 => regout0: ReadWrite<u32, RegOut::Register>),
+ (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

View File

@@ -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<firmware_protection::FirmwareProtection<$C>> =
+ MaybeUninit::uninit();
+ &mut BUF
+ };};
+}
+
+pub struct FirmwareProtectionComponent<C: hil::firmware_protection::FirmwareProtection> {
+ board_kernel: &'static kernel::Kernel,
+ crp: C,
+}
+
+impl<C: 'static + hil::firmware_protection::FirmwareProtection> FirmwareProtectionComponent<C> {
+ pub fn new(board_kernel: &'static kernel::Kernel, crp: C) -> FirmwareProtectionComponent<C> {
+ FirmwareProtectionComponent {
+ board_kernel: board_kernel,
+ crp: crp,
+ }
+ }
+}
+
+impl<C: 'static + hil::firmware_protection::FirmwareProtection> Component
+ for FirmwareProtectionComponent<C>
+{
+ type StaticInput = &'static mut MaybeUninit<firmware_protection::FirmwareProtection<C>>;
+ type Output = &'static firmware_protection::FirmwareProtection<C>;
+
+ 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<C>,
+ 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<nrf52840::uicr::Uicr>,
}
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<nrf52840::uicr::Uicr>,
}
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<C: hil::firmware_protection::FirmwareProtection> {
+ crp_unit: C,
+ apps: Grant<Option<Callback>>,
+}
+
+impl<C: hil::firmware_protection::FirmwareProtection> FirmwareProtection<C> {
+ pub fn new(crp_unit: C, apps: Grant<Option<Callback>>) -> Self {
+ Self { crp_unit, apps }
+ }
+}
+
+impl<C: hil::firmware_protection::FirmwareProtection> Driver for FirmwareProtection<C> {
+ ///
+ /// ### 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<UicrRegisters> =
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<usize> 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;

View File

@@ -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

View File

@@ -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<cbor::reader::DecoderError> 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<Command, Ctap2StatusCode> {
@@ -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<cbor::Value> for AuthenticatorClientPinParameters {
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorAttestationMaterial {
pub certificate: Vec<u8>,
pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH],
}
impl TryFrom<cbor::Value> for AuthenticatorAttestationMaterial {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
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<AuthenticatorAttestationMaterial>,
}
impl TryFrom<cbor::Value> for AuthenticatorVendorConfigureParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
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
})
})
);
}
}

View File

@@ -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

View File

@@ -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"));

View File

@@ -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<ResponseData, Ctap2StatusCode> {
(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) = &current_cert {
if current_cert != &data.certificate {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
}
if let Some(current_priv_key) = &current_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,
}
))
);
}
}

View File

@@ -34,6 +34,7 @@ pub enum ResponseData {
AuthenticatorReset,
#[cfg(feature = "with_ctap2_1")]
AuthenticatorSelection,
AuthenticatorVendor(AuthenticatorVendorResponse),
}
impl From<ResponseData> for Option<cbor::Value> {
@@ -48,6 +49,7 @@ impl From<ResponseData> for Option<cbor::Value> {
ResponseData::AuthenticatorReset => None,
#[cfg(feature = "with_ctap2_1")]
ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorVendor(data) => Some(data.into()),
}
}
}
@@ -231,6 +233,27 @@ impl From<AuthenticatorClientPinResponse> 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<AuthenticatorVendorResponse> 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<cbor::Value> = ResponseData::AuthenticatorSelection.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_vendor_response_into_cbor() {
let response_cbor: Option<cbor::Value> =
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<cbor::Value> =
ResponseData::AuthenticatorVendor(AuthenticatorVendorResponse {
cert_programmed: false,
pkey_programmed: true,
})
.into();
assert_eq!(
response_cbor,
Some(cbor_map_options! {
1 => false,
2 => true,
})
);
}
}

View File

@@ -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);
}

52
third_party/libtock-drivers/src/crp.rs vendored Normal file
View File

@@ -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<usize> 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<ProtectionLevel> {
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(())
}

View File

@@ -2,6 +2,7 @@
pub mod buttons;
pub mod console;
pub mod crp;
pub mod led;
#[cfg(feature = "with_nfc")]
pub mod nfc;

206
tools/configure.py Executable file
View File

@@ -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())