Merge pull request #239 from jmichelp/transparency
Add vendor commands to inject crypto materials
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ Cargo.lock
|
||||
/reproducible/binaries.sha256sum
|
||||
/reproducible/elf2tab.txt
|
||||
/reproducible/reproduced.tar
|
||||
__pycache__
|
||||
|
||||
@@ -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]
|
||||
|
||||
43
README.md
43
README.md
@@ -1,6 +1,5 @@
|
||||
# <img alt="OpenSK logo" src="docs/img/OpenSK.svg" width="200px">
|
||||
|
||||
[](https://travis-ci.org/google/OpenSK)
|
||||

|
||||

|
||||

|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,7 @@ static STRINGS: &'static [&'static str] = &[
|
||||
// Product
|
||||
"OpenSK",
|
||||
// Serial number
|
||||
"v0.1",
|
||||
"v1.0",
|
||||
];
|
||||
|
||||
// State for loading and holding applications.
|
||||
|
||||
59
build.rs
59
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();
|
||||
|
||||
43
deploy.py
43
deploy.py
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
100
patches/tock/06-update-uicr.patch
Normal file
100
patches/tock/06-update-uicr.patch
Normal 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
|
||||
|
||||
426
patches/tock/07-firmware-protect.patch
Normal file
426
patches/tock/07-firmware-protect.patch
Normal 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;
|
||||
|
||||
3
setup.sh
3
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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
198
src/ctap/mod.rs
198
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<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) = ¤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,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
52
third_party/libtock-drivers/src/crp.rs
vendored
Normal 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(())
|
||||
}
|
||||
1
third_party/libtock-drivers/src/lib.rs
vendored
1
third_party/libtock-drivers/src/lib.rs
vendored
@@ -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
206
tools/configure.py
Executable 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())
|
||||
Reference in New Issue
Block a user