Merge branch 'develop' into v2_optim

This commit is contained in:
Julien Cretin
2021-01-18 16:10:08 +01:00
36 changed files with 2849 additions and 1264 deletions

View File

@@ -42,12 +42,6 @@ jobs:
command: check command: check
args: --target thumbv7em-none-eabi --release --features with_ctap1 args: --target thumbv7em-none-eabi --release --features with_ctap1
- name: Check OpenSK with_ctap2_1
uses: actions-rs/cargo@v1
with:
command: check
args: --target thumbv7em-none-eabi --release --features with_ctap2_1
- name: Check OpenSK debug_ctap - name: Check OpenSK debug_ctap
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
@@ -78,17 +72,11 @@ jobs:
command: check command: check
args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1 args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1
- name: Check OpenSK debug_ctap,with_ctap2_1 - name: Check OpenSK debug_ctap,with_ctap1,panic_console,debug_allocations,verbose
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: check command: check
args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap2_1 args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,,panic_console,debug_allocations,verbose
- name: Check OpenSK debug_ctap,with_ctap1,with_ctap2_1,panic_console,debug_allocations,verbose
uses: actions-rs/cargo@v1
with:
command: check
args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,with_ctap2_1,panic_console,debug_allocations,verbose
- name: Check examples - name: Check examples
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1

View File

@@ -51,27 +51,3 @@ jobs:
command: test command: test
args: --features std,with_ctap1 args: --features std,with_ctap1
- name: Unit testing of CTAP2 (release mode + CTAP2.1)
uses: actions-rs/cargo@v1
with:
command: test
args: --release --features std,with_ctap2_1
- name: Unit testing of CTAP2 (debug mode + CTAP2.1)
uses: actions-rs/cargo@v1
with:
command: test
args: --features std,with_ctap2_1
- name: Unit testing of CTAP2 (release mode + CTAP1 + CTAP2.1)
uses: actions-rs/cargo@v1
with:
command: test
args: --release --features std,with_ctap1,with_ctap2_1
- name: Unit testing of CTAP2 (debug mode + CTAP1 + CTAP2.1)
uses: actions-rs/cargo@v1
with:
command: test
args: --features std,with_ctap1,with_ctap2_1

1
.gitignore vendored
View File

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

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ctap2" name = "ctap2"
version = "0.1.0" version = "1.0.0"
authors = [ authors = [
"Fabian Kaczmarczyck <kaczmarczyck@google.com>", "Fabian Kaczmarczyck <kaczmarczyck@google.com>",
"Guillaume Endignoux <guillaumee@google.com>", "Guillaume Endignoux <guillaumee@google.com>",
@@ -27,7 +27,6 @@ panic_console = ["lang_items/panic_console"]
std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"]
verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"]
with_ctap1 = ["crypto/with_ctap1"] with_ctap1 = ["crypto/with_ctap1"]
with_ctap2_1 = []
with_nfc = ["libtock_drivers/with_nfc"] with_nfc = ["libtock_drivers/with_nfc"]
[dev-dependencies] [dev-dependencies]
@@ -35,7 +34,6 @@ elf2tab = "0.6.0"
enum-iterator = "0.6.0" enum-iterator = "0.6.0"
[build-dependencies] [build-dependencies]
openssl = "0.10"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
[profile.dev] [profile.dev]

View File

@@ -1,6 +1,5 @@
# <img alt="OpenSK logo" src="docs/img/OpenSK.svg" width="200px"> # <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) ![markdownlint](https://github.com/google/OpenSK/workflows/markdownlint/badge.svg?branch=master)
![pylint](https://github.com/google/OpenSK/workflows/pylint/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) ![Cargo check](https://github.com/google/OpenSK/workflows/Cargo%20check/badge.svg?branch=master)
@@ -25,14 +24,14 @@ few limitations:
### FIDO2 ### FIDO2
Although we tested and implemented our firmware based on the published The stable branch implements the published
[CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html), [CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html),
our implementation was not reviewed nor officially tested and doesn't claim to but our implementation was not reviewed nor officially tested and doesn't claim
be FIDO Certified. to be FIDO Certified. It already contains some preview features of 2.1, that you
We started adding features of the upcoming next version of the can try by adding the flag `--ctap2.1` to the deploy command.
[CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). The develop branch offers only the
The development is currently between 2.0 and 2.1, with updates hidden behind a feature flag. [CTAP2.1 specifications](https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html).
Please add the flag `--ctap2.1` to the deploy command to include them. The new features of 2.1 are currently work in progress.
### Cryptography ### Cryptography
@@ -58,8 +57,8 @@ For a more detailed guide, please refer to our
./setup.sh ./setup.sh
``` ```
2. Next step is to install Tock OS as well as the OpenSK application on your 1. 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: board. Run:
```shell ```shell
# Nordic nRF52840-DK board # Nordic nRF52840-DK board
@@ -68,7 +67,17 @@ For a more detailed guide, please refer to our
./deploy.py --board=nrf52840_dongle --opensk ./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 with the key. For that purpose we provide a udev rule file that can be
installed with the following command: installed with the following command:
@@ -84,32 +93,36 @@ a few things you can personalize:
1. If you have multiple buttons, choose the buttons responsible for user 1. If you have multiple buttons, choose the buttons responsible for user
presence in `main.rs`. presence in `main.rs`.
2. Decide whether you want to use batch attestation. There is a boolean flag in 1. Decide whether you want to use batch attestation. There is a boolean flag in
`ctap/mod.rs`. It is mandatory for U2F, and you can create your own `ctap/mod.rs`. It is mandatory for U2F, and you can create your own
self-signed certificate. The flag is used for FIDO2 and has some privacy self-signed certificate. The flag is used for FIDO2 and has some privacy
implications. Please check implications. Please check
[WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more [WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more
information. information.
3. Decide whether you want to use signature counters. Currently, only global 1. Decide whether you want to use signature counters. Currently, only global
signature counters are implemented, as they are the default option for U2F. signature counters are implemented, as they are the default option for U2F.
The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy
preserving solution is individual or no signature counters. Again, please preserving solution is individual or no signature counters. Again, please
check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for
documentation. documentation.
4. Depending on your available flash storage, choose an appropriate maximum 1. Depending on your available flash storage, choose an appropriate maximum
number of supported residential keys and number of pages in number of supported resident keys and number of pages in `ctap/storage.rs`.
`ctap/storage.rs`. 1. Change the default level for the credProtect extension in `ctap/mod.rs`.
5. Change the default level for the credProtect extension in `ctap/mod.rs`.
When changing the default, resident credentials become undiscoverable without When changing the default, resident credentials become undiscoverable without
user verification. This helps privacy, but can make usage less comfortable user verification. This helps privacy, but can make usage less comfortable
for credentials that need less protection. for credentials that need less protection.
6. Increase the default minimum length for PINs in `ctap/storage.rs`. 1. Increase the default minimum length for PINs in `ctap/storage.rs`.
The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer
PINs can help establish trust between users and relying parties. It makes PINs can help establish trust between users and relying parties. It makes
user verification harder to break, but less convenient. user verification harder to break, but less convenient.
NIST recommends at least 6-digit PINs in section 5.1.9.1: NIST recommends at least 6-digit PINs in section 5.1.9.1:
https://pages.nist.gov/800-63-3/sp800-63b.html https://pages.nist.gov/800-63-3/sp800-63b.html
You can add relying parties to the list of readers of the minimum PIN length. You can add relying parties to the list of readers of the minimum PIN length.
1. In an enterprise setting, you can adapt `DEFAULT_MIN_PIN_LENGTH_RP_IDS` and
`MAX_RP_IDS_LENGTH` for tuning the `minPinLength` extension. The former
allows some relying parties to read the minimum PIN length by default. The
latter allows storing more relying parties that may check the minimum PIN
length.
### 3D printed enclosure ### 3D printed enclosure
@@ -148,7 +161,7 @@ operation.
The additional output looks like the following. The additional output looks like the following.
``` ```text
# Allocation of 256 byte(s), aligned on 1 byte(s). The allocated address is # Allocation of 256 byte(s), aligned on 1 byte(s). The allocated address is
# 0x2002401c. After this operation, 2 pointers have been allocated, totalling # 0x2002401c. After this operation, 2 pointers have been allocated, totalling
# 384 bytes (the total heap usage may be larger, due to alignment and # 384 bytes (the total heap usage may be larger, due to alignment and
@@ -163,12 +176,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 parses the console output, identifies the lines corresponding to (de)allocation
operations, and first computes some statistics: operations, and first computes some statistics:
- Address range used by the heap over this run of the program, * Address range used by the heap over this run of the program,
- Peak heap usage (how many useful bytes are allocated), * Peak heap usage (how many useful bytes are allocated),
- Peak heap consumption (how many bytes are used by the heap, including * Peak heap consumption (how many bytes are used by the heap, including
unavailable bytes between allocated blocks, due to alignment constraints and unavailable bytes between allocated blocks, due to alignment constraints and
memory fragmentation), 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 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 heap memory. Each frame in this "movie" shows bytes that are currently
@@ -177,10 +190,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. uses the `ncurses` library, that you may have to install beforehand.
You can control the tool with the following parameters: You can control the tool with the following parameters:
- `--logfile` (required) to provide the file which contains the console output
to parse, * `--logfile` (required) to provide the file which contains the console output
- `--fps` (optional) to customize the number of frames per second in the movie to parse,
animation. * `--fps` (optional) to customize the number of frames per second in the movie
animation.
```shell ```shell
cargo run --manifest-path tools/heapviz/Cargo.toml -- --logfile console.log --fps 50 cargo run --manifest-path tools/heapviz/Cargo.toml -- --logfile console.log --fps 50

View File

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

View File

@@ -12,11 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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::env;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
@@ -25,65 +20,11 @@ use std::path::Path;
use uuid::Uuid; use uuid::Uuid;
fn main() { 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"); println!("cargo:rerun-if-changed=crypto_data/aaguid.txt");
let out_dir = env::var_os("OUT_DIR").unwrap(); 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"); 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_bin_file = File::create(&aaguid_bin_path).unwrap();
let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap(); let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap();
let mut content = String::new(); let mut content = String::new();

View File

@@ -710,6 +710,22 @@ class OpenSKInstaller:
check=False, check=False,
timeout=None, timeout=None,
).returncode ).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 return 0
@@ -770,6 +786,33 @@ if __name__ == "__main__":
help=("Erases the persistent storage when installing an application. " help=("Erases the persistent storage when installing an application. "
"All stored data will be permanently lost."), "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( main_parser.add_argument(
"--programmer", "--programmer",
metavar="METHOD", metavar="METHOD",
@@ -838,14 +881,6 @@ if __name__ == "__main__":
help=("Compiles the OpenSK application without backward compatible " help=("Compiles the OpenSK application without backward compatible "
"support for U2F/CTAP1 protocol."), "support for U2F/CTAP1 protocol."),
) )
main_parser.add_argument(
"--ctap2.1",
action="append_const",
const="with_ctap2_1",
dest="features",
help=("Compiles the OpenSK application with backward compatible "
"support for CTAP2.1 protocol."),
)
main_parser.add_argument( main_parser.add_argument(
"--nfc", "--nfc",
action="append_const", action="append_const",
@@ -904,7 +939,16 @@ if __name__ == "__main__":
dest="application", dest="application",
action="store_const", action="store_const",
const="store_latency", const="store_latency",
help=("Compiles and installs the store_latency example.")) help=("Compiles and installs the store_latency example which prints "
"latency statistics of the persistent store library."))
apps_group.add_argument(
"--erase_storage",
dest="application",
action="store_const",
const="erase_storage",
help=("Compiles and installs the erase_storage example which erases "
"the storage. During operation the dongle red light is on. Once "
"the operation is completed the dongle green light is on."))
apps_group.add_argument( apps_group.add_argument(
"--panic_test", "--panic_test",
dest="application", dest="application",

View File

@@ -17,6 +17,7 @@ You will need one the following supported boards:
* [Nordic nRF52840 Dongle](https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF52840-Dongle) * [Nordic nRF52840 Dongle](https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF52840-Dongle)
to have a more practical form factor. to have a more practical form factor.
* [Makerdiary nRF52840-MDK USB dongle](https://wiki.makerdiary.com/nrf52840-mdk/). * [Makerdiary nRF52840-MDK USB dongle](https://wiki.makerdiary.com/nrf52840-mdk/).
* [Feitian OpenSK dongle](https://feitiantech.github.io/OpenSK_USB/).
In the case of the Nordic USB dongle, you may also need the following extra In the case of the Nordic USB dongle, you may also need the following extra
hardware: hardware:
@@ -125,6 +126,7 @@ This is the expected content after running our `setup.sh` script:
File | Purpose File | Purpose
----------------- | -------------------------------------------------------- ----------------- | --------------------------------------------------------
`aaguid.txt` | Text file containaing the AAGUID value
`opensk_ca.csr` | Certificate sign request for the Root CA `opensk_ca.csr` | Certificate sign request for the Root CA
`opensk_ca.key` | ECC secp256r1 private key used 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 `opensk_ca.pem` | PEM encoded certificate of the Root CA
@@ -136,9 +138,11 @@ File | Purpose
If you want to use your own attestation certificate and private key, simply If you want to use your own attestation certificate and private key, simply
replace `opensk_cert.pem` and `opensk.key` files. replace `opensk_cert.pem` and `opensk.key` files.
Our build script `build.rs` is responsible for converting `opensk_cert.pem` and Our build script `build.rs` is responsible for converting the `aaguid.txt` file
`opensk.key` files into raw data that is then used by the Rust file: into raw data that is then used by the Rust file `src/ctap/key_material.rs`.
`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 ### Flashing a firmware

53
examples/erase_storage.rs Normal file
View File

@@ -0,0 +1,53 @@
// 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.
#![no_std]
extern crate lang_items;
use core::fmt::Write;
use ctap2::embedded_flash::new_storage;
use libtock_drivers::console::Console;
use libtock_drivers::led;
use libtock_drivers::result::FlexUnwrap;
use persistent_store::{Storage, StorageIndex};
fn is_page_erased(storage: &dyn Storage, page: usize) -> bool {
let index = StorageIndex { page, byte: 0 };
let length = storage.page_size();
storage
.read_slice(index, length)
.unwrap()
.iter()
.all(|&x| x == 0xff)
}
fn main() {
led::get(1).flex_unwrap().on().flex_unwrap(); // red on dongle
const NUM_PAGES: usize = 20; // should be at least ctap::storage::NUM_PAGES
let mut storage = new_storage(NUM_PAGES);
writeln!(Console::new(), "Erase {} pages of storage:", NUM_PAGES).unwrap();
for page in 0..NUM_PAGES {
write!(Console::new(), "- Page {} ", page).unwrap();
if is_page_erased(&storage, page) {
writeln!(Console::new(), "skipped (was already erased).").unwrap();
} else {
storage.erase_page(page).unwrap();
writeln!(Console::new(), "erased.").unwrap();
}
}
writeln!(Console::new(), "Done.").unwrap();
led::get(1).flex_unwrap().off().flex_unwrap();
led::get(0).flex_unwrap().on().flex_unwrap(); // green on dongle
}

View File

@@ -62,8 +62,10 @@ impl SecKey {
// - https://www.secg.org/sec1-v2.pdf // - https://www.secg.org/sec1-v2.pdf
} }
// DH key agreement method defined in the FIDO2 specification, Section 5.5.4. "Getting /// Creates a shared key using the Diffie Hellman key agreement.
// sharedSecret from Authenticator" ///
/// The key agreement is defined in the FIDO2 specification,
/// Section 6.5.5.4. "Obtaining the Shared Secret"
pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] { pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] {
let p = self.exchange_raw(other); let p = self.exchange_raw(other);
let mut x: [u8; 32] = [Default::default(); 32]; let mut x: [u8; 32] = [Default::default(); 32];
@@ -83,11 +85,13 @@ impl PubKey {
self.p.to_bytes_uncompressed(bytes); self.p.to_bytes_uncompressed(bytes);
} }
/// Creates a new PubKey from its coordinates on the elliptic curve.
pub fn from_coordinates(x: &[u8; NBYTES], y: &[u8; NBYTES]) -> Option<PubKey> { pub fn from_coordinates(x: &[u8; NBYTES], y: &[u8; NBYTES]) -> Option<PubKey> {
PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y)) PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y))
.map(|p| PubKey { p }) .map(|p| PubKey { p })
} }
/// Writes the coordinates into the passed in arrays.
pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) { pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) {
self.p.getx().to_int().to_bin(x); self.p.getx().to_int().to_bin(x);
self.p.gety().to_int().to_bin(y); self.p.gety().to_int().to_bin(y);

View File

@@ -21,12 +21,15 @@ use super::rng256::Rng256;
use super::{Hash256, HashBlockSize64Bytes}; use super::{Hash256, HashBlockSize64Bytes};
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
#[cfg(test)]
use arrayref::array_mut_ref;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use arrayref::array_ref; use arrayref::array_ref;
use arrayref::{array_mut_ref, mut_array_refs}; use arrayref::mut_array_refs;
use cbor::{cbor_bytes, cbor_map_options};
use core::marker::PhantomData; use core::marker::PhantomData;
pub const NBYTES: usize = int256::NBYTES;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
#[cfg_attr(feature = "derive_debug", derive(Debug))] #[cfg_attr(feature = "derive_debug", derive(Debug))]
pub struct SecKey { pub struct SecKey {
@@ -38,6 +41,7 @@ pub struct Signature {
s: NonZeroExponentP256, s: NonZeroExponentP256,
} }
#[cfg_attr(feature = "derive_debug", derive(Clone))]
pub struct PubKey { pub struct PubKey {
p: PointP256, p: PointP256,
} }
@@ -58,10 +62,11 @@ impl SecKey {
} }
} }
// ECDSA signature based on a RNG to generate a suitable randomization parameter. /// Creates an ECDSA signature based on a RNG.
// Under the hood, rejection sampling is used to make sure that the randomization parameter is ///
// uniformly distributed. /// Under the hood, rejection sampling is used to make sure that the
// The provided RNG must be cryptographically secure; otherwise this method is insecure. /// randomization parameter is uniformly distributed. The provided RNG must
/// be cryptographically secure; otherwise this method is insecure.
pub fn sign_rng<H, R>(&self, msg: &[u8], rng: &mut R) -> Signature pub fn sign_rng<H, R>(&self, msg: &[u8], rng: &mut R) -> Signature
where where
H: Hash256, H: Hash256,
@@ -77,8 +82,7 @@ impl SecKey {
} }
} }
// Deterministic ECDSA signature based on RFC 6979 to generate a suitable randomization /// Creates a deterministic ECDSA signature based on RFC 6979.
// parameter.
pub fn sign_rfc6979<H>(&self, msg: &[u8]) -> Signature pub fn sign_rfc6979<H>(&self, msg: &[u8]) -> Signature
where where
H: Hash256 + HashBlockSize64Bytes, H: Hash256 + HashBlockSize64Bytes,
@@ -101,8 +105,10 @@ impl SecKey {
} }
} }
// Try signing a curve element given a randomization parameter k. If no signature can be /// Try signing a curve element given a randomization parameter k.
// obtained from this k, None is returned and the caller should try again with another value. ///
/// If no signature can be obtained from this k, None is returned and the
/// caller should try again with another value.
fn try_sign(&self, k: &NonZeroExponentP256, msg: &ExponentP256) -> Option<Signature> { fn try_sign(&self, k: &NonZeroExponentP256, msg: &ExponentP256) -> Option<Signature> {
let r = ExponentP256::modn(PointP256::base_point_mul(k.as_exponent()).getx().to_int()); let r = ExponentP256::modn(PointP256::base_point_mul(k.as_exponent()).getx().to_int());
// The branching here is fine because all this reveals is that k generated an unsuitable r. // The branching here is fine because all this reveals is that k generated an unsuitable r.
@@ -214,7 +220,6 @@ impl Signature {
} }
impl PubKey { impl PubKey {
pub const ES256_ALGORITHM: i64 = -7;
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
const UNCOMPRESSED_LENGTH: usize = 1 + 2 * int256::NBYTES; const UNCOMPRESSED_LENGTH: usize = 1 + 2 * int256::NBYTES;
@@ -242,35 +247,10 @@ impl PubKey {
representation representation
} }
// Encodes the key according to CBOR Object Signing and Encryption, defined in RFC 8152. /// Writes the coordinates into the passed in arrays.
pub fn to_cose_key(&self) -> Option<Vec<u8>> { pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) {
const EC2_KEY_TYPE: i64 = 2; self.p.getx().to_int().to_bin(x);
const P_256_CURVE: i64 = 1; self.p.gety().to_int().to_bin(y);
let mut x_bytes = vec![0; int256::NBYTES];
self.p
.getx()
.to_int()
.to_bin(array_mut_ref![x_bytes.as_mut_slice(), 0, int256::NBYTES]);
let x_byte_cbor: cbor::Value = cbor_bytes!(x_bytes);
let mut y_bytes = vec![0; int256::NBYTES];
self.p
.gety()
.to_int()
.to_bin(array_mut_ref![y_bytes.as_mut_slice(), 0, int256::NBYTES]);
let y_byte_cbor: cbor::Value = cbor_bytes!(y_bytes);
let cbor_value = cbor_map_options! {
1 => EC2_KEY_TYPE,
3 => PubKey::ES256_ALGORITHM,
-1 => P_256_CURVE,
-2 => x_byte_cbor,
-3 => y_byte_cbor,
};
let mut encoded_key = Vec::new();
if cbor::write(cbor_value, &mut encoded_key) {
Some(encoded_key)
} else {
None
}
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]

View File

@@ -117,7 +117,7 @@ index d72d20482..118ea6d68 100644
+ // Product + // Product
+ "OpenSK", + "OpenSK",
+ // Serial number + // Serial number
+ "v0.1", + "v1.0",
+]; +];
+ +
// State for loading and holding applications. // State for loading and holding applications.
@@ -189,7 +189,7 @@ index 2ebb384d8..4a7bfffdd 100644
+ // Product + // Product
+ "OpenSK", + "OpenSK",
+ // Serial number + // Serial number
+ "v0.1", + "v1.0",
+]; +];
+ +
// State for loading and holding applications. // 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,7 +44,6 @@ cargo test --manifest-path tools/heapviz/Cargo.toml
echo "Checking that CTAP2 builds properly..." echo "Checking that CTAP2 builds properly..."
cargo check --release --target=thumbv7em-none-eabi cargo check --release --target=thumbv7em-none-eabi
cargo check --release --target=thumbv7em-none-eabi --features with_ctap1 cargo check --release --target=thumbv7em-none-eabi --features with_ctap1
cargo check --release --target=thumbv7em-none-eabi --features with_ctap2_1
cargo check --release --target=thumbv7em-none-eabi --features debug_ctap cargo check --release --target=thumbv7em-none-eabi --features debug_ctap
cargo check --release --target=thumbv7em-none-eabi --features panic_console cargo check --release --target=thumbv7em-none-eabi --features panic_console
cargo check --release --target=thumbv7em-none-eabi --features debug_allocations cargo check --release --target=thumbv7em-none-eabi --features debug_allocations
@@ -116,16 +115,4 @@ then
echo "Running unit tests on the desktop (debug mode + CTAP1)..." echo "Running unit tests on the desktop (debug mode + CTAP1)..."
cargo test --features std,with_ctap1 cargo test --features std,with_ctap1
echo "Running unit tests on the desktop (release mode + CTAP2.1)..."
cargo test --release --features std,with_ctap2_1
echo "Running unit tests on the desktop (debug mode + CTAP2.1)..."
cargo test --features std,with_ctap2_1
echo "Running unit tests on the desktop (release mode + CTAP1 + CTAP2.1)..."
cargo test --release --features std,with_ctap1,with_ctap2_1
echo "Running unit tests on the desktop (debug mode + CTAP1 + CTAP2.1)..."
cargo test --features std,with_ctap1,with_ctap2_1
fi fi

View File

@@ -44,3 +44,6 @@ rustup target add thumbv7em-none-eabi
# Install dependency to create applications. # Install dependency to create applications.
mkdir -p elf2tab mkdir -p elf2tab
cargo install elf2tab --version 0.6.0 --root 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

@@ -1,4 +1,4 @@
// Copyright 2020 Google LLC // Copyright 2020-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -16,43 +16,31 @@ use alloc::vec::Vec;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use core::convert::TryFrom; use core::convert::TryFrom;
type ByteArray = &'static [u8];
const APDU_HEADER_LEN: usize = 4; const APDU_HEADER_LEN: usize = 4;
#[cfg_attr(test, derive(Clone, Debug))] #[cfg_attr(test, derive(Clone, Debug))]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types, dead_code)]
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum ApduStatusCode { pub enum ApduStatusCode {
SW_SUCCESS, SW_SUCCESS = 0x90_00,
/// Command successfully executed; 'XX' bytes of data are /// Command successfully executed; 'XX' bytes of data are
/// available and can be requested using GET RESPONSE. /// available and can be requested using GET RESPONSE.
SW_GET_RESPONSE, SW_GET_RESPONSE = 0x61_00,
SW_WRONG_DATA, SW_MEMERR = 0x65_01,
SW_WRONG_LENGTH, SW_WRONG_DATA = 0x6a_80,
SW_COND_USE_NOT_SATISFIED, SW_WRONG_LENGTH = 0x67_00,
SW_FILE_NOT_FOUND, SW_COND_USE_NOT_SATISFIED = 0x69_85,
SW_INCORRECT_P1P2, SW_FILE_NOT_FOUND = 0x6a_82,
SW_INCORRECT_P1P2 = 0x6a_86,
/// Instruction code not supported or invalid /// Instruction code not supported or invalid
SW_INS_INVALID, SW_INS_INVALID = 0x6d_00,
SW_CLA_INVALID, SW_CLA_INVALID = 0x6e_00,
SW_INTERNAL_EXCEPTION, SW_INTERNAL_EXCEPTION = 0x6f_00,
} }
impl From<ApduStatusCode> for ByteArray { impl From<ApduStatusCode> for u16 {
fn from(status_code: ApduStatusCode) -> ByteArray { fn from(code: ApduStatusCode) -> Self {
match status_code { code as u16
ApduStatusCode::SW_SUCCESS => b"\x90\x00",
ApduStatusCode::SW_GET_RESPONSE => b"\x61\x00",
ApduStatusCode::SW_WRONG_DATA => b"\x6A\x80",
ApduStatusCode::SW_WRONG_LENGTH => b"\x67\x00",
ApduStatusCode::SW_COND_USE_NOT_SATISFIED => b"\x69\x85",
ApduStatusCode::SW_FILE_NOT_FOUND => b"\x6a\x82",
ApduStatusCode::SW_INCORRECT_P1P2 => b"\x6a\x86",
ApduStatusCode::SW_INS_INVALID => b"\x6d\x00",
ApduStatusCode::SW_CLA_INVALID => b"\x6e\x00",
ApduStatusCode::SW_INTERNAL_EXCEPTION => b"\x6f\x00",
}
} }
} }
@@ -67,10 +55,10 @@ pub enum ApduInstructions {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
pub struct ApduHeader { pub struct ApduHeader {
cla: u8, pub cla: u8,
ins: u8, pub ins: u8,
p1: u8, pub p1: u8,
p2: u8, pub p2: u8,
} }
impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader { impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader {
@@ -110,11 +98,11 @@ pub enum ApduType {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(PartialEq)] #[derive(PartialEq)]
pub struct APDU { pub struct APDU {
header: ApduHeader, pub header: ApduHeader,
lc: u16, pub lc: u16,
data: Vec<u8>, pub data: Vec<u8>,
le: u32, pub le: u32,
case_type: ApduType, pub case_type: ApduType,
} }
impl TryFrom<&[u8]> for APDU { impl TryFrom<&[u8]> for APDU {
@@ -182,12 +170,19 @@ impl TryFrom<&[u8]> for APDU {
} }
if payload.len() > 2 { if payload.len() > 2 {
// Lc is possibly three-bytes long // Lc is possibly three-bytes long
let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..]) as usize; let extended_apdu_lc = BigEndian::read_u16(&payload[1..3]) as usize;
let extended_apdu_le_len: usize = if payload.len() > extended_apdu_lc { if payload.len() < extended_apdu_lc + 3 {
payload.len() - extended_apdu_lc - 3 return Err(ApduStatusCode::SW_WRONG_LENGTH);
} else { }
0
}; let extended_apdu_le_len: usize = payload
.len()
.checked_sub(extended_apdu_lc + 3)
.ok_or(ApduStatusCode::SW_WRONG_LENGTH)?;
if extended_apdu_le_len > 3 {
return Err(ApduStatusCode::SW_WRONG_LENGTH);
}
if byte_0 == 0 && extended_apdu_le_len <= 3 { if byte_0 == 0 && extended_apdu_le_len <= 3 {
// If first byte is zero AND the next two bytes can be parsed as a big-endian // If first byte is zero AND the next two bytes can be parsed as a big-endian
// length that covers the rest of the block (plus few additional bytes for Le), we // length that covers the rest of the block (plus few additional bytes for Le), we

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -13,14 +13,17 @@
// limitations under the License. // limitations under the License.
use super::data_formats::{ use super::data_formats::{
extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned, extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string,
ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions, extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams,
MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions,
PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams,
}; };
use super::key_material;
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use arrayref::array_ref;
use cbor::destructure_cbor_map; use cbor::destructure_cbor_map;
use core::convert::TryFrom; use core::convert::TryFrom;
@@ -38,9 +41,11 @@ pub enum Command {
AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorClientPin(AuthenticatorClientPinParameters),
AuthenticatorReset, AuthenticatorReset,
AuthenticatorGetNextAssertion, AuthenticatorGetNextAssertion,
#[cfg(feature = "with_ctap2_1")]
AuthenticatorSelection, AuthenticatorSelection,
AuthenticatorConfig(AuthenticatorConfigParameters),
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
// Vendor specific commands
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters),
} }
impl From<cbor::reader::DecoderError> for Ctap2StatusCode { impl From<cbor::reader::DecoderError> for Ctap2StatusCode {
@@ -60,11 +65,13 @@ impl Command {
const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08;
// TODO(kaczmarczyck) use or remove those constants // TODO(kaczmarczyck) use or remove those constants
const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09;
const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A;
const AUTHENTICATOR_SELECTION: u8 = 0xB0; const AUTHENTICATOR_SELECTION: u8 = 0x0B;
const AUTHENTICATOR_CONFIG: u8 = 0xC0; const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C;
const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; const AUTHENTICATOR_CONFIG: u8 = 0x0D;
const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; const _AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40;
const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40;
const _AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF;
pub fn deserialize(bytes: &[u8]) -> Result<Command, Ctap2StatusCode> { pub fn deserialize(bytes: &[u8]) -> Result<Command, Ctap2StatusCode> {
if bytes.is_empty() { if bytes.is_empty() {
@@ -104,11 +111,22 @@ impl Command {
// Parameters are ignored. // Parameters are ignored.
Ok(Command::AuthenticatorGetNextAssertion) Ok(Command::AuthenticatorGetNextAssertion)
} }
#[cfg(feature = "with_ctap2_1")]
Command::AUTHENTICATOR_SELECTION => { Command::AUTHENTICATOR_SELECTION => {
// Parameters are ignored. // Parameters are ignored.
Ok(Command::AuthenticatorSelection) Ok(Command::AuthenticatorSelection)
} }
Command::AUTHENTICATOR_CONFIG => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorConfig(
AuthenticatorConfigParameters::try_from(decoded_cbor)?,
))
}
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), _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND),
} }
} }
@@ -279,13 +297,7 @@ pub struct AuthenticatorClientPinParameters {
pub pin_auth: Option<Vec<u8>>, pub pin_auth: Option<Vec<u8>>,
pub new_pin_enc: Option<Vec<u8>>, pub new_pin_enc: Option<Vec<u8>>,
pub pin_hash_enc: Option<Vec<u8>>, pub pin_hash_enc: Option<Vec<u8>>,
#[cfg(feature = "with_ctap2_1")]
pub min_pin_length: Option<u8>,
#[cfg(feature = "with_ctap2_1")]
pub min_pin_length_rp_ids: Option<Vec<String>>,
#[cfg(feature = "with_ctap2_1")]
pub permissions: Option<u8>, pub permissions: Option<u8>,
#[cfg(feature = "with_ctap2_1")]
pub permissions_rp_id: Option<String>, pub permissions_rp_id: Option<String>,
} }
@@ -293,7 +305,6 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
type Error = Ctap2StatusCode; type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> { fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
#[cfg(not(feature = "with_ctap2_1"))]
destructure_cbor_map! { destructure_cbor_map! {
let { let {
1 => pin_protocol, 1 => pin_protocol,
@@ -302,19 +313,6 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
4 => pin_auth, 4 => pin_auth,
5 => new_pin_enc, 5 => new_pin_enc,
6 => pin_hash_enc, 6 => pin_hash_enc,
} = extract_map(cbor_value)?;
}
#[cfg(feature = "with_ctap2_1")]
destructure_cbor_map! {
let {
1 => pin_protocol,
2 => sub_command,
3 => key_agreement,
4 => pin_auth,
5 => new_pin_enc,
6 => pin_hash_enc,
7 => min_pin_length,
8 => min_pin_length_rp_ids,
9 => permissions, 9 => permissions,
10 => permissions_rp_id, 10 => permissions_rp_id,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
@@ -322,35 +320,16 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?;
let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?;
let key_agreement = key_agreement.map(extract_map).transpose()?.map(CoseKey); let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?;
let pin_auth = pin_auth.map(extract_byte_string).transpose()?; let pin_auth = pin_auth.map(extract_byte_string).transpose()?;
let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?; let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?;
let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?; let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?;
#[cfg(feature = "with_ctap2_1")]
let min_pin_length = min_pin_length
.map(extract_unsigned)
.transpose()?
.map(u8::try_from)
.transpose()
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
#[cfg(feature = "with_ctap2_1")]
let min_pin_length_rp_ids = match min_pin_length_rp_ids {
Some(entry) => Some(
extract_array(entry)?
.into_iter()
.map(extract_text_string)
.collect::<Result<Vec<String>, Ctap2StatusCode>>()?,
),
None => None,
};
#[cfg(feature = "with_ctap2_1")]
// We expect a bit field of 8 bits, and drop everything else. // We expect a bit field of 8 bits, and drop everything else.
// This means we ignore extensions in future versions. // This means we ignore extensions in future versions.
let permissions = permissions let permissions = permissions
.map(extract_unsigned) .map(extract_unsigned)
.transpose()? .transpose()?
.map(|p| p as u8); .map(|p| p as u8);
#[cfg(feature = "with_ctap2_1")]
let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?;
Ok(AuthenticatorClientPinParameters { Ok(AuthenticatorClientPinParameters {
@@ -360,18 +339,108 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
pin_auth, pin_auth,
new_pin_enc, new_pin_enc,
pin_hash_enc, pin_hash_enc,
#[cfg(feature = "with_ctap2_1")]
min_pin_length,
#[cfg(feature = "with_ctap2_1")]
min_pin_length_rp_ids,
#[cfg(feature = "with_ctap2_1")]
permissions, permissions,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id, permissions_rp_id,
}) })
} }
} }
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorConfigParameters {
pub sub_command: ConfigSubCommand,
pub sub_command_params: Option<ConfigSubCommandParams>,
pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>,
}
impl TryFrom<cbor::Value> for AuthenticatorConfigParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => sub_command,
0x02 => sub_command_params,
0x03 => pin_uv_auth_param,
0x04 => pin_uv_auth_protocol,
} = extract_map(cbor_value)?;
}
let sub_command = ConfigSubCommand::try_from(ok_or_missing(sub_command)?)?;
let sub_command_params = match sub_command {
ConfigSubCommand::SetMinPinLength => Some(ConfigSubCommandParams::SetMinPinLength(
SetMinPinLengthParams::try_from(ok_or_missing(sub_command_params)?)?,
)),
_ => None,
};
let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?;
let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?;
Ok(AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_param,
pin_uv_auth_protocol,
})
}
}
#[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)] #[cfg(test)]
mod test { mod test {
use super::super::data_formats::{ use super::super::data_formats::{
@@ -380,8 +449,8 @@ mod test {
}; };
use super::super::ES256_CRED_PARAM; use super::super::ES256_CRED_PARAM;
use super::*; use super::*;
use alloc::collections::BTreeMap;
use cbor::{cbor_array, cbor_map}; use cbor::{cbor_array, cbor_map};
use crypto::rng256::ThreadRng256;
#[test] #[test]
fn test_from_cbor_make_credential_parameters() { fn test_from_cbor_make_credential_parameters() {
@@ -491,27 +560,18 @@ mod test {
#[test] #[test]
fn test_from_cbor_client_pin_parameters() { fn test_from_cbor_client_pin_parameters() {
// TODO(kaczmarczyck) inline the #cfg when #128 is resolved: let mut rng = ThreadRng256 {};
// https://github.com/google/OpenSK/issues/128 let sk = crypto::ecdh::SecKey::gensk(&mut rng);
#[cfg(not(feature = "with_ctap2_1"))] let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let cbor_value = cbor_map! { let cbor_value = cbor_map! {
1 => 1, 1 => 1,
2 => ClientPinSubCommand::GetPinRetries, 2 => ClientPinSubCommand::GetPinRetries,
3 => cbor_map!{}, 3 => cbor::Value::from(cose_key.clone()),
4 => vec! [0xBB], 4 => vec! [0xBB],
5 => vec! [0xCC], 5 => vec! [0xCC],
6 => vec! [0xDD], 6 => vec! [0xDD],
};
#[cfg(feature = "with_ctap2_1")]
let cbor_value = cbor_map! {
1 => 1,
2 => ClientPinSubCommand::GetPinRetries,
3 => cbor_map!{},
4 => vec! [0xBB],
5 => vec! [0xCC],
6 => vec! [0xDD],
7 => 4,
8 => cbor_array!["example.com"],
9 => 0x03, 9 => 0x03,
10 => "example.com", 10 => "example.com",
}; };
@@ -521,17 +581,11 @@ mod test {
let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { let expected_pin_protocol_parameters = AuthenticatorClientPinParameters {
pin_protocol: 1, pin_protocol: 1,
sub_command: ClientPinSubCommand::GetPinRetries, sub_command: ClientPinSubCommand::GetPinRetries,
key_agreement: Some(CoseKey(BTreeMap::new())), key_agreement: Some(cose_key),
pin_auth: Some(vec![0xBB]), pin_auth: Some(vec![0xBB]),
new_pin_enc: Some(vec![0xCC]), new_pin_enc: Some(vec![0xCC]),
pin_hash_enc: Some(vec![0xDD]), pin_hash_enc: Some(vec![0xDD]),
#[cfg(feature = "with_ctap2_1")]
min_pin_length: Some(4),
#[cfg(feature = "with_ctap2_1")]
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
#[cfg(feature = "with_ctap2_1")]
permissions: Some(0x03), permissions: Some(0x03),
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id: Some("example.com".to_string()), permissions_rp_id: Some("example.com".to_string()),
}; };
@@ -563,11 +617,89 @@ mod test {
assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion));
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_deserialize_selection() { fn test_deserialize_selection() {
let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; let cbor_bytes = [Command::AUTHENTICATOR_SELECTION];
let command = Command::deserialize(&cbor_bytes); let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Ok(Command::AuthenticatorSelection)); 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
})
})
);
}
} }

272
src/ctap/config_command.rs Normal file
View File

@@ -0,0 +1,272 @@
// Copyright 2020-2021 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.
use super::check_pin_uv_auth_protocol;
use super::command::AuthenticatorConfigParameters;
use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams};
use super::pin_protocol_v1::PinProtocolV1;
use super::response::ResponseData;
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use alloc::vec;
/// Processes the subcommand setMinPINLength for AuthenticatorConfig.
fn process_set_min_pin_length(
persistent_store: &mut PersistentStore,
params: SetMinPinLengthParams,
) -> Result<ResponseData, Ctap2StatusCode> {
let SetMinPinLengthParams {
new_min_pin_length,
min_pin_length_rp_ids,
force_change_pin,
} = params;
let store_min_pin_length = persistent_store.min_pin_length()?;
let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length);
if new_min_pin_length < store_min_pin_length {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
let mut force_change_pin = force_change_pin.unwrap_or(false);
if force_change_pin && persistent_store.pin_hash()?.is_none() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
}
if let Some(old_length) = persistent_store.pin_code_point_length()? {
force_change_pin |= new_min_pin_length > old_length;
}
if force_change_pin {
// TODO(kaczmarczyck) actually force a PIN change in PinProtocolV1
persistent_store.force_pin_change()?;
}
persistent_store.set_min_pin_length(new_min_pin_length)?;
if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids {
persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?;
}
Ok(ResponseData::AuthenticatorConfig)
}
/// Processes the AuthenticatorConfig command.
pub fn process_config(
persistent_store: &mut PersistentStore,
pin_protocol_v1: &mut PinProtocolV1,
params: AuthenticatorConfigParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = params;
if persistent_store.pin_hash()?.is_some() {
// TODO(kaczmarczyck) The error code is specified inconsistently with other commands.
check_pin_uv_auth_protocol(pin_uv_auth_protocol)
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
// Constants are taken from the specification, section 6.11, step 4.2.
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, sub_command as u8]);
if let Some(sub_command_params) = sub_command_params.clone() {
if !cbor::write(sub_command_params.into(), &mut config_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
if !pin_protocol_v1.verify_pin_auth_token(&config_data, &auth_param) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
}
match sub_command {
ConfigSubCommand::SetMinPinLength => {
if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params {
process_set_min_pin_length(persistent_store, params)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
}
}
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
}
}
#[cfg(test)]
mod test {
use super::*;
use crypto::rng256::ThreadRng256;
fn create_min_pin_config_params(
min_pin_length: u8,
min_pin_length_rp_ids: Option<Vec<String>>,
) -> AuthenticatorConfigParameters {
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(min_pin_length),
min_pin_length_rp_ids,
force_change_pin: None,
};
AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param: None,
pin_uv_auth_protocol: Some(1),
}
}
#[test]
fn test_process_set_min_pin_length() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
// First, increase minimum PIN length from 4 to 6 without PIN auth.
let min_pin_length = 6;
let config_params = create_min_pin_config_params(min_pin_length, None);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
// Second, increase minimum PIN length from 6 to 8 with PIN auth.
// The stored PIN or its length don't matter since we control the token.
persistent_store.set_pin(&[0x88; 16], 8).unwrap();
let min_pin_length = 8;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_auth = vec![
0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29,
0xB2, 0xDE,
];
config_params.pin_uv_auth_param = Some(pin_auth);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
// Third, decreasing the minimum PIN length from 8 to 7 fails.
let mut config_params = create_min_pin_config_params(7, None);
let pin_auth = vec![
0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8,
0xA7, 0x71,
];
config_params.pin_uv_auth_param = Some(pin_auth);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
);
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
}
#[test]
fn test_process_set_min_pin_length_rp_ids() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
// First, set RP IDs without PIN auth.
let min_pin_length = 6;
let min_pin_length_rp_ids = vec!["example.com".to_string()];
let config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids)
);
// Second, change the RP IDs with PIN auth.
let min_pin_length = 8;
let min_pin_length_rp_ids = vec!["another.example.com".to_string()];
// The stored PIN or its length don't matter since we control the token.
persistent_store.set_pin(&[0x88; 16], 8).unwrap();
let mut config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let pin_auth = vec![
0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D,
0xD6, 0xDA,
];
config_params.pin_uv_auth_param = Some(pin_auth.clone());
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids.clone())
);
// Third, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different lengths.
let mut config_params =
create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone()));
config_params.pin_uv_auth_param = Some(pin_auth.clone());
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids.clone())
);
// Forth, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different RP IDs.
let mut config_params = create_min_pin_config_params(
min_pin_length,
Some(vec!["counter.example.com".to_string()]),
);
config_params.pin_uv_auth_param = Some(pin_auth);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids)
);
}
#[test]
fn test_process_config_vendor_prototype() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::VendorPrototype,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use super::apdu::{ApduStatusCode, APDU};
use super::hid::ChannelID; use super::hid::ChannelID;
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
use super::CtapState; use super::CtapState;
@@ -22,49 +23,12 @@ use core::convert::TryFrom;
use crypto::rng256::Rng256; use crypto::rng256::Rng256;
use libtock_drivers::timer::ClockValue; use libtock_drivers::timer::ClockValue;
// For now, they're the same thing with apdu.rs containing the authoritative definition
pub type Ctap1StatusCode = ApduStatusCode;
// The specification referenced in this file is at: // The specification referenced in this file is at:
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf
// status codes specification (version 20170411) section 3.3
#[allow(non_camel_case_types)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub enum Ctap1StatusCode {
SW_NO_ERROR = 0x9000,
SW_CONDITIONS_NOT_SATISFIED = 0x6985,
SW_WRONG_DATA = 0x6A80,
SW_WRONG_LENGTH = 0x6700,
SW_CLA_NOT_SUPPORTED = 0x6E00,
SW_INS_NOT_SUPPORTED = 0x6D00,
SW_MEMERR = 0x6501,
SW_COMMAND_ABORTED = 0x6F00,
SW_VENDOR_KEY_HANDLE_TOO_LONG = 0xF000,
}
impl TryFrom<u16> for Ctap1StatusCode {
type Error = ();
fn try_from(value: u16) -> Result<Ctap1StatusCode, ()> {
match value {
0x9000 => Ok(Ctap1StatusCode::SW_NO_ERROR),
0x6985 => Ok(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED),
0x6A80 => Ok(Ctap1StatusCode::SW_WRONG_DATA),
0x6700 => Ok(Ctap1StatusCode::SW_WRONG_LENGTH),
0x6E00 => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED),
0x6D00 => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED),
0x6501 => Ok(Ctap1StatusCode::SW_MEMERR),
0x6F00 => Ok(Ctap1StatusCode::SW_COMMAND_ABORTED),
0xF000 => Ok(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG),
_ => Err(()),
}
}
}
impl Into<u16> for Ctap1StatusCode {
fn into(self) -> u16 {
self as u16
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug))]
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum Ctap1Flags { pub enum Ctap1Flags {
@@ -118,11 +82,14 @@ impl TryFrom<&[u8]> for U2fCommand {
type Error = Ctap1StatusCode; type Error = Ctap1StatusCode;
fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> { fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> {
if message.len() < Ctap1Command::APDU_HEADER_LEN as usize { let apdu: APDU = match APDU::try_from(message) {
return Err(Ctap1StatusCode::SW_WRONG_DATA); Ok(apdu) => apdu,
} Err(apdu_status_code) => {
return Err(Ctap1StatusCode::try_from(apdu_status_code).unwrap())
}
};
let (apdu, payload) = message.split_at(Ctap1Command::APDU_HEADER_LEN as usize); let lc = apdu.lc as usize;
// ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always // ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always
// encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected). // encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected).
@@ -131,19 +98,17 @@ impl TryFrom<&[u8]> for U2fCommand {
// +-----+-----+----+----+-----+-----+-----+ // +-----+-----+----+----+-----+-----+-----+
// | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 | // | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 |
// +-----+-----+----+----+-----+-----+-----+ // +-----+-----+----+----+-----+-----+-----+
if apdu[0] != Ctap1Command::CTAP1_CLA { if apdu.header.cla != Ctap1Command::CTAP1_CLA {
return Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED); return Err(Ctap1StatusCode::SW_CLA_INVALID);
} }
let lc = (((apdu[4] as u32) << 16) | ((apdu[5] as u32) << 8) | (apdu[6] as u32)) as usize;
// Since there is always request data, the expected length is either omitted or // Since there is always request data, the expected length is either omitted or
// encoded in 2 bytes. // encoded in 2 bytes.
if lc != payload.len() && lc + 2 != payload.len() { if lc != apdu.data.len() && lc + 2 != apdu.data.len() {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH); return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
} }
match apdu[1] { match apdu.header.ins {
// U2F raw message format specification, Section 4.1 // U2F raw message format specification, Section 4.1
// +-----------------+-------------------+ // +-----------------+-------------------+
// + Challenge (32B) | Application (32B) | // + Challenge (32B) | Application (32B) |
@@ -153,8 +118,8 @@ impl TryFrom<&[u8]> for U2fCommand {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH); return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
} }
Ok(Self::Register { Ok(Self::Register {
challenge: *array_ref!(payload, 0, 32), challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(payload, 32, 32), application: *array_ref!(apdu.data, 32, 32),
}) })
} }
@@ -166,15 +131,15 @@ impl TryFrom<&[u8]> for U2fCommand {
if lc < 65 { if lc < 65 {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH); return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
} }
let handle_length = payload[64] as usize; let handle_length = apdu.data[64] as usize;
if lc != 65 + handle_length { if lc != 65 + handle_length {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH); return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
} }
let flag = Ctap1Flags::try_from(apdu[2])?; let flag = Ctap1Flags::try_from(apdu.header.p1)?;
Ok(Self::Authenticate { Ok(Self::Authenticate {
challenge: *array_ref!(payload, 0, 32), challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(payload, 32, 32), application: *array_ref!(apdu.data, 32, 32),
key_handle: payload[65..lc].to_vec(), key_handle: apdu.data[65..].to_vec(),
flags: flag, flags: flag,
}) })
} }
@@ -190,11 +155,11 @@ impl TryFrom<&[u8]> for U2fCommand {
// For Vendor specific command. // For Vendor specific command.
Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => { Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => {
Ok(Self::VendorSpecific { Ok(Self::VendorSpecific {
payload: payload.to_vec(), payload: apdu.data.to_vec(),
}) })
} }
_ => Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), _ => Err(Ctap1StatusCode::SW_INS_INVALID),
} }
} }
} }
@@ -202,8 +167,6 @@ impl TryFrom<&[u8]> for U2fCommand {
pub struct Ctap1Command {} pub struct Ctap1Command {}
impl Ctap1Command { impl Ctap1Command {
const APDU_HEADER_LEN: u32 = 7; // CLA + INS + P1 + P2 + LC1-3
const CTAP1_CLA: u8 = 0; const CTAP1_CLA: u8 = 0;
// This byte is used in Register, but only serves backwards compatibility. // This byte is used in Register, but only serves backwards compatibility.
const LEGACY_BYTE: u8 = 0x05; const LEGACY_BYTE: u8 = 0x05;
@@ -234,7 +197,7 @@ impl Ctap1Command {
application, application,
} => { } => {
if !ctap_state.u2f_up_state.consume_up(clock_value) { if !ctap_state.u2f_up_state.consume_up(clock_value) {
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED); return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
} }
Ctap1Command::process_register(challenge, application, ctap_state) Ctap1Command::process_register(challenge, application, ctap_state)
} }
@@ -249,7 +212,7 @@ impl Ctap1Command {
if flags == Ctap1Flags::EnforceUpAndSign if flags == Ctap1Flags::EnforceUpAndSign
&& !ctap_state.u2f_up_state.consume_up(clock_value) && !ctap_state.u2f_up_state.consume_up(clock_value)
{ {
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED); return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
} }
Ctap1Command::process_authenticate( Ctap1Command::process_authenticate(
challenge, challenge,
@@ -264,7 +227,7 @@ impl Ctap1Command {
U2fCommand::Version => Ok(Vec::<u8>::from(super::U2F_VERSION_STRING)), U2fCommand::Version => Ok(Vec::<u8>::from(super::U2F_VERSION_STRING)),
// TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED? // TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED?
U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_NO_ERROR), U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_SUCCESS),
} }
} }
@@ -292,22 +255,22 @@ impl Ctap1Command {
let pk = sk.genpk(); let pk = sk.genpk();
let key_handle = ctap_state let key_handle = ctap_state
.encrypt_key_handle(sk, &application) .encrypt_key_handle(sk, &application)
.map_err(|_| Ctap1StatusCode::SW_COMMAND_ABORTED)?; .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
if key_handle.len() > 0xFF { if key_handle.len() > 0xFF {
// This is just being defensive with unreachable code. // This is just being defensive with unreachable code.
return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG); return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION);
} }
let certificate = ctap_state let certificate = ctap_state
.persistent_store .persistent_store
.attestation_certificate() .attestation_certificate()
.map_err(|_| Ctap1StatusCode::SW_MEMERR)? .map_err(|_| Ctap1StatusCode::SW_MEMERR)?
.ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; .ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let private_key = ctap_state let private_key = ctap_state
.persistent_store .persistent_store
.attestation_private_key() .attestation_private_key()
.map_err(|_| Ctap1StatusCode::SW_MEMERR)? .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
.ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; .ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len()); let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len());
response.push(Ctap1Command::LEGACY_BYTE); response.push(Ctap1Command::LEGACY_BYTE);
@@ -362,7 +325,7 @@ impl Ctap1Command {
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
if let Some(credential_source) = credential_source { if let Some(credential_source) = credential_source {
if flags == Ctap1Flags::CheckOnly { if flags == Ctap1Flags::CheckOnly {
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED); return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
} }
ctap_state ctap_state
.increment_global_signature_counter() .increment_global_signature_counter()
@@ -448,7 +411,7 @@ mod test {
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
// Certificate and private key are missing // Certificate and private key are missing
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH];
assert!(ctap_state assert!(ctap_state
@@ -459,7 +422,7 @@ mod test {
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
// Certificate is still missing // Certificate is still missing
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
let fake_cert = [0x99u8; 100]; // Arbitrary length let fake_cert = [0x99u8; 100]; // Arbitrary length
assert!(ctap_state assert!(ctap_state
@@ -513,7 +476,7 @@ mod test {
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
let response = let response =
Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE); Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED)); assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
} }
#[test] #[test]
@@ -529,7 +492,7 @@ mod test {
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED)); assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
} }
#[test] #[test]
@@ -559,15 +522,24 @@ mod test {
let rp_id = "example.com"; let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap();
let mut message = let mut message = create_authenticate_message(
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); &application,
Ctap1Flags::DontEnforceUpAndSign,
&key_handle,
);
message.push(0x00); message.push(0x00);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); assert!(response.is_ok());
// Two extra zeros are okay, they could encode the expected response length.
message.push(0x00); message.push(0x00);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert!(response.is_ok());
message.push(0x00); message.push(0x00);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
@@ -588,7 +560,7 @@ mod test {
message[0] = 0xEE; message[0] = 0xEE;
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED)); assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_INVALID));
} }
#[test] #[test]
@@ -606,7 +578,7 @@ mod test {
message[1] = 0xEE; message[1] = 0xEE;
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); assert_eq!(response, Err(Ctap1StatusCode::SW_INS_INVALID));
} }
#[test] #[test]
@@ -722,6 +694,6 @@ mod test {
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
let response = let response =
Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE); Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED)); assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
} }
} }

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -17,12 +17,15 @@ use alloc::collections::BTreeMap;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use arrayref::array_ref; use arrayref::array_ref;
use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; use cbor::{cbor_array_vec, cbor_map, cbor_map_options, destructure_cbor_map};
use core::convert::TryFrom; use core::convert::TryFrom;
use crypto::{ecdh, ecdsa}; use crypto::{ecdh, ecdsa};
#[cfg(test)] #[cfg(test)]
use enum_iterator::IntoEnumIterator; use enum_iterator::IntoEnumIterator;
// Used as the identifier for ECDSA in assertion signatures and COSE.
const ES256_ALGORITHM: i64 = -7;
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct PublicKeyCredentialRpEntity { pub struct PublicKeyCredentialRpEntity {
@@ -262,6 +265,7 @@ impl From<PublicKeyCredentialDescriptor> for cbor::Value {
pub struct MakeCredentialExtensions { pub struct MakeCredentialExtensions {
pub hmac_secret: bool, pub hmac_secret: bool,
pub cred_protect: Option<CredentialProtectionPolicy>, pub cred_protect: Option<CredentialProtectionPolicy>,
pub min_pin_length: bool,
} }
impl TryFrom<cbor::Value> for MakeCredentialExtensions { impl TryFrom<cbor::Value> for MakeCredentialExtensions {
@@ -272,6 +276,7 @@ impl TryFrom<cbor::Value> for MakeCredentialExtensions {
let { let {
"credProtect" => cred_protect, "credProtect" => cred_protect,
"hmac-secret" => hmac_secret, "hmac-secret" => hmac_secret,
"minPinLength" => min_pin_length,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
} }
@@ -279,9 +284,11 @@ impl TryFrom<cbor::Value> for MakeCredentialExtensions {
let cred_protect = cred_protect let cred_protect = cred_protect
.map(CredentialProtectionPolicy::try_from) .map(CredentialProtectionPolicy::try_from)
.transpose()?; .transpose()?;
let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?;
Ok(Self { Ok(Self {
hmac_secret, hmac_secret,
cred_protect, cred_protect,
min_pin_length,
}) })
} }
} }
@@ -322,17 +329,17 @@ impl TryFrom<cbor::Value> for GetAssertionHmacSecretInput {
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> { fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! { destructure_cbor_map! {
let { let {
1 => cose_key, 1 => key_agreement,
2 => salt_enc, 2 => salt_enc,
3 => salt_auth, 3 => salt_auth,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
} }
let cose_key = extract_map(ok_or_missing(cose_key)?)?; let key_agreement = CoseKey::try_from(ok_or_missing(key_agreement)?)?;
let salt_enc = extract_byte_string(ok_or_missing(salt_enc)?)?; let salt_enc = extract_byte_string(ok_or_missing(salt_enc)?)?;
let salt_auth = extract_byte_string(ok_or_missing(salt_auth)?)?; let salt_auth = extract_byte_string(ok_or_missing(salt_auth)?)?;
Ok(Self { Ok(Self {
key_agreement: CoseKey(cose_key), key_agreement,
salt_enc, salt_enc,
salt_auth, salt_auth,
}) })
@@ -432,7 +439,7 @@ impl From<PackedAttestationStatement> for cbor::Value {
#[derive(PartialEq)] #[derive(PartialEq)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
pub enum SignatureAlgorithm { pub enum SignatureAlgorithm {
ES256 = ecdsa::PubKey::ES256_ALGORITHM as isize, ES256 = ES256_ALGORITHM as isize,
// This is the default for all numbers not covered above. // This is the default for all numbers not covered above.
// Unknown types should be ignored, instead of returning errors. // Unknown types should be ignored, instead of returning errors.
Unknown = 0, Unknown = 0,
@@ -449,7 +456,7 @@ impl TryFrom<cbor::Value> for SignatureAlgorithm {
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> { fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
match extract_integer(cbor_value)? { match extract_integer(cbor_value)? {
ecdsa::PubKey::ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256),
_ => Ok(SignatureAlgorithm::Unknown), _ => Ok(SignatureAlgorithm::Unknown),
} }
} }
@@ -614,72 +621,42 @@ impl PublicKeyCredentialSource {
} }
} }
// TODO(kaczmarczyck) we could decide to split this data type up // The COSE key is used for both ECDH and ECDSA public keys for transmission.
// It depends on the algorithm though, I think.
// So before creating a mess, this is my workaround.
#[derive(Clone)] #[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct CoseKey(pub BTreeMap<cbor::KeyType, cbor::Value>); pub struct CoseKey {
x_bytes: [u8; ecdh::NBYTES],
// This is the algorithm specifier that is supposed to be used in a COSE key y_bytes: [u8; ecdh::NBYTES],
// map. The CTAP specification says -25 which represents ECDH-ES + HKDF-256 algorithm: i64,
// here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// In fact, this is just used for compatibility with older specification versions.
const ECDH_ALGORITHM: i64 = -25;
// This is the identifier used by OpenSSH. To be compatible, we accept both.
const ES256_ALGORITHM: i64 = -7;
const EC2_KEY_TYPE: i64 = 2;
const P_256_CURVE: i64 = 1;
impl From<ecdh::PubKey> for CoseKey {
fn from(pk: ecdh::PubKey) -> Self {
let mut x_bytes = [0; ecdh::NBYTES];
let mut y_bytes = [0; ecdh::NBYTES];
pk.to_coordinates(&mut x_bytes, &mut y_bytes);
let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes);
let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes);
// TODO(kaczmarczyck) do not write optional parameters, spec is unclear
let cose_cbor_value = cbor_map_options! {
1 => EC2_KEY_TYPE,
3 => ECDH_ALGORITHM,
-1 => P_256_CURVE,
-2 => x_byte_cbor,
-3 => y_byte_cbor,
};
if let cbor::Value::Map(cose_map) = cose_cbor_value {
CoseKey(cose_map)
} else {
unreachable!();
}
}
} }
impl TryFrom<CoseKey> for ecdh::PubKey { impl CoseKey {
// This is the algorithm specifier for ECDH.
// CTAP requests -25 which represents ECDH-ES + HKDF-256 here:
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
const ECDH_ALGORITHM: i64 = -25;
// The parameter behind map key 1.
const EC2_KEY_TYPE: i64 = 2;
// The parameter behind map key -1.
const P_256_CURVE: i64 = 1;
}
// This conversion accepts both ECDH and ECDSA.
impl TryFrom<cbor::Value> for CoseKey {
type Error = Ctap2StatusCode; type Error = Ctap2StatusCode;
fn try_from(cose_key: CoseKey) -> Result<Self, Ctap2StatusCode> { fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! { destructure_cbor_map! {
let { let {
// This is sorted correctly, negative encoding is bigger.
1 => key_type, 1 => key_type,
3 => algorithm, 3 => algorithm,
-1 => curve, -1 => curve,
-2 => x_bytes, -2 => x_bytes,
-3 => y_bytes, -3 => y_bytes,
} = cose_key.0; } = extract_map(cbor_value)?;
} }
let key_type = extract_integer(ok_or_missing(key_type)?)?;
if key_type != EC2_KEY_TYPE {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let algorithm = extract_integer(ok_or_missing(algorithm)?)?;
if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let curve = extract_integer(ok_or_missing(curve)?)?;
if curve != P_256_CURVE {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let x_bytes = extract_byte_string(ok_or_missing(x_bytes)?)?; let x_bytes = extract_byte_string(ok_or_missing(x_bytes)?)?;
if x_bytes.len() != ecdh::NBYTES { if x_bytes.len() != ecdh::NBYTES {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
@@ -688,10 +665,89 @@ impl TryFrom<CoseKey> for ecdh::PubKey {
if y_bytes.len() != ecdh::NBYTES { if y_bytes.len() != ecdh::NBYTES {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
} }
let curve = extract_integer(ok_or_missing(curve)?)?;
if curve != CoseKey::P_256_CURVE {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let key_type = extract_integer(ok_or_missing(key_type)?)?;
if key_type != CoseKey::EC2_KEY_TYPE {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let algorithm = extract_integer(ok_or_missing(algorithm)?)?;
if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let x_array_ref = array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES]; Ok(CoseKey {
let y_array_ref = array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES]; x_bytes: *array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES],
ecdh::PubKey::from_coordinates(x_array_ref, y_array_ref) y_bytes: *array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES],
algorithm,
})
}
}
impl From<CoseKey> for cbor::Value {
fn from(cose_key: CoseKey) -> Self {
let CoseKey {
x_bytes,
y_bytes,
algorithm,
} = cose_key;
cbor_map! {
1 => CoseKey::EC2_KEY_TYPE,
3 => algorithm,
-1 => CoseKey::P_256_CURVE,
-2 => x_bytes,
-3 => y_bytes,
}
}
}
impl From<ecdh::PubKey> for CoseKey {
fn from(pk: ecdh::PubKey) -> Self {
let mut x_bytes = [0; ecdh::NBYTES];
let mut y_bytes = [0; ecdh::NBYTES];
pk.to_coordinates(&mut x_bytes, &mut y_bytes);
CoseKey {
x_bytes,
y_bytes,
algorithm: CoseKey::ECDH_ALGORITHM,
}
}
}
impl From<ecdsa::PubKey> for CoseKey {
fn from(pk: ecdsa::PubKey) -> Self {
let mut x_bytes = [0; ecdh::NBYTES];
let mut y_bytes = [0; ecdh::NBYTES];
pk.to_coordinates(&mut x_bytes, &mut y_bytes);
CoseKey {
x_bytes,
y_bytes,
algorithm: ES256_ALGORITHM,
}
}
}
impl TryFrom<CoseKey> for ecdh::PubKey {
type Error = Ctap2StatusCode;
fn try_from(cose_key: CoseKey) -> Result<Self, Ctap2StatusCode> {
let CoseKey {
x_bytes,
y_bytes,
algorithm,
} = cose_key;
// Since algorithm can be used for different COSE key types, we check
// whether the current type is correct for ECDH. For an OpenSSH bugfix,
// the algorithm ES256_ALGORITHM is allowed here too.
// https://github.com/google/OpenSK/issues/90
if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes)
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
} }
} }
@@ -704,13 +760,8 @@ pub enum ClientPinSubCommand {
SetPin = 0x03, SetPin = 0x03,
ChangePin = 0x04, ChangePin = 0x04,
GetPinToken = 0x05, GetPinToken = 0x05,
#[cfg(feature = "with_ctap2_1")]
GetPinUvAuthTokenUsingUvWithPermissions = 0x06, GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
#[cfg(feature = "with_ctap2_1")]
GetUvRetries = 0x07, GetUvRetries = 0x07,
#[cfg(feature = "with_ctap2_1")]
SetMinPinLength = 0x08,
#[cfg(feature = "with_ctap2_1")]
GetPinUvAuthTokenUsingPinWithPermissions = 0x09, GetPinUvAuthTokenUsingPinWithPermissions = 0x09,
} }
@@ -731,18 +782,112 @@ impl TryFrom<cbor::Value> for ClientPinSubCommand {
0x03 => Ok(ClientPinSubCommand::SetPin), 0x03 => Ok(ClientPinSubCommand::SetPin),
0x04 => Ok(ClientPinSubCommand::ChangePin), 0x04 => Ok(ClientPinSubCommand::ChangePin),
0x05 => Ok(ClientPinSubCommand::GetPinToken), 0x05 => Ok(ClientPinSubCommand::GetPinToken),
#[cfg(feature = "with_ctap2_1")]
0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions),
#[cfg(feature = "with_ctap2_1")]
0x07 => Ok(ClientPinSubCommand::GetUvRetries), 0x07 => Ok(ClientPinSubCommand::GetUvRetries),
#[cfg(feature = "with_ctap2_1")]
0x08 => Ok(ClientPinSubCommand::SetMinPinLength),
#[cfg(feature = "with_ctap2_1")]
0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions),
#[cfg(feature = "with_ctap2_1")]
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND),
#[cfg(not(feature = "with_ctap2_1"))] }
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), }
}
#[derive(Clone, Copy)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
#[cfg_attr(test, derive(IntoEnumIterator))]
pub enum ConfigSubCommand {
EnableEnterpriseAttestation = 0x01,
ToggleAlwaysUv = 0x02,
SetMinPinLength = 0x03,
VendorPrototype = 0xFF,
}
impl From<ConfigSubCommand> for cbor::Value {
fn from(subcommand: ConfigSubCommand) -> Self {
(subcommand as u64).into()
}
}
impl TryFrom<cbor::Value> for ConfigSubCommand {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let subcommand_int = extract_unsigned(cbor_value)?;
match subcommand_int {
0x01 => Ok(ConfigSubCommand::EnableEnterpriseAttestation),
0x02 => Ok(ConfigSubCommand::ToggleAlwaysUv),
0x03 => Ok(ConfigSubCommand::SetMinPinLength),
0xFF => Ok(ConfigSubCommand::VendorPrototype),
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND),
}
}
}
#[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub enum ConfigSubCommandParams {
SetMinPinLength(SetMinPinLengthParams),
}
impl From<ConfigSubCommandParams> for cbor::Value {
fn from(params: ConfigSubCommandParams) -> Self {
match params {
ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params) => {
set_min_pin_length_params.into()
}
}
}
}
#[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct SetMinPinLengthParams {
pub new_min_pin_length: Option<u8>,
pub min_pin_length_rp_ids: Option<Vec<String>>,
pub force_change_pin: Option<bool>,
}
impl TryFrom<cbor::Value> for SetMinPinLengthParams {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => new_min_pin_length,
0x02 => min_pin_length_rp_ids,
0x03 => force_change_pin,
} = extract_map(cbor_value)?;
}
let new_min_pin_length = new_min_pin_length
.map(extract_unsigned)
.transpose()?
.map(u8::try_from)
.transpose()
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
let min_pin_length_rp_ids = match min_pin_length_rp_ids {
Some(entry) => Some(
extract_array(entry)?
.into_iter()
.map(extract_text_string)
.collect::<Result<Vec<String>, Ctap2StatusCode>>()?,
),
None => None,
};
let force_change_pin = force_change_pin.map(extract_bool).transpose()?;
Ok(Self {
new_min_pin_length,
min_pin_length_rp_ids,
force_change_pin,
})
}
}
impl From<SetMinPinLengthParams> for cbor::Value {
fn from(params: SetMinPinLengthParams) -> Self {
cbor_map_options! {
0x01 => params.new_min_pin_length.map(|u| u as u64),
0x02 => params.min_pin_length_rp_ids.map(|vec| cbor_array_vec!(vec)),
0x03 => params.force_change_pin,
} }
} }
} }
@@ -816,8 +961,8 @@ mod test {
use super::*; use super::*;
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use cbor::{ use cbor::{
cbor_array, cbor_bool, cbor_bytes, cbor_false, cbor_int, cbor_map, cbor_null, cbor_text, cbor_array, cbor_bool, cbor_bytes, cbor_bytes_lit, cbor_false, cbor_int, cbor_null,
cbor_unsigned, cbor_text, cbor_unsigned,
}; };
use crypto::rng256::{Rng256, ThreadRng256}; use crypto::rng256::{Rng256, ThreadRng256};
@@ -1140,7 +1285,7 @@ mod test {
#[test] #[test]
fn test_from_into_signature_algorithm() { fn test_from_into_signature_algorithm() {
let cbor_signature_algorithm: cbor::Value = cbor_int!(ecdsa::PubKey::ES256_ALGORITHM); let cbor_signature_algorithm: cbor::Value = cbor_int!(ES256_ALGORITHM);
let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone());
let expected_signature_algorithm = SignatureAlgorithm::ES256; let expected_signature_algorithm = SignatureAlgorithm::ES256;
assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); assert_eq!(signature_algorithm, Ok(expected_signature_algorithm));
@@ -1214,7 +1359,7 @@ mod test {
fn test_from_into_public_key_credential_parameter() { fn test_from_into_public_key_credential_parameter() {
let cbor_credential_parameter = cbor_map! { let cbor_credential_parameter = cbor_map! {
"type" => "public-key", "type" => "public-key",
"alg" => ecdsa::PubKey::ES256_ALGORITHM, "alg" => ES256_ALGORITHM,
}; };
let credential_parameter = let credential_parameter =
PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone());
@@ -1251,11 +1396,13 @@ mod test {
let cbor_extensions = cbor_map! { let cbor_extensions = cbor_map! {
"hmac-secret" => true, "hmac-secret" => true,
"credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired,
"minPinLength" => true,
}; };
let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let extensions = MakeCredentialExtensions::try_from(cbor_extensions);
let expected_extensions = MakeCredentialExtensions { let expected_extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
min_pin_length: true,
}; };
assert_eq!(extensions, Ok(expected_extensions)); assert_eq!(extensions, Ok(expected_extensions));
} }
@@ -1268,7 +1415,7 @@ mod test {
let cose_key = CoseKey::from(pk); let cose_key = CoseKey::from(pk);
let cbor_extensions = cbor_map! { let cbor_extensions = cbor_map! {
"hmac-secret" => cbor_map! { "hmac-secret" => cbor_map! {
1 => cbor::Value::Map(cose_key.0.clone()), 1 => cbor::Value::from(cose_key.clone()),
2 => vec![0x02; 32], 2 => vec![0x02; 32],
3 => vec![0x03; 16], 3 => vec![0x03; 16],
}, },
@@ -1333,7 +1480,103 @@ mod test {
} }
#[test] #[test]
fn test_from_into_cose_key() { fn test_from_into_cose_key_cbor() {
for algorithm in &[CoseKey::ECDH_ALGORITHM, ES256_ALGORITHM] {
let cbor_value = cbor_map! {
1 => CoseKey::EC2_KEY_TYPE,
3 => algorithm,
-1 => CoseKey::P_256_CURVE,
-2 => [0u8; 32],
-3 => [0u8; 32],
};
let cose_key = CoseKey::try_from(cbor_value.clone()).unwrap();
let created_cbor_value = cbor::Value::from(cose_key);
assert_eq!(created_cbor_value, cbor_value);
}
}
#[test]
fn test_cose_key_unknown_algorithm() {
let cbor_value = cbor_map! {
1 => CoseKey::EC2_KEY_TYPE,
// unknown algorithm
3 => 0,
-1 => CoseKey::P_256_CURVE,
-2 => [0u8; 32],
-3 => [0u8; 32],
};
assert_eq!(
CoseKey::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM)
);
}
#[test]
fn test_cose_key_unknown_type() {
let cbor_value = cbor_map! {
// unknown type
1 => 0,
3 => CoseKey::ECDH_ALGORITHM,
-1 => CoseKey::P_256_CURVE,
-2 => [0u8; 32],
-3 => [0u8; 32],
};
assert_eq!(
CoseKey::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM)
);
}
#[test]
fn test_cose_key_unknown_curve() {
let cbor_value = cbor_map! {
1 => CoseKey::EC2_KEY_TYPE,
3 => CoseKey::ECDH_ALGORITHM,
// unknown curve
-1 => 0,
-2 => [0u8; 32],
-3 => [0u8; 32],
};
assert_eq!(
CoseKey::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM)
);
}
#[test]
fn test_cose_key_wrong_length_x() {
let cbor_value = cbor_map! {
1 => CoseKey::EC2_KEY_TYPE,
3 => CoseKey::ECDH_ALGORITHM,
-1 => CoseKey::P_256_CURVE,
// wrong length
-2 => [0u8; 31],
-3 => [0u8; 32],
};
assert_eq!(
CoseKey::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
#[test]
fn test_cose_key_wrong_length_y() {
let cbor_value = cbor_map! {
1 => CoseKey::EC2_KEY_TYPE,
3 => CoseKey::ECDH_ALGORITHM,
-1 => CoseKey::P_256_CURVE,
-2 => [0u8; 32],
// wrong length
-3 => [0u8; 33],
};
assert_eq!(
CoseKey::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
#[test]
fn test_from_into_cose_key_ecdh() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let sk = crypto::ecdh::SecKey::gensk(&mut rng); let sk = crypto::ecdh::SecKey::gensk(&mut rng);
let pk = sk.genpk(); let pk = sk.genpk();
@@ -1342,6 +1585,15 @@ mod test {
assert_eq!(created_pk, Ok(pk)); assert_eq!(created_pk, Ok(pk));
} }
#[test]
fn test_into_cose_key_ecdsa() {
let mut rng = ThreadRng256 {};
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
assert_eq!(cose_key.algorithm, ES256_ALGORITHM);
}
#[test] #[test]
fn test_from_into_client_pin_sub_command() { fn test_from_into_client_pin_sub_command() {
let cbor_sub_command: cbor::Value = cbor_int!(0x01); let cbor_sub_command: cbor::Value = cbor_int!(0x01);
@@ -1358,6 +1610,56 @@ mod test {
} }
} }
#[test]
fn test_from_into_config_sub_command() {
let cbor_sub_command: cbor::Value = cbor_int!(0x01);
let sub_command = ConfigSubCommand::try_from(cbor_sub_command.clone());
let expected_sub_command = ConfigSubCommand::EnableEnterpriseAttestation;
assert_eq!(sub_command, Ok(expected_sub_command));
let created_cbor: cbor::Value = sub_command.unwrap().into();
assert_eq!(created_cbor, cbor_sub_command);
for command in ConfigSubCommand::into_enum_iter() {
let created_cbor: cbor::Value = command.clone().into();
let reconstructed = ConfigSubCommand::try_from(created_cbor).unwrap();
assert_eq!(command, reconstructed);
}
}
#[test]
fn test_from_set_min_pin_length_params() {
let params = SetMinPinLengthParams {
new_min_pin_length: Some(6),
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
force_change_pin: Some(true),
};
let cbor_params = cbor_map! {
0x01 => 6,
0x02 => cbor_array_vec!(vec!["example.com".to_string()]),
0x03 => true,
};
assert_eq!(cbor::Value::from(params.clone()), cbor_params);
let reconstructed_params = SetMinPinLengthParams::try_from(cbor_params);
assert_eq!(reconstructed_params, Ok(params));
}
#[test]
fn test_from_config_sub_command_params() {
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(6),
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
force_change_pin: Some(true),
};
let config_sub_command_params =
ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params);
let cbor_params = cbor_map! {
0x01 => 6,
0x02 => cbor_array_vec!(vec!["example.com".to_string()]),
0x03 => true,
};
assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params);
}
#[test] #[test]
fn test_credential_source_cbor_round_trip() { fn test_credential_source_cbor_round_trip() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -68,8 +68,8 @@ pub struct CtapHid {
// vendor specific. // vendor specific.
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are // We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
// allocated. // allocated.
// In packets, the ids are then encoded with the native endianness (with the // In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the
// u32::to/from_ne_bytes methods). // u32::to/from_be_bytes methods).
allocated_cids: usize, allocated_cids: usize,
pub wink_permission: TimedPermission, pub wink_permission: TimedPermission,
} }
@@ -117,9 +117,8 @@ impl CtapHid {
// CTAP specification (version 20190130) section 8.1.9.1.3 // CTAP specification (version 20190130) section 8.1.9.1.3
const PROTOCOL_VERSION: u8 = 2; const PROTOCOL_VERSION: u8 = 2;
// The device version number is vendor-defined. For now we define them to be zero. // The device version number is vendor-defined.
// TODO: Update with device version? const DEVICE_VERSION_MAJOR: u8 = 1;
const DEVICE_VERSION_MAJOR: u8 = 0;
const DEVICE_VERSION_MINOR: u8 = 0; const DEVICE_VERSION_MINOR: u8 = 0;
const DEVICE_VERSION_BUILD: u8 = 0; const DEVICE_VERSION_BUILD: u8 = 0;
@@ -220,7 +219,7 @@ impl CtapHid {
cid, cid,
cmd: CtapHid::COMMAND_CBOR, cmd: CtapHid::COMMAND_CBOR,
payload: vec![ payload: vec![
Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG as u8, Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8,
], ],
}) })
.unwrap() .unwrap()
@@ -235,7 +234,7 @@ impl CtapHid {
let new_cid = if cid == CtapHid::CHANNEL_BROADCAST { let new_cid = if cid == CtapHid::CHANNEL_BROADCAST {
// TODO: Prevent allocating 2^32 channels. // TODO: Prevent allocating 2^32 channels.
self.allocated_cids += 1; self.allocated_cids += 1;
(self.allocated_cids as u32).to_ne_bytes() (self.allocated_cids as u32).to_be_bytes()
} else { } else {
// Sync the channel and discard the current transaction. // Sync the channel and discard the current transaction.
cid cid
@@ -342,7 +341,7 @@ impl CtapHid {
} }
fn is_allocated_channel(&self, cid: ChannelID) -> bool { fn is_allocated_channel(&self, cid: ChannelID) -> bool {
cid != CtapHid::CHANNEL_RESERVED && u32::from_ne_bytes(cid) as usize <= self.allocated_cids cid != CtapHid::CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids
} }
fn error_message(cid: ChannelID, error_code: u8) -> HidPacketIterator { fn error_message(cid: ChannelID, error_code: u8) -> HidPacketIterator {
@@ -417,7 +416,7 @@ impl CtapHid {
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator { fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator {
let mut response = payload.to_vec(); let mut response = payload.to_vec();
let code: u16 = ctap1::Ctap1StatusCode::SW_NO_ERROR.into(); let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into();
response.extend_from_slice(&code.to_be_bytes()); response.extend_from_slice(&code.to_be_bytes());
CtapHid::split_message(Message { CtapHid::split_message(Message {
cid, cid,
@@ -569,12 +568,12 @@ mod test {
0xBC, 0xBC,
0xDE, 0xDE,
0xF0, 0xF0,
0x01, // Allocated CID 0x00, // Allocated CID
0x00,
0x00, 0x00,
0x00, 0x00,
0x01,
0x02, // Protocol version 0x02, // Protocol version
0x00, // Device version 0x01, // Device version
0x00, 0x00,
0x00, 0x00,
CtapHid::CAPABILITIES CtapHid::CAPABILITIES
@@ -634,7 +633,7 @@ mod test {
cid[2], cid[2],
cid[3], cid[3],
0x02, // Protocol version 0x02, // Protocol version
0x00, // Device version 0x01, // Device version
0x00, 0x00,
0x00, 0x00,
CtapHid::CAPABILITIES CtapHid::CAPABILITIES

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -17,9 +17,3 @@ pub const AAGUID_LENGTH: usize = 16;
pub const AAGUID: &[u8; AAGUID_LENGTH] = pub const AAGUID: &[u8; AAGUID_LENGTH] =
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); 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"));

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2020 Google LLC // Copyright 2020-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@ use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretIn
use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::response::{AuthenticatorClientPinResponse, ResponseData};
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore; use super::storage::PersistentStore;
#[cfg(feature = "with_ctap2_1")] use alloc::str;
use alloc::string::String; use alloc::string::String;
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -28,7 +28,7 @@ use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits};
use crypto::rng256::Rng256; use crypto::rng256::Rng256;
use crypto::sha256::Sha256; use crypto::sha256::Sha256;
use crypto::Hash256; use crypto::Hash256;
#[cfg(all(test, feature = "with_ctap2_1"))] #[cfg(test)]
use enum_iterator::IntoEnumIterator; use enum_iterator::IntoEnumIterator;
use subtle::ConstantTimeEq; use subtle::ConstantTimeEq;
@@ -59,7 +59,7 @@ fn encrypt_hmac_secret_output(
cred_random: &[u8; 32], cred_random: &[u8; 32],
) -> Result<Vec<u8>, Ctap2StatusCode> { ) -> Result<Vec<u8>, Ctap2StatusCode> {
if salt_enc.len() != 32 && salt_enc.len() != 64 { if salt_enc.len() != 32 && salt_enc.len() != 64 {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
} }
let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret);
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
@@ -141,21 +141,18 @@ fn check_and_store_new_pin(
let pin = decrypt_pin(aes_dec_key, new_pin_enc) let pin = decrypt_pin(aes_dec_key, new_pin_enc)
.ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
#[cfg(feature = "with_ctap2_1")]
let min_pin_length = persistent_store.min_pin_length()? as usize; let min_pin_length = persistent_store.min_pin_length()? as usize;
#[cfg(not(feature = "with_ctap2_1"))] let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count();
let min_pin_length = 4; if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH {
if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH {
// TODO(kaczmarczyck) check 4 code point minimum instead
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
} }
let mut pin_hash = [0u8; 16]; let mut pin_hash = [0u8; 16];
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]);
persistent_store.set_pin_hash(&pin_hash)?; // The PIN length is always < 64.
persistent_store.set_pin(&pin_hash, pin_length as u8)?;
Ok(()) Ok(())
} }
#[cfg(feature = "with_ctap2_1")]
#[cfg_attr(test, derive(IntoEnumIterator))] #[cfg_attr(test, derive(IntoEnumIterator))]
// TODO remove when all variants are used // TODO remove when all variants are used
#[allow(dead_code)] #[allow(dead_code)]
@@ -173,9 +170,7 @@ pub struct PinProtocolV1 {
key_agreement_key: crypto::ecdh::SecKey, key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
consecutive_pin_mismatches: u8, consecutive_pin_mismatches: u8,
#[cfg(feature = "with_ctap2_1")]
permissions: u8, permissions: u8,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id: Option<String>, permissions_rp_id: Option<String>,
} }
@@ -187,9 +182,7 @@ impl PinProtocolV1 {
key_agreement_key, key_agreement_key,
pin_uv_auth_token, pin_uv_auth_token,
consecutive_pin_mismatches: 0, consecutive_pin_mismatches: 0,
#[cfg(feature = "with_ctap2_1")]
permissions: 0, permissions: 0,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id: None, permissions_rp_id: None,
} }
} }
@@ -232,7 +225,7 @@ impl PinProtocolV1 {
} }
} }
// This status code is not explicitly mentioned in the specification. // This status code is not explicitly mentioned in the specification.
None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED), None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED),
} }
persistent_store.reset_pin_retries()?; persistent_store.reset_pin_retries()?;
self.consecutive_pin_mismatches = 0; self.consecutive_pin_mismatches = 0;
@@ -345,11 +338,8 @@ impl PinProtocolV1 {
cbc_encrypt(&token_encryption_key, iv, &mut blocks); cbc_encrypt(&token_encryption_key, iv, &mut blocks);
let pin_token: Vec<u8> = blocks.iter().flatten().cloned().collect(); let pin_token: Vec<u8> = blocks.iter().flatten().cloned().collect();
#[cfg(feature = "with_ctap2_1")] self.permissions = 0x03;
{ self.permissions_rp_id = None;
self.permissions = 0x03;
self.permissions_rp_id = None;
}
Ok(AuthenticatorClientPinResponse { Ok(AuthenticatorClientPinResponse {
key_agreement: None, key_agreement: None,
@@ -358,7 +348,6 @@ impl PinProtocolV1 {
}) })
} }
#[cfg(feature = "with_ctap2_1")]
fn process_get_pin_uv_auth_token_using_uv_with_permissions( fn process_get_pin_uv_auth_token_using_uv_with_permissions(
&self, &self,
// If you want to support local user verification, implement this function. // If you want to support local user verification, implement this function.
@@ -368,79 +357,14 @@ impl PinProtocolV1 {
_permissions_rp_id: Option<String>, _permissions_rp_id: Option<String>,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> { ) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
// User verifications is only supported through PIN currently. // User verifications is only supported through PIN currently.
#[cfg(not(feature = "with_ctap2_1"))] Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
{
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)
}
#[cfg(feature = "with_ctap2_1")]
{
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
}
} }
#[cfg(feature = "with_ctap2_1")]
fn process_get_uv_retries(&self) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> { fn process_get_uv_retries(&self) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
// User verifications is only supported through PIN currently. // User verifications is only supported through PIN currently.
#[cfg(not(feature = "with_ctap2_1"))] Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
{
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)
}
#[cfg(feature = "with_ctap2_1")]
{
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
}
} }
#[cfg(feature = "with_ctap2_1")]
fn process_set_min_pin_length(
&mut self,
persistent_store: &mut PersistentStore,
min_pin_length: u8,
min_pin_length_rp_ids: Option<Vec<String>>,
pin_auth: Option<Vec<u8>>,
) -> Result<(), Ctap2StatusCode> {
if min_pin_length_rp_ids.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
if persistent_store.pin_hash()?.is_some() {
match pin_auth {
Some(pin_auth) => {
if self.consecutive_pin_mismatches >= 3 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
}
// TODO(kaczmarczyck) Values are taken from the (not yet public) new revision
// of CTAP 2.1. The code should link the specification when published.
// From CTAP2.1: "If request contains pinUvAuthParam, the Authenticator calls
// verify(pinUvAuthToken, 32×0xff || 0x0608 || uint32LittleEndian(minPINLength)
// || minPinLengthRPIDs, pinUvAuthParam)"
let mut message = vec![0xFF; 32];
message.extend(&[0x06, 0x08]);
message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]);
// TODO(kaczmarczyck) commented code is useful for the extension
// https://github.com/google/OpenSK/issues/129
// if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) {
// return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
// }
if !verify_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
}
None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
};
}
if min_pin_length < persistent_store.min_pin_length()? {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
persistent_store.set_min_pin_length(min_pin_length)?;
// TODO(kaczmarczyck) commented code is useful for the extension
// https://github.com/google/OpenSK/issues/129
// if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids {
// persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?;
// }
Ok(())
}
#[cfg(feature = "with_ctap2_1")]
fn process_get_pin_uv_auth_token_using_pin_with_permissions( fn process_get_pin_uv_auth_token_using_pin_with_permissions(
&mut self, &mut self,
rng: &mut impl Rng256, rng: &mut impl Rng256,
@@ -480,20 +404,11 @@ impl PinProtocolV1 {
pin_auth, pin_auth,
new_pin_enc, new_pin_enc,
pin_hash_enc, pin_hash_enc,
#[cfg(feature = "with_ctap2_1")]
min_pin_length,
#[cfg(feature = "with_ctap2_1")]
min_pin_length_rp_ids,
#[cfg(feature = "with_ctap2_1")]
permissions, permissions,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id, permissions_rp_id,
} = client_pin_params; } = client_pin_params;
if pin_protocol != 1 { if pin_protocol != 1 {
#[cfg(not(feature = "with_ctap2_1"))]
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
#[cfg(feature = "with_ctap2_1")]
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
} }
@@ -528,7 +443,6 @@ impl PinProtocolV1 {
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?), )?),
#[cfg(feature = "with_ctap2_1")]
ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some(
self.process_get_pin_uv_auth_token_using_uv_with_permissions( self.process_get_pin_uv_auth_token_using_uv_with_permissions(
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
@@ -536,19 +450,7 @@ impl PinProtocolV1 {
permissions_rp_id, permissions_rp_id,
)?, )?,
), ),
#[cfg(feature = "with_ctap2_1")]
ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?),
#[cfg(feature = "with_ctap2_1")]
ClientPinSubCommand::SetMinPinLength => {
self.process_set_min_pin_length(
persistent_store,
min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
min_pin_length_rp_ids,
pin_auth,
)?;
None
}
#[cfg(feature = "with_ctap2_1")]
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some(
self.process_get_pin_uv_auth_token_using_pin_with_permissions( self.process_get_pin_uv_auth_token_using_pin_with_permissions(
rng, rng,
@@ -571,11 +473,8 @@ impl PinProtocolV1 {
self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
self.pin_uv_auth_token = rng.gen_uniform_u8x32(); self.pin_uv_auth_token = rng.gen_uniform_u8x32();
self.consecutive_pin_mismatches = 0; self.consecutive_pin_mismatches = 0;
#[cfg(feature = "with_ctap2_1")] self.permissions = 0;
{ self.permissions_rp_id = None;
self.permissions = 0;
self.permissions_rp_id = None;
}
} }
pub fn process_hmac_secret( pub fn process_hmac_secret(
@@ -593,12 +492,11 @@ impl PinProtocolV1 {
// HMAC-secret does the same 16 byte truncated check. // HMAC-secret does the same 16 byte truncated check.
if !verify_pin_auth(&shared_secret, &salt_enc, &salt_auth) { if !verify_pin_auth(&shared_secret, &salt_enc, &salt_auth) {
// Hard to tell what the correct error code here is. // Hard to tell what the correct error code here is.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
} }
encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random)
} }
#[cfg(feature = "with_ctap2_1")]
pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> {
// Relies on the fact that all permissions are represented by powers of two. // Relies on the fact that all permissions are represented by powers of two.
if permission as u8 & self.permissions != 0 { if permission as u8 & self.permissions != 0 {
@@ -608,7 +506,6 @@ impl PinProtocolV1 {
} }
} }
#[cfg(feature = "with_ctap2_1")]
pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
if let Some(permissions_rp_id) = &self.permissions_rp_id { if let Some(permissions_rp_id) = &self.permissions_rp_id {
if rp_id != permissions_rp_id { if rp_id != permissions_rp_id {
@@ -623,15 +520,13 @@ impl PinProtocolV1 {
#[cfg(test)] #[cfg(test)]
pub fn new_test( pub fn new_test(
key_agreement_key: crypto::ecdh::SecKey, key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; 32], pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
) -> PinProtocolV1 { ) -> PinProtocolV1 {
PinProtocolV1 { PinProtocolV1 {
key_agreement_key, key_agreement_key,
pin_uv_auth_token, pin_uv_auth_token,
consecutive_pin_mismatches: 0, consecutive_pin_mismatches: 0,
#[cfg(feature = "with_ctap2_1")]
permissions: 0xFF, permissions: 0xFF,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id: None, permissions_rp_id: None,
} }
} }
@@ -648,7 +543,7 @@ mod test {
pin[..4].copy_from_slice(b"1234"); pin[..4].copy_from_slice(b"1234");
let mut pin_hash = [0u8; 16]; let mut pin_hash = [0u8; 16];
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]);
persistent_store.set_pin_hash(&pin_hash).unwrap(); persistent_store.set_pin(&pin_hash, 4).unwrap();
} }
// Encrypts the message with a zero IV and key derived from shared_secret. // Encrypts the message with a zero IV and key derived from shared_secret.
@@ -710,7 +605,7 @@ mod test {
0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36,
0xC4, 0x12, 0xC4, 0x12,
]; ];
persistent_store.set_pin_hash(&pin_hash).unwrap(); persistent_store.set_pin(&pin_hash, 4).unwrap();
let shared_secret = [0x88; 32]; let shared_secret = [0x88; 32];
let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret);
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
@@ -919,7 +814,6 @@ mod test {
); );
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -963,7 +857,7 @@ mod test {
&mut rng, &mut rng,
&mut persistent_store, &mut persistent_store,
key_agreement.clone(), key_agreement.clone(),
pin_hash_enc.clone(), pin_hash_enc,
0x03, 0x03,
None, None,
), ),
@@ -984,41 +878,6 @@ mod test {
); );
} }
#[cfg(feature = "with_ctap2_1")]
#[test]
fn test_process_set_min_pin_length() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng);
let min_pin_length = 8;
pin_protocol_v1.pin_uv_auth_token = [0x55; PIN_TOKEN_LENGTH];
let pin_auth = vec![
0x94, 0x86, 0xEF, 0x4C, 0xB3, 0x84, 0x2C, 0x85, 0x72, 0x02, 0xBF, 0xE4, 0x36, 0x22,
0xFE, 0xC9,
];
// TODO(kaczmarczyck) implement test for the min PIN length extension
// https://github.com/google/OpenSK/issues/129
let response = pin_protocol_v1.process_set_min_pin_length(
&mut persistent_store,
min_pin_length,
None,
Some(pin_auth.clone()),
);
assert_eq!(response, Ok(()));
assert_eq!(persistent_store.min_pin_length().unwrap(), min_pin_length);
let response = pin_protocol_v1.process_set_min_pin_length(
&mut persistent_store,
7,
None,
Some(pin_auth),
);
assert_eq!(
response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
);
assert_eq!(persistent_store.min_pin_length().unwrap(), min_pin_length);
}
#[test] #[test]
fn test_process() { fn test_process() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1031,13 +890,7 @@ mod test {
pin_auth: None, pin_auth: None,
new_pin_enc: None, new_pin_enc: None,
pin_hash_enc: None, pin_hash_enc: None,
#[cfg(feature = "with_ctap2_1")]
min_pin_length: None,
#[cfg(feature = "with_ctap2_1")]
min_pin_length_rp_ids: None,
#[cfg(feature = "with_ctap2_1")]
permissions: None, permissions: None,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id: None, permissions_rp_id: None,
}; };
assert!(pin_protocol_v1 assert!(pin_protocol_v1
@@ -1051,18 +904,9 @@ mod test {
pin_auth: None, pin_auth: None,
new_pin_enc: None, new_pin_enc: None,
pin_hash_enc: None, pin_hash_enc: None,
#[cfg(feature = "with_ctap2_1")]
min_pin_length: None,
#[cfg(feature = "with_ctap2_1")]
min_pin_length_rp_ids: None,
#[cfg(feature = "with_ctap2_1")]
permissions: None, permissions: None,
#[cfg(feature = "with_ctap2_1")]
permissions_rp_id: None, permissions_rp_id: None,
}; };
#[cfg(not(feature = "with_ctap2_1"))]
let error_code = Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID;
#[cfg(feature = "with_ctap2_1")]
let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER;
assert_eq!( assert_eq!(
pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params), pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params),
@@ -1174,10 +1018,7 @@ mod test {
let salt_enc = [0x5E; 48]; let salt_enc = [0x5E; 48];
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
assert_eq!( assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER));
output,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
let salt_enc = [0x5E; 64]; let salt_enc = [0x5E; 64];
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
@@ -1234,7 +1075,6 @@ mod test {
assert_eq!(&output_dec[..32], &expected_output1); assert_eq!(&output_dec[..32], &expected_output1);
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_has_permission() { fn test_has_permission() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1252,7 +1092,6 @@ mod test {
} }
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_has_permission_for_rp_id() { fn test_has_permission_for_rp_id() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -12,11 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
#[cfg(feature = "with_ctap2_1")]
use super::data_formats::{AuthenticatorTransport, PublicKeyCredentialParameter};
use super::data_formats::{ use super::data_formats::{
CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement,
PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity,
}; };
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use alloc::string::String; use alloc::string::String;
@@ -32,8 +30,10 @@ pub enum ResponseData {
AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>), AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
AuthenticatorReset, AuthenticatorReset,
#[cfg(feature = "with_ctap2_1")]
AuthenticatorSelection, AuthenticatorSelection,
// TODO(kaczmarczyck) dummy, extend
AuthenticatorConfig,
AuthenticatorVendor(AuthenticatorVendorResponse),
} }
impl From<ResponseData> for Option<cbor::Value> { impl From<ResponseData> for Option<cbor::Value> {
@@ -46,8 +46,9 @@ impl From<ResponseData> for Option<cbor::Value> {
ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()),
ResponseData::AuthenticatorClientPin(None) => None, ResponseData::AuthenticatorClientPin(None) => None,
ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorReset => None,
#[cfg(feature = "with_ctap2_1")]
ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorConfig => None,
ResponseData::AuthenticatorVendor(data) => Some(data.into()),
} }
} }
} }
@@ -109,30 +110,25 @@ impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
pub struct AuthenticatorGetInfoResponse { pub struct AuthenticatorGetInfoResponse {
// TODO(kaczmarczyck) add maxAuthenticatorConfigLength and defaultCredProtect
pub versions: Vec<String>, pub versions: Vec<String>,
pub extensions: Option<Vec<String>>, pub extensions: Option<Vec<String>>,
pub aaguid: [u8; 16], pub aaguid: [u8; 16],
pub options: Option<BTreeMap<String, bool>>, pub options: Option<BTreeMap<String, bool>>,
pub max_msg_size: Option<u64>, pub max_msg_size: Option<u64>,
pub pin_protocols: Option<Vec<u64>>, pub pin_protocols: Option<Vec<u64>>,
#[cfg(feature = "with_ctap2_1")]
pub max_credential_count_in_list: Option<u64>, pub max_credential_count_in_list: Option<u64>,
#[cfg(feature = "with_ctap2_1")]
pub max_credential_id_length: Option<u64>, pub max_credential_id_length: Option<u64>,
#[cfg(feature = "with_ctap2_1")]
pub transports: Option<Vec<AuthenticatorTransport>>, pub transports: Option<Vec<AuthenticatorTransport>>,
#[cfg(feature = "with_ctap2_1")]
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>, pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
pub default_cred_protect: Option<CredentialProtectionPolicy>, pub default_cred_protect: Option<CredentialProtectionPolicy>,
#[cfg(feature = "with_ctap2_1")]
pub min_pin_length: u8, pub min_pin_length: u8,
#[cfg(feature = "with_ctap2_1")]
pub firmware_version: Option<u64>, pub firmware_version: Option<u64>,
pub max_cred_blob_length: Option<u64>,
pub max_rp_ids_for_set_min_pin_length: Option<u64>,
pub remaining_discoverable_credentials: Option<u64>,
} }
impl From<AuthenticatorGetInfoResponse> for cbor::Value { impl From<AuthenticatorGetInfoResponse> for cbor::Value {
#[cfg(feature = "with_ctap2_1")]
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
let AuthenticatorGetInfoResponse { let AuthenticatorGetInfoResponse {
versions, versions,
@@ -148,6 +144,9 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
default_cred_protect, default_cred_protect,
min_pin_length, min_pin_length,
firmware_version, firmware_version,
max_cred_blob_length,
max_rp_ids_for_set_min_pin_length,
remaining_discoverable_credentials,
} = get_info_response; } = get_info_response;
let options_cbor: Option<cbor::Value> = options.map(|options| { let options_cbor: Option<cbor::Value> = options.map(|options| {
@@ -172,37 +171,9 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
0x0C => default_cred_protect.map(|p| p as u64), 0x0C => default_cred_protect.map(|p| p as u64),
0x0D => min_pin_length as u64, 0x0D => min_pin_length as u64,
0x0E => firmware_version, 0x0E => firmware_version,
} 0x0F => max_cred_blob_length,
} 0x10 => max_rp_ids_for_set_min_pin_length,
0x14 => remaining_discoverable_credentials,
#[cfg(not(feature = "with_ctap2_1"))]
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
let AuthenticatorGetInfoResponse {
versions,
extensions,
aaguid,
options,
max_msg_size,
pin_protocols,
default_cred_protect,
} = get_info_response;
let options_cbor: Option<cbor::Value> = options.map(|options| {
let option_map: BTreeMap<_, _> = options
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_bool!(value)))
.collect();
cbor_map_btree!(option_map)
});
cbor_map_options! {
0x01 => cbor_array_vec!(versions),
0x02 => extensions.map(|vec| cbor_array_vec!(vec)),
0x03 => &aaguid,
0x04 => options_cbor,
0x05 => max_msg_size,
0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)),
0x0C => default_cred_protect.map(|p| p as u64),
} }
} }
} }
@@ -224,17 +195,37 @@ impl From<AuthenticatorClientPinResponse> for cbor::Value {
} = client_pin_response; } = client_pin_response;
cbor_map_options! { cbor_map_options! {
1 => key_agreement.map(|cose_key| cbor_map_btree!(cose_key.0)), 1 => key_agreement.map(cbor::Value::from),
2 => pin_token, 2 => pin_token,
3 => retries, 3 => retries,
} }
} }
} }
#[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)] #[cfg(test)]
mod test { mod test {
use super::super::data_formats::PackedAttestationStatement; use super::super::data_formats::PackedAttestationStatement;
#[cfg(feature = "with_ctap2_1")]
use super::super::ES256_CRED_PARAM; use super::super::ES256_CRED_PARAM;
use super::*; use super::*;
use cbor::{cbor_bytes, cbor_map}; use cbor::{cbor_bytes, cbor_map};
@@ -298,28 +289,19 @@ mod test {
options: None, options: None,
max_msg_size: None, max_msg_size: None,
pin_protocols: None, pin_protocols: None,
#[cfg(feature = "with_ctap2_1")]
max_credential_count_in_list: None, max_credential_count_in_list: None,
#[cfg(feature = "with_ctap2_1")]
max_credential_id_length: None, max_credential_id_length: None,
#[cfg(feature = "with_ctap2_1")]
transports: None, transports: None,
#[cfg(feature = "with_ctap2_1")]
algorithms: None, algorithms: None,
default_cred_protect: None, default_cred_protect: None,
#[cfg(feature = "with_ctap2_1")]
min_pin_length: 4, min_pin_length: 4,
#[cfg(feature = "with_ctap2_1")]
firmware_version: None, firmware_version: None,
max_cred_blob_length: None,
max_rp_ids_for_set_min_pin_length: None,
remaining_discoverable_credentials: None,
}; };
let response_cbor: Option<cbor::Value> = let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into(); ResponseData::AuthenticatorGetInfo(get_info_response).into();
#[cfg(not(feature = "with_ctap2_1"))]
let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![versions],
0x03 => vec![0x00; 16],
};
#[cfg(feature = "with_ctap2_1")]
let expected_cbor = cbor_map_options! { let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![versions], 0x01 => cbor_array_vec![versions],
0x03 => vec![0x00; 16], 0x03 => vec![0x00; 16],
@@ -329,7 +311,6 @@ mod test {
} }
#[test] #[test]
#[cfg(feature = "with_ctap2_1")]
fn test_get_info_optionals_into_cbor() { fn test_get_info_optionals_into_cbor() {
let mut options_map = BTreeMap::new(); let mut options_map = BTreeMap::new();
options_map.insert(String::from("rk"), true); options_map.insert(String::from("rk"), true);
@@ -347,6 +328,9 @@ mod test {
default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
min_pin_length: 4, min_pin_length: 4,
firmware_version: Some(0), firmware_version: Some(0),
max_cred_blob_length: Some(1024),
max_rp_ids_for_set_min_pin_length: Some(8),
remaining_discoverable_credentials: Some(150),
}; };
let response_cbor: Option<cbor::Value> = let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into(); ResponseData::AuthenticatorGetInfo(get_info_response).into();
@@ -364,6 +348,9 @@ mod test {
0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64,
0x0D => 4, 0x0D => 4,
0x0E => 0, 0x0E => 0,
0x0F => 1024,
0x10 => 8,
0x14 => 150,
}; };
assert_eq!(response_cbor, Some(expected_cbor)); assert_eq!(response_cbor, Some(expected_cbor));
} }
@@ -395,10 +382,45 @@ mod test {
assert_eq!(response_cbor, None); assert_eq!(response_cbor, None);
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_selection_into_cbor() { fn test_selection_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into(); let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into();
assert_eq!(response_cbor, None); assert_eq!(response_cbor, None);
} }
#[test]
fn test_config_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.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

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -31,11 +31,8 @@ pub enum Ctap2StatusCode {
CTAP2_ERR_INVALID_CBOR = 0x12, CTAP2_ERR_INVALID_CBOR = 0x12,
CTAP2_ERR_MISSING_PARAMETER = 0x14, CTAP2_ERR_MISSING_PARAMETER = 0x14,
CTAP2_ERR_LIMIT_EXCEEDED = 0x15, CTAP2_ERR_LIMIT_EXCEEDED = 0x15,
CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_FP_DATABASE_FULL = 0x17, CTAP2_ERR_FP_DATABASE_FULL = 0x17,
#[cfg(feature = "with_ctap2_1")] CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18,
CTAP2_ERR_PC_STORAGE_FULL = 0x18,
CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,
CTAP2_ERR_PROCESSING = 0x21, CTAP2_ERR_PROCESSING = 0x21,
CTAP2_ERR_INVALID_CREDENTIAL = 0x22, CTAP2_ERR_INVALID_CREDENTIAL = 0x22,
@@ -57,25 +54,22 @@ pub enum Ctap2StatusCode {
CTAP2_ERR_PIN_AUTH_INVALID = 0x33, CTAP2_ERR_PIN_AUTH_INVALID = 0x33,
CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34, CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34,
CTAP2_ERR_PIN_NOT_SET = 0x35, CTAP2_ERR_PIN_NOT_SET = 0x35,
CTAP2_ERR_PIN_REQUIRED = 0x36, CTAP2_ERR_PUAT_REQUIRED = 0x36,
CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37, CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37,
CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38, CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38,
CTAP2_ERR_REQUEST_TOO_LARGE = 0x39, CTAP2_ERR_REQUEST_TOO_LARGE = 0x39,
CTAP2_ERR_ACTION_TIMEOUT = 0x3A, CTAP2_ERR_ACTION_TIMEOUT = 0x3A,
CTAP2_ERR_UP_REQUIRED = 0x3B, CTAP2_ERR_UP_REQUIRED = 0x3B,
CTAP2_ERR_UV_BLOCKED = 0x3C, CTAP2_ERR_UV_BLOCKED = 0x3C,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_INTEGRITY_FAILURE = 0x3D, CTAP2_ERR_INTEGRITY_FAILURE = 0x3D,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E, CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E,
CTAP2_ERR_UV_INVALID = 0x3F,
CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40,
CTAP1_ERR_OTHER = 0x7F, CTAP1_ERR_OTHER = 0x7F,
CTAP2_ERR_SPEC_LAST = 0xDF, _CTAP2_ERR_SPEC_LAST = 0xDF,
CTAP2_ERR_EXTENSION_FIRST = 0xE0, _CTAP2_ERR_EXTENSION_FIRST = 0xE0,
CTAP2_ERR_EXTENSION_LAST = 0xEF, _CTAP2_ERR_EXTENSION_LAST = 0xEF,
// CTAP2_ERR_VENDOR_FIRST = 0xF0, _CTAP2_ERR_VENDOR_FIRST = 0xF0,
CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0,
CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1,
/// An internal invariant is broken. /// An internal invariant is broken.
/// ///
/// This type of error is unexpected and the current state is undefined. /// This type of error is unexpected and the current state is undefined.
@@ -85,6 +79,5 @@ pub enum Ctap2StatusCode {
/// ///
/// It may be possible that some of those errors are actually internal errors. /// It may be possible that some of those errors are actually internal errors.
CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3, CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3,
_CTAP2_ERR_VENDOR_LAST = 0xFF,
CTAP2_ERR_VENDOR_LAST = 0xFF,
} }

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -14,20 +14,19 @@
mod key; mod key;
#[cfg(feature = "with_ctap2_1")] use crate::ctap::data_formats::{
use crate::ctap::data_formats::{extract_array, extract_text_string}; extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource,
use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; PublicKeyCredentialUserEntity,
};
use crate::ctap::key_material; use crate::ctap::key_material;
use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH;
use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::INITIAL_SIGNATURE_COUNTER; use crate::ctap::INITIAL_SIGNATURE_COUNTER;
use crate::embedded_flash::{new_storage, Storage}; use crate::embedded_flash::{new_storage, Storage};
#[cfg(feature = "with_ctap2_1")]
use alloc::string::String; use alloc::string::String;
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
use arrayref::array_ref; use arrayref::array_ref;
#[cfg(feature = "with_ctap2_1")]
use cbor::cbor_array_vec; use cbor::cbor_array_vec;
use core::convert::TryInto; use core::convert::TryInto;
use crypto::rng256::Rng256; use crypto::rng256::Rng256;
@@ -39,11 +38,11 @@ use crypto::rng256::Rng256;
// number of pages. This may improve in the future. Currently, using 20 pages gives between 20ms and // number of pages. This may improve in the future. Currently, using 20 pages gives between 20ms and
// 240ms per operation. The rule of thumb is between 1ms and 12ms per additional page. // 240ms per operation. The rule of thumb is between 1ms and 12ms per additional page.
// //
// Limiting the number of residential keys permits to ensure a minimum number of counter increments. // Limiting the number of resident keys permits to ensure a minimum number of counter increments.
// Let: // Let:
// - P the number of pages (NUM_PAGES) // - P the number of pages (NUM_PAGES)
// - K the maximum number of residential keys (MAX_SUPPORTED_RESIDENTIAL_KEYS) // - K the maximum number of resident keys (MAX_SUPPORTED_RESIDENT_KEYS)
// - S the maximum size of a residential key (about 500) // - S the maximum size of a resident key (about 500)
// - C the number of erase cycles (10000) // - C the number of erase cycles (10000)
// - I the minimum number of counter increments // - I the minimum number of counter increments
// //
@@ -51,18 +50,14 @@ use crypto::rng256::Rng256;
// //
// With P=20 and K=150, we have I=2M which is enough for 500 increments per day for 10 years. // With P=20 and K=150, we have I=2M which is enough for 500 increments per day for 10 years.
const NUM_PAGES: usize = 20; const NUM_PAGES: usize = 20;
const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150;
const MAX_PIN_RETRIES: u8 = 8; const MAX_PIN_RETRIES: u8 = 8;
#[cfg(feature = "with_ctap2_1")]
const DEFAULT_MIN_PIN_LENGTH: u8 = 4; const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
// TODO(kaczmarczyck) use this for the minPinLength extension const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new();
// https://github.com/google/OpenSK/issues/129 // This constant is an attempt to limit storage requirements. If you don't set it to 0,
#[cfg(feature = "with_ctap2_1")] // the stored strings can still be unbounded, but that is true for all RP IDs.
const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new(); const MAX_RP_IDS_LENGTH: usize = 8;
// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly.
#[cfg(feature = "with_ctap2_1")]
const _MAX_RP_IDS_LENGTH: usize = 8;
/// Wrapper for master keys. /// Wrapper for master keys.
pub struct MasterKeys { pub struct MasterKeys {
@@ -73,6 +68,15 @@ pub struct MasterKeys {
pub hmac: [u8; 32], pub hmac: [u8; 32],
} }
/// Wrapper for PIN properties.
struct PinProperties {
/// 16 byte prefix of SHA256 of the currently set PIN.
hash: [u8; PIN_AUTH_LENGTH],
/// Length of the current PIN in code points.
code_point_length: u8,
}
/// CTAP persistent storage. /// CTAP persistent storage.
pub struct PersistentStore { pub struct PersistentStore {
store: persistent_store::Store<Storage>, store: persistent_store::Store<Storage>,
@@ -115,34 +119,51 @@ impl PersistentStore {
self.store.insert(key::CRED_RANDOM_SECRET, &cred_random)?; 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() { if self.store.find_handle(key::AAGUID)?.is_none() {
self.set_aaguid(key_material::AAGUID)?; self.set_aaguid(key_material::AAGUID)?;
} }
Ok(()) Ok(())
} }
// TODO(jmichel): remove this function when vendor command is in place. /// Returns the credential at the given key.
#[cfg(not(test))] ///
fn load_attestation_data_from_firmware(&mut self) -> Result<(), Ctap2StatusCode> { /// # Errors
// The following 2 entries are meant to be written by vendor-specific commands. ///
if self /// Returns `CTAP2_ERR_VENDOR_INTERNAL_ERROR` if the key does not hold a valid credential.
.store pub fn get_credential(&self, key: usize) -> Result<PublicKeyCredentialSource, Ctap2StatusCode> {
.find_handle(key::ATTESTATION_PRIVATE_KEY)? let min_key = key::CREDENTIALS.start;
.is_none() if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENT_KEYS {
{ return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY)?;
} }
if self let credential_entry = self
.store .store
.find_handle(key::ATTESTATION_CERTIFICATE)? .find(key)?
.is_none() .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?;
{ deserialize_credential(&credential_entry)
self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE)?; .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
}
/// Finds the key and value for a given credential ID.
///
/// # Errors
///
/// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found.
fn find_credential_item(
&self,
credential_id: &[u8],
) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> {
let mut iter_result = Ok(());
let iter = self.iter_credentials(&mut iter_result)?;
let mut credentials: Vec<(usize, PublicKeyCredentialSource)> = iter
.filter(|(_, credential)| credential.credential_id == credential_id)
.collect();
iter_result?;
if credentials.len() > 1 {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
} }
Ok(()) credentials
.pop()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
} }
/// Returns the first matching credential. /// Returns the first matching credential.
@@ -155,22 +176,17 @@ impl PersistentStore {
credential_id: &[u8], credential_id: &[u8],
check_cred_protect: bool, check_cred_protect: bool,
) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> { ) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> {
let mut iter_result = Ok(()); let credential = match self.find_credential_item(credential_id) {
let iter = self.iter_credentials(&mut iter_result)?; Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) => return Ok(None),
// We don't check whether there is more than one matching credential to be able to exit Err(e) => return Err(e),
// early. Ok((_key, credential)) => credential,
let result = iter.map(|(_, credential)| credential).find(|credential| { };
credential.rp_id == rp_id && credential.credential_id == credential_id let is_protected = credential.cred_protect_policy
}); == Some(CredentialProtectionPolicy::UserVerificationRequired);
iter_result?; if credential.rp_id != rp_id || (check_cred_protect && is_protected) {
if let Some(cred) = &result { return Ok(None);
let user_verification_required = cred.cred_protect_policy
== Some(CredentialProtectionPolicy::UserVerificationRequired);
if check_cred_protect && user_verification_required {
return Ok(None);
}
} }
Ok(result) Ok(Some(credential))
} }
/// Stores or updates a credential. /// Stores or updates a credential.
@@ -184,13 +200,11 @@ impl PersistentStore {
let mut old_key = None; let mut old_key = None;
let min_key = key::CREDENTIALS.start; let min_key = key::CREDENTIALS.start;
// Holds whether a key is used (indices are shifted by min_key). // Holds whether a key is used (indices are shifted by min_key).
let mut keys = vec![false; MAX_SUPPORTED_RESIDENTIAL_KEYS]; let mut keys = vec![false; MAX_SUPPORTED_RESIDENT_KEYS];
let mut iter_result = Ok(()); let mut iter_result = Ok(());
let iter = self.iter_credentials(&mut iter_result)?; let iter = self.iter_credentials(&mut iter_result)?;
for (key, credential) in iter { for (key, credential) in iter {
if key < min_key if key < min_key || key - min_key >= MAX_SUPPORTED_RESIDENT_KEYS || keys[key - min_key]
|| key - min_key >= MAX_SUPPORTED_RESIDENTIAL_KEYS
|| keys[key - min_key]
{ {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
} }
@@ -205,16 +219,14 @@ impl PersistentStore {
} }
} }
iter_result?; iter_result?;
if old_key.is_none() if old_key.is_none() && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENT_KEYS {
&& keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENTIAL_KEYS
{
return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL);
} }
let key = match old_key { let key = match old_key {
// This is a new credential being added, we need to allocate a free key. We choose the // This is a new credential being added, we need to allocate a free key. We choose the
// first available key. // first available key.
None => key::CREDENTIALS None => key::CREDENTIALS
.take(MAX_SUPPORTED_RESIDENTIAL_KEYS) .take(MAX_SUPPORTED_RESIDENT_KEYS)
.find(|key| !keys[key - min_key]) .find(|key| !keys[key - min_key])
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?,
// This is an existing credential being updated, we reuse its key. // This is an existing credential being updated, we reuse its key.
@@ -225,32 +237,35 @@ impl PersistentStore {
Ok(()) Ok(())
} }
/// Returns the list of matching credentials. /// Deletes a credential.
/// ///
/// Does not return credentials that are not discoverable if `check_cred_protect` is set. /// # Errors
pub fn filter_credential( ///
&self, /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found.
rp_id: &str, pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> {
check_cred_protect: bool, let (key, _) = self.find_credential_item(credential_id)?;
) -> Result<Vec<PublicKeyCredentialSource>, Ctap2StatusCode> { Ok(self.store.remove(key)?)
let mut iter_result = Ok(()); }
let iter = self.iter_credentials(&mut iter_result)?;
let result = iter /// Updates a credential's user information.
.filter_map(|(_, credential)| { ///
if credential.rp_id == rp_id { /// # Errors
Some(credential) ///
} else { /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found.
None pub fn _update_credential(
} &mut self,
}) credential_id: &[u8],
.filter(|cred| !check_cred_protect || cred.is_discoverable()) user: PublicKeyCredentialUserEntity,
.collect(); ) -> Result<(), Ctap2StatusCode> {
iter_result?; let (key, mut credential) = self.find_credential_item(credential_id)?;
Ok(result) credential.user_name = user.user_name;
credential.user_display_name = user.user_display_name;
credential.user_icon = user.user_icon;
let value = serialize_credential(credential)?;
Ok(self.store.insert(key, &value)?)
} }
/// Returns the number of credentials. /// Returns the number of credentials.
#[cfg(test)]
pub fn count_credentials(&self) -> Result<usize, Ctap2StatusCode> { pub fn count_credentials(&self) -> Result<usize, Ctap2StatusCode> {
let mut iter_result = Ok(()); let mut iter_result = Ok(());
let iter = self.iter_credentials(&mut iter_result)?; let iter = self.iter_credentials(&mut iter_result)?;
@@ -259,10 +274,17 @@ impl PersistentStore {
Ok(result) Ok(result)
} }
/// Returns the estimated number of credentials that can still be stored.
pub fn remaining_credentials(&self) -> Result<usize, Ctap2StatusCode> {
MAX_SUPPORTED_RESIDENT_KEYS
.checked_sub(self.count_credentials()?)
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
}
/// Iterates through the credentials. /// Iterates through the credentials.
/// ///
/// If an error is encountered during iteration, it is written to `result`. /// If an error is encountered during iteration, it is written to `result`.
fn iter_credentials<'a>( pub fn iter_credentials<'a>(
&'a self, &'a self,
result: &'a mut Result<(), Ctap2StatusCode>, result: &'a mut Result<(), Ctap2StatusCode>,
) -> Result<IterCredentials<'a>, Ctap2StatusCode> { ) -> Result<IterCredentials<'a>, Ctap2StatusCode> {
@@ -325,26 +347,44 @@ impl PersistentStore {
Ok(*array_ref![cred_random_secret, offset, 32]) Ok(*array_ref![cred_random_secret, offset, 32])
} }
/// Returns the PIN hash if defined. /// Reads the PIN properties and wraps them into PinProperties.
pub fn pin_hash(&self) -> Result<Option<[u8; PIN_AUTH_LENGTH]>, Ctap2StatusCode> { fn pin_properties(&self) -> Result<Option<PinProperties>, Ctap2StatusCode> {
let pin_hash = match self.store.find(key::PIN_HASH)? { let pin_properties = match self.store.find(key::PIN_PROPERTIES)? {
None => return Ok(None), None => return Ok(None),
Some(pin_hash) => pin_hash, Some(pin_properties) => pin_properties,
}; };
if pin_hash.len() != PIN_AUTH_LENGTH { const PROPERTIES_LENGTH: usize = PIN_AUTH_LENGTH + 1;
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); match pin_properties.len() {
PROPERTIES_LENGTH => Ok(Some(PinProperties {
hash: *array_ref![pin_properties, 1, PIN_AUTH_LENGTH],
code_point_length: pin_properties[0],
})),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
} }
Ok(Some(*array_ref![pin_hash, 0, PIN_AUTH_LENGTH]))
} }
/// Sets the PIN hash. /// Returns the PIN hash if defined.
pub fn pin_hash(&self) -> Result<Option<[u8; PIN_AUTH_LENGTH]>, Ctap2StatusCode> {
Ok(self.pin_properties()?.map(|p| p.hash))
}
/// Returns the length of the currently set PIN if defined.
pub fn pin_code_point_length(&self) -> Result<Option<u8>, Ctap2StatusCode> {
Ok(self.pin_properties()?.map(|p| p.code_point_length))
}
/// Sets the PIN hash and length.
/// ///
/// If it was already defined, it is updated. /// If it was already defined, it is updated.
pub fn set_pin_hash( pub fn set_pin(
&mut self, &mut self,
pin_hash: &[u8; PIN_AUTH_LENGTH], pin_hash: &[u8; PIN_AUTH_LENGTH],
pin_code_point_length: u8,
) -> Result<(), Ctap2StatusCode> { ) -> Result<(), Ctap2StatusCode> {
Ok(self.store.insert(key::PIN_HASH, pin_hash)?) let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH];
pin_properties[0] = pin_code_point_length;
pin_properties[1..].clone_from_slice(pin_hash);
Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?)
} }
/// Returns the number of remaining PIN retries. /// Returns the number of remaining PIN retries.
@@ -372,7 +412,6 @@ impl PersistentStore {
} }
/// Returns the minimum PIN length. /// Returns the minimum PIN length.
#[cfg(feature = "with_ctap2_1")]
pub fn min_pin_length(&self) -> Result<u8, Ctap2StatusCode> { pub fn min_pin_length(&self) -> Result<u8, Ctap2StatusCode> {
match self.store.find(key::MIN_PIN_LENGTH)? { match self.store.find(key::MIN_PIN_LENGTH)? {
None => Ok(DEFAULT_MIN_PIN_LENGTH), None => Ok(DEFAULT_MIN_PIN_LENGTH),
@@ -382,43 +421,40 @@ impl PersistentStore {
} }
/// Sets the minimum PIN length. /// Sets the minimum PIN length.
#[cfg(feature = "with_ctap2_1")]
pub fn set_min_pin_length(&mut self, min_pin_length: u8) -> Result<(), Ctap2StatusCode> { pub fn set_min_pin_length(&mut self, min_pin_length: u8) -> Result<(), Ctap2StatusCode> {
Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?)
} }
/// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is
/// allowed. /// allowed.
#[cfg(feature = "with_ctap2_1")] pub fn min_pin_length_rp_ids(&self) -> Result<Vec<String>, Ctap2StatusCode> {
pub fn _min_pin_length_rp_ids(&self) -> Result<Vec<String>, Ctap2StatusCode> {
let rp_ids = self let rp_ids = self
.store .store
.find(key::_MIN_PIN_LENGTH_RP_IDS)? .find(key::MIN_PIN_LENGTH_RP_IDS)?
.map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| {
_deserialize_min_pin_length_rp_ids(&value) deserialize_min_pin_length_rp_ids(&value)
}); });
debug_assert!(rp_ids.is_some()); debug_assert!(rp_ids.is_some());
Ok(rp_ids.unwrap_or(vec![])) Ok(rp_ids.unwrap_or_default())
} }
/// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed.
#[cfg(feature = "with_ctap2_1")] pub fn set_min_pin_length_rp_ids(
pub fn _set_min_pin_length_rp_ids(
&mut self, &mut self,
min_pin_length_rp_ids: Vec<String>, min_pin_length_rp_ids: Vec<String>,
) -> Result<(), Ctap2StatusCode> { ) -> Result<(), Ctap2StatusCode> {
let mut min_pin_length_rp_ids = min_pin_length_rp_ids; let mut min_pin_length_rp_ids = min_pin_length_rp_ids;
for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS {
if !min_pin_length_rp_ids.contains(&rp_id) { if !min_pin_length_rp_ids.contains(&rp_id) {
min_pin_length_rp_ids.push(rp_id); min_pin_length_rp_ids.push(rp_id);
} }
} }
if min_pin_length_rp_ids.len() > _MAX_RP_IDS_LENGTH { if min_pin_length_rp_ids.len() > MAX_RP_IDS_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL);
} }
Ok(self.store.insert( Ok(self.store.insert(
key::_MIN_PIN_LENGTH_RP_IDS, key::MIN_PIN_LENGTH_RP_IDS,
&_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, &serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?,
)?) )?)
} }
@@ -504,6 +540,11 @@ impl PersistentStore {
self.init(rng)?; self.init(rng)?;
Ok(()) Ok(())
} }
pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> {
// TODO(kaczmarczyck) implement storage logic
Ok(())
}
} }
impl From<persistent_store::StoreError> for Ctap2StatusCode { impl From<persistent_store::StoreError> for Ctap2StatusCode {
@@ -527,7 +568,7 @@ impl From<persistent_store::StoreError> for Ctap2StatusCode {
} }
/// Iterator for credentials. /// Iterator for credentials.
struct IterCredentials<'a> { pub struct IterCredentials<'a> {
/// The store being iterated. /// The store being iterated.
store: &'a persistent_store::Store<Storage>, store: &'a persistent_store::Store<Storage>,
@@ -601,13 +642,12 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>
if cbor::write(credential.into(), &mut data) { if cbor::write(credential.into(), &mut data) {
Ok(data) Ok(data)
} else { } else {
Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
} }
} }
/// Deserializes a list of RP IDs from storage representation. /// Deserializes a list of RP IDs from storage representation.
#[cfg(feature = "with_ctap2_1")] fn deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> {
fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> {
let cbor = cbor::read(data).ok()?; let cbor = cbor::read(data).ok()?;
extract_array(cbor) extract_array(cbor)
.ok()? .ok()?
@@ -618,13 +658,12 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> {
} }
/// Serializes a list of RP IDs to storage representation. /// Serializes a list of RP IDs to storage representation.
#[cfg(feature = "with_ctap2_1")] fn serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2StatusCode> {
fn _serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2StatusCode> {
let mut data = Vec::new(); let mut data = Vec::new();
if cbor::write(cbor_array_vec!(rp_ids), &mut data) { if cbor::write(cbor_array_vec!(rp_ids), &mut data) {
Ok(data) Ok(data)
} else { } else {
Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
} }
} }
@@ -664,6 +703,66 @@ mod test {
assert!(persistent_store.count_credentials().unwrap() > 0); assert!(persistent_store.count_credentials().unwrap() > 0);
} }
#[test]
fn test_delete_credential() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
assert_eq!(persistent_store.count_credentials().unwrap(), 0);
let mut credential_ids = vec![];
for i in 0..MAX_SUPPORTED_RESIDENT_KEYS {
let user_handle = i.to_ne_bytes().to_vec();
let credential_source = create_credential_source(&mut rng, "example.com", user_handle);
credential_ids.push(credential_source.credential_id.clone());
assert!(persistent_store.store_credential(credential_source).is_ok());
assert_eq!(persistent_store.count_credentials().unwrap(), i + 1);
}
let mut count = persistent_store.count_credentials().unwrap();
for credential_id in credential_ids {
assert!(persistent_store._delete_credential(&credential_id).is_ok());
count -= 1;
assert_eq!(persistent_store.count_credentials().unwrap(), count);
}
}
#[test]
fn test_update_credential() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let user = PublicKeyCredentialUserEntity {
// User ID is ignored.
user_id: vec![0x00],
user_name: Some("name".to_string()),
user_display_name: Some("display_name".to_string()),
user_icon: Some("icon".to_string()),
};
assert_eq!(
persistent_store._update_credential(&[0x1D], user.clone()),
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
);
let credential_source = create_credential_source(&mut rng, "example.com", vec![0x1D]);
let credential_id = credential_source.credential_id.clone();
assert!(persistent_store.store_credential(credential_source).is_ok());
let stored_credential = persistent_store
.find_credential("example.com", &credential_id, false)
.unwrap()
.unwrap();
assert_eq!(stored_credential.user_name, None);
assert_eq!(stored_credential.user_display_name, None);
assert_eq!(stored_credential.user_icon, None);
assert!(persistent_store
._update_credential(&credential_id, user.clone())
.is_ok());
let stored_credential = persistent_store
.find_credential("example.com", &credential_id, false)
.unwrap()
.unwrap();
assert_eq!(stored_credential.user_name, user.user_name);
assert_eq!(stored_credential.user_display_name, user.user_display_name);
assert_eq!(stored_credential.user_icon, user.user_icon);
}
#[test] #[test]
fn test_credential_order() { fn test_credential_order() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -680,24 +779,21 @@ mod test {
} }
#[test] #[test]
#[allow(clippy::assertions_on_constants)]
fn test_fill_store() { fn test_fill_store() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng); let mut persistent_store = PersistentStore::new(&mut rng);
assert_eq!(persistent_store.count_credentials().unwrap(), 0); assert_eq!(persistent_store.count_credentials().unwrap(), 0);
// To make this test work for bigger storages, implement better int -> Vec conversion. for i in 0..MAX_SUPPORTED_RESIDENT_KEYS {
assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); let user_handle = i.to_ne_bytes().to_vec();
for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { let credential_source = create_credential_source(&mut rng, "example.com", user_handle);
let credential_source =
create_credential_source(&mut rng, "example.com", vec![i as u8]);
assert!(persistent_store.store_credential(credential_source).is_ok()); assert!(persistent_store.store_credential(credential_source).is_ok());
assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1);
} }
let credential_source = create_credential_source( let credential_source = create_credential_source(
&mut rng, &mut rng,
"example.com", "example.com",
vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], vec![MAX_SUPPORTED_RESIDENT_KEYS as u8],
); );
assert_eq!( assert_eq!(
persistent_store.store_credential(credential_source), persistent_store.store_credential(credential_source),
@@ -705,12 +801,11 @@ mod test {
); );
assert_eq!( assert_eq!(
persistent_store.count_credentials().unwrap(), persistent_store.count_credentials().unwrap(),
MAX_SUPPORTED_RESIDENTIAL_KEYS MAX_SUPPORTED_RESIDENT_KEYS
); );
} }
#[test] #[test]
#[allow(clippy::assertions_on_constants)]
fn test_overwrite() { fn test_overwrite() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng); let mut persistent_store = PersistentStore::new(&mut rng);
@@ -718,7 +813,8 @@ mod test {
// These should have different IDs. // These should have different IDs.
let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]);
let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x00]);
let expected_credential = credential_source1.clone(); let credential_id0 = credential_source0.credential_id.clone();
let credential_id1 = credential_source1.credential_id.clone();
assert!(persistent_store assert!(persistent_store
.store_credential(credential_source0) .store_credential(credential_source0)
@@ -727,25 +823,26 @@ mod test {
.store_credential(credential_source1) .store_credential(credential_source1)
.is_ok()); .is_ok());
assert_eq!(persistent_store.count_credentials().unwrap(), 1); assert_eq!(persistent_store.count_credentials().unwrap(), 1);
assert_eq!( assert!(persistent_store
&persistent_store .find_credential("example.com", &credential_id0, false)
.filter_credential("example.com", false) .unwrap()
.unwrap(), .is_none());
&[expected_credential] assert!(persistent_store
); .find_credential("example.com", &credential_id1, false)
.unwrap()
.is_some());
// To make this test work for bigger storages, implement better int -> Vec conversion. let mut persistent_store = PersistentStore::new(&mut rng);
assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); for i in 0..MAX_SUPPORTED_RESIDENT_KEYS {
for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { let user_handle = i.to_ne_bytes().to_vec();
let credential_source = let credential_source = create_credential_source(&mut rng, "example.com", user_handle);
create_credential_source(&mut rng, "example.com", vec![i as u8]);
assert!(persistent_store.store_credential(credential_source).is_ok()); assert!(persistent_store.store_credential(credential_source).is_ok());
assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1);
} }
let credential_source = create_credential_source( let credential_source = create_credential_source(
&mut rng, &mut rng,
"example.com", "example.com",
vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], vec![MAX_SUPPORTED_RESIDENT_KEYS as u8],
); );
assert_eq!( assert_eq!(
persistent_store.store_credential(credential_source), persistent_store.store_credential(credential_source),
@@ -753,69 +850,26 @@ mod test {
); );
assert_eq!( assert_eq!(
persistent_store.count_credentials().unwrap(), persistent_store.count_credentials().unwrap(),
MAX_SUPPORTED_RESIDENTIAL_KEYS MAX_SUPPORTED_RESIDENT_KEYS
); );
} }
#[test] #[test]
fn test_filter() { fn test_get_credential() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng); let mut persistent_store = PersistentStore::new(&mut rng);
assert_eq!(persistent_store.count_credentials().unwrap(), 0);
let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]);
let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]);
let credential_source2 = let credential_source2 =
create_credential_source(&mut rng, "another.example.com", vec![0x02]); create_credential_source(&mut rng, "another.example.com", vec![0x02]);
let id0 = credential_source0.credential_id.clone(); let credential_sources = vec![credential_source0, credential_source1, credential_source2];
let id1 = credential_source1.credential_id.clone(); for credential_source in credential_sources.into_iter() {
assert!(persistent_store let cred_id = credential_source.credential_id.clone();
.store_credential(credential_source0) assert!(persistent_store.store_credential(credential_source).is_ok());
.is_ok()); let (key, _) = persistent_store.find_credential_item(&cred_id).unwrap();
assert!(persistent_store let cred = persistent_store.get_credential(key).unwrap();
.store_credential(credential_source1) assert_eq!(&cred_id, &cred.credential_id);
.is_ok()); }
assert!(persistent_store
.store_credential(credential_source2)
.is_ok());
let filtered_credentials = persistent_store
.filter_credential("example.com", false)
.unwrap();
assert_eq!(filtered_credentials.len(), 2);
assert!(
(filtered_credentials[0].credential_id == id0
&& filtered_credentials[1].credential_id == id1)
|| (filtered_credentials[1].credential_id == id0
&& filtered_credentials[0].credential_id == id1)
);
}
#[test]
fn test_filter_with_cred_protect() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
assert_eq!(persistent_store.count_credentials().unwrap(), 0);
let private_key = crypto::ecdsa::SecKey::gensk(&mut rng);
let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: rng.gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x00],
user_display_name: None,
cred_protect_policy: Some(
CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
),
creation_order: 0,
user_name: None,
user_icon: None,
};
assert!(persistent_store.store_credential(credential).is_ok());
let no_credential = persistent_store
.filter_credential("example.com", true)
.unwrap();
assert_eq!(no_credential, vec![]);
} }
#[test] #[test]
@@ -926,28 +980,38 @@ mod test {
} }
#[test] #[test]
fn test_pin_hash() { fn test_pin_hash_and_length() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng); let mut persistent_store = PersistentStore::new(&mut rng);
// Pin hash is initially not set. // Pin hash is initially not set.
assert!(persistent_store.pin_hash().unwrap().is_none()); assert!(persistent_store.pin_hash().unwrap().is_none());
assert!(persistent_store.pin_code_point_length().unwrap().is_none());
// Setting the pin hash sets the pin hash. // Setting the pin sets the pin hash.
let random_data = rng.gen_uniform_u8x32(); let random_data = rng.gen_uniform_u8x32();
assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH); assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH);
let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH); let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH);
let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH); let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH);
persistent_store.set_pin_hash(&pin_hash_1).unwrap(); let pin_length_1 = 4;
let pin_length_2 = 63;
persistent_store.set_pin(&pin_hash_1, pin_length_1).unwrap();
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1));
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); assert_eq!(
persistent_store.set_pin_hash(&pin_hash_2).unwrap(); persistent_store.pin_code_point_length().unwrap(),
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); Some(pin_length_1)
);
persistent_store.set_pin(&pin_hash_2, pin_length_2).unwrap();
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2));
assert_eq!(
persistent_store.pin_code_point_length().unwrap(),
Some(pin_length_2)
);
// Resetting the storage resets the pin hash. // Resetting the storage resets the pin hash.
persistent_store.reset(&mut rng).unwrap(); persistent_store.reset(&mut rng).unwrap();
assert!(persistent_store.pin_hash().unwrap().is_none()); assert!(persistent_store.pin_hash().unwrap().is_none());
assert!(persistent_store.pin_code_point_length().unwrap().is_none());
} }
#[test] #[test]
@@ -988,12 +1052,14 @@ mod test {
.unwrap() .unwrap()
.is_none()); .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 persistent_store
.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) .set_attestation_private_key(&dummy_key)
.unwrap(); .unwrap();
persistent_store persistent_store
.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) .set_attestation_certificate(&dummy_cert)
.unwrap(); .unwrap();
assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID);
@@ -1001,16 +1067,15 @@ mod test {
persistent_store.reset(&mut rng).unwrap(); persistent_store.reset(&mut rng).unwrap();
assert_eq!( assert_eq!(
&persistent_store.attestation_private_key().unwrap().unwrap(), &persistent_store.attestation_private_key().unwrap().unwrap(),
key_material::ATTESTATION_PRIVATE_KEY &dummy_key
); );
assert_eq!( assert_eq!(
persistent_store.attestation_certificate().unwrap().unwrap(), persistent_store.attestation_certificate().unwrap().unwrap(),
key_material::ATTESTATION_CERTIFICATE &dummy_cert
); );
assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID);
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_min_pin_length() { fn test_min_pin_length() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1033,7 +1098,6 @@ mod test {
); );
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_min_pin_length_rp_ids() { fn test_min_pin_length_rp_ids() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1041,22 +1105,22 @@ mod test {
// The minimum PIN length RP IDs are initially at the default. // The minimum PIN length RP IDs are initially at the default.
assert_eq!( assert_eq!(
persistent_store._min_pin_length_rp_ids().unwrap(), persistent_store.min_pin_length_rp_ids().unwrap(),
_DEFAULT_MIN_PIN_LENGTH_RP_IDS DEFAULT_MIN_PIN_LENGTH_RP_IDS
); );
// Changes by the setter are reflected by the getter. // Changes by the setter are reflected by the getter.
let mut rp_ids = vec![String::from("example.com")]; let mut rp_ids = vec![String::from("example.com")];
assert_eq!( assert_eq!(
persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()), persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()),
Ok(()) Ok(())
); );
for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS {
if !rp_ids.contains(&rp_id) { if !rp_ids.contains(&rp_id) {
rp_ids.push(rp_id); rp_ids.push(rp_id);
} }
} }
assert_eq!(persistent_store._min_pin_length_rp_ids().unwrap(), rp_ids); assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids);
} }
#[test] #[test]
@@ -1102,12 +1166,11 @@ mod test {
assert_eq!(credential, reconstructed); assert_eq!(credential, reconstructed);
} }
#[cfg(feature = "with_ctap2_1")]
#[test] #[test]
fn test_serialize_deserialize_min_pin_length_rp_ids() { fn test_serialize_deserialize_min_pin_length_rp_ids() {
let rp_ids = vec![String::from("example.com")]; let rp_ids = vec![String::from("example.com")];
let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); let serialized = serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap();
let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap(); let reconstructed = deserialize_min_pin_length_rp_ids(&serialized).unwrap();
assert_eq!(rp_ids, reconstructed); assert_eq!(rp_ids, reconstructed);
} }
} }

View File

@@ -84,21 +84,19 @@ make_partition! {
/// The credentials. /// The credentials.
/// ///
/// Depending on `MAX_SUPPORTED_RESIDENTIAL_KEYS`, only a prefix of those keys is used. Each /// Depending on `MAX_SUPPORTED_RESIDENT_KEYS`, only a prefix of those keys is used. Each
/// board may configure `MAX_SUPPORTED_RESIDENTIAL_KEYS` depending on the storage size. /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size.
CREDENTIALS = 1700..2000; CREDENTIALS = 1700..2000;
/// The secret of the CredRandom feature. /// The secret of the CredRandom feature.
CRED_RANDOM_SECRET = 2041; CRED_RANDOM_SECRET = 2041;
/// List of RP IDs allowed to read the minimum PIN length. /// List of RP IDs allowed to read the minimum PIN length.
#[cfg(feature = "with_ctap2_1")] MIN_PIN_LENGTH_RP_IDS = 2042;
_MIN_PIN_LENGTH_RP_IDS = 2042;
/// The minimum PIN length. /// The minimum PIN length.
/// ///
/// If the entry is absent, the minimum PIN length is `DEFAULT_MIN_PIN_LENGTH`. /// If the entry is absent, the minimum PIN length is `DEFAULT_MIN_PIN_LENGTH`.
#[cfg(feature = "with_ctap2_1")]
MIN_PIN_LENGTH = 2043; MIN_PIN_LENGTH = 2043;
/// The number of PIN retries. /// The number of PIN retries.
@@ -106,10 +104,11 @@ make_partition! {
/// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`. /// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`.
PIN_RETRIES = 2044; PIN_RETRIES = 2044;
/// The PIN hash. /// The PIN hash and length.
/// ///
/// If the entry is absent, there is no PIN set. /// If the entry is absent, there is no PIN set. The first byte represents
PIN_HASH = 2045; /// the length, the following are an array with the hash.
PIN_PROPERTIES = 2045;
/// The encryption and hmac keys. /// The encryption and hmac keys.
/// ///
@@ -128,8 +127,8 @@ mod test {
#[test] #[test]
fn enough_credentials() { fn enough_credentials() {
use super::super::MAX_SUPPORTED_RESIDENTIAL_KEYS; use super::super::MAX_SUPPORTED_RESIDENT_KEYS;
assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS <= CREDENTIALS.end - CREDENTIALS.start); assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start);
} }
#[test] #[test]

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC // Copyright 2019-2021 Google LLC
// //
// Licensed under the Apache License, Version 2 (the "License"); // Licensed under the Apache License, Version 2 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.

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 buttons;
pub mod console; pub mod console;
pub mod crp;
pub mod led; pub mod led;
#[cfg(feature = "with_nfc")] #[cfg(feature = "with_nfc")]
pub mod 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())