Merge branch 'develop' into usize_32_or_std

This commit is contained in:
Julien Cretin
2021-04-26 13:12:55 +02:00
92 changed files with 13886 additions and 6934 deletions

View File

@@ -42,12 +42,6 @@ jobs:
command: check
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
uses: actions-rs/cargo@v1
with:
@@ -66,12 +60,6 @@ jobs:
command: check
args: --target thumbv7em-none-eabi --release --features debug_allocations
- name: Check OpenSK ram_storage
uses: actions-rs/cargo@v1
with:
command: check
args: --target thumbv7em-none-eabi --release --features ram_storage
- name: Check OpenSK verbose
uses: actions-rs/cargo@v1
with:
@@ -84,17 +72,11 @@ jobs:
command: check
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
with:
command: check
args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap2_1
- 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
args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,,panic_console,debug_allocations,verbose
- name: Check examples
uses: actions-rs/cargo@v1

View File

@@ -33,10 +33,10 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path libraries/crypto/Cargo.toml --release --features std,derive_debug
args: --manifest-path libraries/crypto/Cargo.toml --release --features std
- name: Unit testing of crypto library (debug mode)
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path libraries/crypto/Cargo.toml --features std,derive_debug
args: --manifest-path libraries/crypto/Cargo.toml --features std

View File

@@ -51,27 +51,3 @@ jobs:
command: test
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

View File

@@ -13,6 +13,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Unit testing of Persistent store library (release mode)
uses: actions-rs/cargo@v1
with:

1
.gitignore vendored
View File

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

2
.gitmodules vendored
View File

@@ -1,6 +1,8 @@
[submodule "third_party/libtock-rs"]
path = third_party/libtock-rs
url = https://github.com/tock/libtock-rs
ignore = dirty
[submodule "third_party/tock"]
path = third_party/tock
url = https://github.com/tock/tock
ignore = dirty

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"davidanson.vscode-markdownlint",
"rust-lang.rust",
"ms-python.python"
]
}

27
.vscode/settings.json vendored
View File

@@ -1,27 +0,0 @@
{
"clang-format.fallbackStyle": "google",
"editor.detectIndentation": true,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.insertSpaces": true,
"editor.tabSize": 4,
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"rust-client.channel": "nightly",
// The toolchain is updated from time to time so let's make sure that RLS is updated too
"rust-client.updateOnStartup": true,
"rust.clippy_preference": "on",
// Try to make VSCode formating as close as possible to the Google style.
"python.formatting.provider": "yapf",
"python.formatting.yapfArgs": [
"--style=yapf"
],
"python.linting.enabled": true,
"python.linting.lintOnSave": true,
"python.linting.pylintEnabled": true,
"python.linting.pylintPath": "pylint",
"[python]": {
"editor.tabSize": 2
},
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ctap2"
version = "0.1.0"
version = "1.0.0"
authors = [
"Fabian Kaczmarczyck <kaczmarczyck@google.com>",
"Guillaume Endignoux <guillaumee@google.com>",
@@ -15,19 +15,18 @@ libtock_drivers = { path = "third_party/libtock-drivers" }
lang_items = { path = "third_party/lang-items" }
cbor = { path = "libraries/cbor" }
crypto = { path = "libraries/crypto" }
persistent_store = { path = "libraries/persistent_store" }
byteorder = { version = "1", default-features = false }
arrayref = "0.3.6"
subtle = { version = "2.2", default-features = false, features = ["nightly"] }
[features]
debug_allocations = ["lang_items/debug_allocations"]
debug_ctap = ["crypto/derive_debug", "libtock_drivers/debug_ctap"]
debug_ctap = ["libtock_drivers/debug_ctap"]
panic_console = ["lang_items/panic_console"]
std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std"]
ram_storage = []
std = ["cbor/std", "crypto/std", "lang_items/std", "persistent_store/std"]
verbose = ["debug_ctap", "libtock_drivers/verbose_usb"]
with_ctap1 = ["crypto/with_ctap1"]
with_ctap2_1 = []
with_nfc = ["libtock_drivers/with_nfc"]
[dev-dependencies]
@@ -35,7 +34,6 @@ elf2tab = "0.6.0"
enum-iterator = "0.6.0"
[build-dependencies]
openssl = "0.10"
uuid = { version = "0.8", features = ["v4"] }
[profile.dev]
@@ -45,3 +43,4 @@ lto = true # Link Time Optimization usually reduces size of binaries and static
[profile.release]
panic = "abort"
lto = true # Link Time Optimization usually reduces size of binaries and static libraries
opt-level = "z"

57
OpenSK.code-workspace Normal file
View File

@@ -0,0 +1,57 @@
{
"folders": [
{
"name": "OpenSK",
"path": "."
},
{
"name": "tock",
"path": "third_party/tock"
},
{
"name": "libtock-rs",
"path": "third_party/libtock-rs"
},
{
"name": "libtock-drivers",
"path": "third_party/libtock-drivers"
}
],
"settings": {
"clang-format.fallbackStyle": "google",
"editor.detectIndentation": true,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.insertSpaces": true,
"editor.tabSize": 4,
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
// Ensure we use the toolchain we set in rust-toolchain file
"rust-client.channel": "default",
// The toolchain is updated from time to time so let's make sure that RLS is updated too
"rust-client.updateOnStartup": true,
"rust.clippy_preference": "on",
"rust.target": "thumbv7em-none-eabi",
"rust.all_targets": false,
// Try to make VSCode formating as close as possible to the Google style.
"python.formatting.provider": "yapf",
"python.formatting.yapfArgs": [
"--style=yapf"
],
"python.linting.enabled": true,
"python.linting.lintOnSave": true,
"python.linting.pylintEnabled": true,
"python.linting.pylintPath": "pylint",
"[python]": {
"editor.tabSize": 2
},
},
"extensions": {
"recommendations": [
"davidanson.vscode-markdownlint",
"rust-lang.rust",
"ms-python.python"
]
}
}

View File

@@ -1,6 +1,5 @@
# <img alt="OpenSK logo" src="docs/img/OpenSK.svg" width="200px">
[![Build Status](https://travis-ci.org/google/OpenSK.svg?branch=master)](https://travis-ci.org/google/OpenSK)
![markdownlint](https://github.com/google/OpenSK/workflows/markdownlint/badge.svg?branch=master)
![pylint](https://github.com/google/OpenSK/workflows/pylint/badge.svg?branch=master)
![Cargo check](https://github.com/google/OpenSK/workflows/Cargo%20check/badge.svg?branch=master)
@@ -25,14 +24,16 @@ few limitations:
### FIDO2
Although we tested and implemented our firmware based on the published
[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
be FIDO Certified.
We started adding features of the upcoming next version of the
[CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html).
The development is currently between 2.0 and 2.1, with updates hidden behind a feature flag.
Please add the flag `--ctap2.1` to the deploy command to include them.
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)
and is FIDO certified.
<img alt="FIDO2 certified L1" src="docs/img/FIDO2_Certified_L1.png" width="200px">
It already contains some preview features of 2.1, that you can try by adding the
flag `--ctap2.1` to the deploy command. The full
[CTAP2.1 specification](https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html)
is work in progress in the develop branch and is tested less thoroughly.
### Cryptography
@@ -58,8 +59,8 @@ For a more detailed guide, please refer to our
./setup.sh
```
2. Next step is to install Tock OS as well as the OpenSK application on your
board (**Warning**: it will erase the locally stored credentials). Run:
1. Next step is to install Tock OS as well as the OpenSK application on your
board. Run:
```shell
# Nordic nRF52840-DK board
@@ -68,7 +69,17 @@ For a more detailed guide, please refer to our
./deploy.py --board=nrf52840_dongle --opensk
```
3. On Linux, you may want to avoid the need for `root` privileges to interact
1. Finally you need to inject the cryptographic material if you enabled
batch attestation or CTAP1/U2F compatibility (which is the case by
default):
```shell
./tools/configure.py \
--certificate=crypto_data/opensk_cert.pem \
--private-key=crypto_data/opensk.key
```
1. On Linux, you may want to avoid the need for `root` privileges to interact
with the key. For that purpose we provide a udev rule file that can be
installed with the following command:
@@ -83,33 +94,19 @@ If you build your own security key, depending on the hardware you use, there are
a few things you can personalize:
1. If you have multiple buttons, choose the buttons responsible for user
presence in `main.rs`.
2. 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
self-signed certificate. The flag is used for FIDO2 and has some privacy
implications. Please check
[WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more
information.
3. Decide whether you want to use signature counters. Currently, only global
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
preserving solution is individual or no signature counters. Again, please
check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for
documentation.
4. Depending on your available flash storage, choose an appropriate maximum
number of supported residential keys and number of pages in
`ctap/storage.rs`.
5. Change the default level for the credProtect extension in `ctap/mod.rs`.
When changing the default, resident credentials become undiscoverable without
user verification. This helps privacy, but can make usage less comfortable
for credentials that need less protection.
6. 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
PINs can help establish trust between users and relying parties. It makes
user verification harder to break, but less convenient.
NIST recommends at least 6-digit PINs in section 5.1.9.1:
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.
presence in `src/main.rs`.
1. If you have colored LEDs, like different blinking patterns and want to play
around with the code in `src/main.rs` more, take a look at e.g. `wink_leds`.
1. You find more options and documentation in `src/ctap/customization.rs`,
including:
- The default level for the credProtect extension.
- The default minimum PIN length, and what relying parties can set it.
- Whether you want to enforce alwaysUv.
- Settings for enterprise attestation.
- The maximum PIN retries.
- Whether you want to use batch attestation.
- Whether you want to use signature counters.
- Various constants to adapt to different hardware.
### 3D printed enclosure
@@ -148,7 +145,7 @@ operation.
The additional output looks like the following.
```
```text
# Allocation of 256 byte(s), aligned on 1 byte(s). The allocated address is
# 0x2002401c. After this operation, 2 pointers have been allocated, totalling
# 384 bytes (the total heap usage may be larger, due to alignment and
@@ -163,12 +160,12 @@ A tool is provided to analyze such reports, in `tools/heapviz`. This tool
parses the console output, identifies the lines corresponding to (de)allocation
operations, and first computes some statistics:
- Address range used by the heap over this run of the program,
- Peak heap usage (how many useful bytes are allocated),
- Peak heap consumption (how many bytes are used by the heap, including
unavailable bytes between allocated blocks, due to alignment constraints and
memory fragmentation),
- Fragmentation overhead (difference between heap consumption and usage).
* Address range used by the heap over this run of the program,
* Peak heap usage (how many useful bytes are allocated),
* Peak heap consumption (how many bytes are used by the heap, including
unavailable bytes between allocated blocks, due to alignment constraints and
memory fragmentation),
* Fragmentation overhead (difference between heap consumption and usage).
Then, the `heapviz` tool displays an animated "movie" of the allocated bytes in
heap memory. Each frame in this "movie" shows bytes that are currently
@@ -177,10 +174,11 @@ allocated. A new frame is generated for each (de)allocation operation. This tool
uses the `ncurses` library, that you may have to install beforehand.
You can control the tool with the following parameters:
- `--logfile` (required) to provide the file which contains the console output
to parse,
- `--fps` (optional) to customize the number of frames per second in the movie
animation.
* `--logfile` (required) to provide the file which contains the console output
to parse,
* `--fps` (optional) to customize the number of frames per second in the movie
animation.
```shell
cargo run --manifest-path tools/heapviz/Cargo.toml -- --logfile console.log --fps 50

View File

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

View File

@@ -12,11 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use openssl::asn1;
use openssl::ec;
use openssl::nid::Nid;
use openssl::pkey::PKey;
use openssl::x509;
use std::env;
use std::fs::File;
use std::io::Read;
@@ -25,65 +20,11 @@ use std::path::Path;
use uuid::Uuid;
fn main() {
println!("cargo:rerun-if-changed=crypto_data/opensk.key");
println!("cargo:rerun-if-changed=crypto_data/opensk_cert.pem");
println!("cargo:rerun-if-changed=crypto_data/aaguid.txt");
let out_dir = env::var_os("OUT_DIR").unwrap();
let priv_key_bin_path = Path::new(&out_dir).join("opensk_pkey.bin");
let cert_bin_path = Path::new(&out_dir).join("opensk_cert.bin");
let aaguid_bin_path = Path::new(&out_dir).join("opensk_aaguid.bin");
// Load the OpenSSL PEM ECC key
let ecc_data = include_bytes!("crypto_data/opensk.key");
let pkey =
ec::EcKey::private_key_from_pem(ecc_data).expect("Failed to load OpenSK private key file");
// Check key validity
pkey.check_key().unwrap();
assert_eq!(pkey.group().curve_name(), Some(Nid::X9_62_PRIME256V1));
// Private keys generated by OpenSSL have variable size but we only handle
// constant size. Serialization is done in big endian so if the size is less
// than 32 bytes, we need to prepend with null bytes.
// If the size is 33 bytes, this means the serialized BigInt is negative.
// Any other size is invalid.
let priv_key_hex = pkey.private_key().to_hex_str().unwrap();
let priv_key_vec = pkey.private_key().to_vec();
let key_len = priv_key_vec.len();
assert!(
key_len <= 33,
"Invalid private key (too big): {} ({:#?})",
priv_key_hex,
priv_key_vec,
);
// Copy OpenSSL generated key to our vec, starting from the end
let mut output_vec = [0u8; 32];
let min_key_len = std::cmp::min(key_len, 32);
output_vec[32 - min_key_len..].copy_from_slice(&priv_key_vec[key_len - min_key_len..]);
// Create the raw private key out of the OpenSSL data
let mut priv_key_bin_file = File::create(&priv_key_bin_path).unwrap();
priv_key_bin_file.write_all(&output_vec).unwrap();
// Convert the PEM certificate to DER and extract the serial for AAGUID
let input_pem_cert = include_bytes!("crypto_data/opensk_cert.pem");
let cert = x509::X509::from_pem(input_pem_cert).expect("Failed to load OpenSK certificate");
// Do some sanity check on the certificate
assert!(cert
.public_key()
.unwrap()
.public_eq(&PKey::from_ec_key(pkey).unwrap()));
let now = asn1::Asn1Time::days_from_now(0).unwrap();
assert!(cert.not_after() > now);
assert!(cert.not_before() <= now);
let mut cert_bin_file = File::create(&cert_bin_path).unwrap();
cert_bin_file.write_all(&cert.to_der().unwrap()).unwrap();
let mut aaguid_bin_file = File::create(&aaguid_bin_path).unwrap();
let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap();
let mut content = String::new();

View File

@@ -352,6 +352,7 @@ class OpenSKInstaller:
def build_opensk(self):
info("Building OpenSK application")
self._check_invariants()
self._build_app_or_example(is_example=False)
def _build_app_or_example(self, is_example):
@@ -390,6 +391,11 @@ class OpenSKInstaller:
# Create a TAB file
self.create_tab_file({props.arch: app_path})
def _check_invariants(self):
print("Testing invariants in customization.rs...")
self.checked_command_output(
["cargo", "test", "--features=std", "--lib", "customization"])
def generate_crypto_materials(self, force_regenerate):
has_error = subprocess.call([
os.path.join("tools", "gen_key_materials.sh"),
@@ -710,6 +716,22 @@ class OpenSKInstaller:
check=False,
timeout=None,
).returncode
# Configure OpenSK through vendor specific command if needed
if any([
self.args.lock_device,
self.args.config_cert,
self.args.config_pkey,
]):
# pylint: disable=g-import-not-at-top,import-outside-toplevel
import tools.configure
tools.configure.main(
argparse.Namespace(
batch=False,
certificate=self.args.config_cert,
priv_key=self.args.config_pkey,
lock=self.args.lock_device,
))
return 0
@@ -770,6 +792,33 @@ if __name__ == "__main__":
help=("Erases the persistent storage when installing an application. "
"All stored data will be permanently lost."),
)
main_parser.add_argument(
"--lock-device",
action="store_true",
default=False,
dest="lock_device",
help=("Try to disable JTAG at the end of the operations. This "
"operation may fail if the device is already locked or if "
"the certificate/private key are not programmed."),
)
main_parser.add_argument(
"--inject-certificate",
default=None,
metavar="PEM_FILE",
type=argparse.FileType("rb"),
dest="config_cert",
help=("If this option is set, the corresponding certificate "
"will be programmed into the key as the last operation."),
)
main_parser.add_argument(
"--inject-private-key",
default=None,
metavar="PEM_FILE",
type=argparse.FileType("rb"),
dest="config_pkey",
help=("If this option is set, the corresponding private key "
"will be programmed into the key as the last operation."),
)
main_parser.add_argument(
"--programmer",
metavar="METHOD",
@@ -838,14 +887,6 @@ if __name__ == "__main__":
help=("Compiles the OpenSK application without backward compatible "
"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(
"--nfc",
action="append_const",
@@ -863,14 +904,6 @@ if __name__ == "__main__":
"This is useful to allow flashing multiple OpenSK authenticators "
"in a row without them being considered clones."),
)
main_parser.add_argument(
"--no-persistent-storage",
action="append_const",
const="ram_storage",
dest="features",
help=("Compiles and installs the OpenSK application without persistent "
"storage (i.e. unplugging the key will reset the key)."),
)
main_parser.add_argument(
"--elf2tab-output",
@@ -907,6 +940,21 @@ if __name__ == "__main__":
const="crypto_bench",
help=("Compiles and installs the crypto_bench example that benchmarks "
"the performance of the cryptographic algorithms on the board."))
apps_group.add_argument(
"--store_latency",
dest="application",
action="store_const",
const="store_latency",
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(
"--panic_test",
dest="application",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

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)
to have a more practical form factor.
* [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
hardware:
@@ -125,6 +126,7 @@ This is the expected content after running our `setup.sh` script:
File | Purpose
----------------- | --------------------------------------------------------
`aaguid.txt` | Text file containaing the AAGUID value
`opensk_ca.csr` | Certificate sign request for the Root CA
`opensk_ca.key` | ECC secp256r1 private key used for the Root CA
`opensk_ca.pem` | PEM encoded certificate of the Root CA
@@ -136,9 +138,11 @@ File | Purpose
If you want to use your own attestation certificate and private key, simply
replace `opensk_cert.pem` and `opensk.key` files.
Our build script `build.rs` is responsible for converting `opensk_cert.pem` and
`opensk.key` files into raw data that is then used by the Rust file:
`src/ctap/key_material.rs`.
Our build script `build.rs` is responsible for converting the `aaguid.txt` file
into raw data that is then used by the Rust file `src/ctap/key_material.rs`.
Our configuration script `tools/configure.py` is responsible for configuring
an OpenSK device with the correct certificate and private key.
### Flashing a firmware

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
}

138
examples/store_latency.rs Normal file
View File

@@ -0,0 +1,138 @@
// Copyright 2019-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 alloc;
extern crate lang_items;
use alloc::vec;
use core::fmt::Write;
use ctap2::embedded_flash::{new_storage, Storage};
use libtock_drivers::console::Console;
use libtock_drivers::timer::{self, Duration, Timer, Timestamp};
use persistent_store::Store;
fn timestamp(timer: &Timer) -> Timestamp<f64> {
Timestamp::<f64>::from_clock_value(timer.get_current_clock().ok().unwrap())
}
fn measure<T>(timer: &Timer, operation: impl FnOnce() -> T) -> (T, Duration<f64>) {
let before = timestamp(timer);
let result = operation();
let after = timestamp(timer);
(result, after - before)
}
// Only use one store at a time.
unsafe fn boot_store(num_pages: usize, erase: bool) -> Store<Storage> {
let mut storage = new_storage(num_pages);
if erase {
for page in 0..num_pages {
use persistent_store::Storage;
storage.erase_page(page).unwrap();
}
}
Store::new(storage).ok().unwrap()
}
fn compute_latency(timer: &Timer, num_pages: usize, key_increment: usize, word_length: usize) {
let mut console = Console::new();
writeln!(
console,
"\nLatency for num_pages={} key_increment={} word_length={}.",
num_pages, key_increment, word_length
)
.unwrap();
let mut store = unsafe { boot_store(num_pages, true) };
let total_capacity = store.capacity().unwrap().total();
assert_eq!(store.capacity().unwrap().used(), 0);
assert_eq!(store.lifetime().unwrap().used(), 0);
// Burn N words to align the end of the user capacity with the virtual capacity.
store.insert(0, &vec![0; 4 * (num_pages - 1)]).unwrap();
store.remove(0).unwrap();
assert_eq!(store.capacity().unwrap().used(), 0);
assert_eq!(store.lifetime().unwrap().used(), num_pages);
// Insert entries until there is space for one more.
let count = total_capacity / (1 + word_length) - 1;
let ((), time) = measure(timer, || {
for i in 0..count {
let key = 1 + key_increment * i;
// For some reason the kernel sometimes fails.
while store.insert(key, &vec![0; 4 * word_length]).is_err() {
// We never enter this loop in practice, but we still need it for the kernel.
writeln!(console, "Retry insert.").unwrap();
}
}
});
writeln!(console, "Setup: {:.1}ms for {} entries.", time.ms(), count).unwrap();
// Measure latency of insert.
let key = 1 + key_increment * count;
let ((), time) = measure(&timer, || {
store.insert(key, &vec![0; 4 * word_length]).unwrap()
});
writeln!(console, "Insert: {:.1}ms.", time.ms()).unwrap();
assert_eq!(
store.lifetime().unwrap().used(),
num_pages + (1 + count) * (1 + word_length)
);
// Measure latency of boot.
let (mut store, time) = measure(&timer, || unsafe { boot_store(num_pages, false) });
writeln!(console, "Boot: {:.1}ms.", time.ms()).unwrap();
// Measure latency of remove.
let ((), time) = measure(&timer, || store.remove(key).unwrap());
writeln!(console, "Remove: {:.1}ms.", time.ms()).unwrap();
// Measure latency of compaction.
let length = total_capacity + num_pages - store.lifetime().unwrap().used();
if length > 0 {
// Fill the store such that compaction is needed for one word.
store.insert(0, &vec![0; 4 * (length - 1)]).unwrap();
store.remove(0).unwrap();
}
assert!(store.capacity().unwrap().remaining() > 0);
assert_eq!(store.lifetime().unwrap().used(), num_pages + total_capacity);
let ((), time) = measure(timer, || store.prepare(1).unwrap());
writeln!(console, "Compaction: {:.1}ms.", time.ms()).unwrap();
assert!(store.lifetime().unwrap().used() > total_capacity + num_pages);
}
fn main() {
let mut with_callback = timer::with_callback(|_, _| {});
let timer = with_callback.init().ok().unwrap();
writeln!(Console::new(), "\nRunning 4 tests...").unwrap();
// Those non-overwritten 50 words entries simulate credentials.
compute_latency(&timer, 3, 1, 50);
compute_latency(&timer, 20, 1, 50);
// Those overwritten 1 word entries simulate counters.
compute_latency(&timer, 3, 0, 1);
compute_latency(&timer, 20, 0, 1);
writeln!(Console::new(), "\nDone.").unwrap();
// Results on nrf52840dk:
//
// | Pages | Overwrite | Length | Boot | Compaction | Insert | Remove |
// | ----- | --------- | --------- | ------- | ---------- | ------ | ------ |
// | 3 | no | 50 words | 2.0 ms | 132.8 ms | 4.3 ms | 1.2 ms |
// | 20 | no | 50 words | 7.8 ms | 135.7 ms | 9.9 ms | 4.0 ms |
// | 3 | yes | 1 word | 19.6 ms | 90.8 ms | 4.7 ms | 2.3 ms |
// | 20 | yes | 1 word | 183.3 ms | 90.9 ms | 4.8 ms | 2.3 ms |
}

View File

@@ -10,5 +10,5 @@ arrayref = "0.3.6"
libtock_drivers = { path = "../../third_party/libtock-drivers" }
crypto = { path = "../../libraries/crypto", features = ['std'] }
cbor = { path = "../../libraries/cbor", features = ['std'] }
ctap2 = { path = "../..", features = ['std', 'ram_storage'] }
ctap2 = { path = "../..", features = ['std'] }
lang_items = { path = "../../third_party/lang-items", features = ['std'] }

View File

@@ -147,7 +147,7 @@ fn process_message<CheckUserPresence>(
pub fn process_ctap_any_type(data: &[u8]) {
// Initialize ctap state and hid and get the allocated cid.
let mut rng = ThreadRng256 {};
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let mut ctap_hid = CtapHid::new();
let cid = initialize(&mut ctap_state, &mut ctap_hid);
// Wrap input as message with the allocated cid.
@@ -165,7 +165,7 @@ pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) {
}
// Initialize ctap state and hid and get the allocated cid.
let mut rng = ThreadRng256 {};
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let mut ctap_hid = CtapHid::new();
let cid = initialize(&mut ctap_state, &mut ctap_hid);
// Wrap input as message with allocated cid and command type.

View File

@@ -24,5 +24,5 @@ pub mod values;
pub mod writer;
pub use self::reader::read;
pub use self::values::{KeyType, SimpleValue, Value};
pub use self::values::{SimpleValue, Value};
pub use self::writer::write;

View File

@@ -12,15 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::values::{KeyType, Value};
use alloc::collections::btree_map;
use crate::values::Value;
use alloc::vec;
use core::cmp::Ordering;
use core::iter::Peekable;
/// This macro generates code to extract multiple values from a `BTreeMap<KeyType, Value>` at once
/// in an optimized manner, consuming the input map.
/// This macro generates code to extract multiple values from a `Vec<(Value, Value)>` at once
/// in an optimized manner, consuming the input vector.
///
/// It takes as input a `BTreeMap` as well as a list of identifiers and keys, and generates code
/// It takes as input a `Vec` as well as a list of identifiers and keys, and generates code
/// that assigns the corresponding values to new variables using the given identifiers. Each of
/// these variables has type `Option<Value>`, to account for the case where keys aren't found.
///
@@ -32,16 +32,14 @@ use core::iter::Peekable;
/// the keys are indeed sorted. This macro is therefore **not suitable for dynamic keys** that can
/// change at runtime.
///
/// Semantically, provided that the keys are sorted as specified above, the following two snippets
/// of code are equivalent, but the `destructure_cbor_map!` version is more optimized, as it doesn't
/// re-balance the `BTreeMap` for each key, contrary to the `BTreeMap::remove` operations.
/// Example usage:
///
/// ```rust
/// # extern crate alloc;
/// # use cbor::destructure_cbor_map;
/// #
/// # fn main() {
/// # let map = alloc::collections::BTreeMap::new();
/// # let map = alloc::vec::Vec::new();
/// destructure_cbor_map! {
/// let {
/// 1 => x,
@@ -50,17 +48,6 @@ use core::iter::Peekable;
/// }
/// # }
/// ```
///
/// ```rust
/// # extern crate alloc;
/// #
/// # fn main() {
/// # let mut map = alloc::collections::BTreeMap::<cbor::KeyType, _>::new();
/// use cbor::values::IntoCborKey;
/// let x: Option<cbor::Value> = map.remove(&1.into_cbor_key());
/// let y: Option<cbor::Value> = map.remove(&"key".into_cbor_key());
/// # }
/// ```
#[macro_export]
macro_rules! destructure_cbor_map {
( let { $( $key:expr => $variable:ident, )+ } = $map:expr; ) => {
@@ -70,7 +57,7 @@ macro_rules! destructure_cbor_map {
#[cfg(test)]
$crate::assert_sorted_keys!($( $key, )+);
use $crate::values::{IntoCborKey, Value};
use $crate::values::{IntoCborValue, Value};
use $crate::macros::destructure_cbor_map_peek_value;
// This algorithm first converts the map into a peekable iterator - whose items are sorted
@@ -83,7 +70,7 @@ macro_rules! destructure_cbor_map {
// to come in the same order (i.e. sorted).
let mut it = $map.into_iter().peekable();
$(
let $variable: Option<Value> = destructure_cbor_map_peek_value(&mut it, $key.into_cbor_key());
let $variable: Option<Value> = destructure_cbor_map_peek_value(&mut it, $key.into_cbor_value());
)+
};
}
@@ -100,14 +87,14 @@ macro_rules! destructure_cbor_map {
/// would be inlined for every use case. As of June 2020, this saves ~40KB of binary size for the
/// CTAP2 application of OpenSK.
pub fn destructure_cbor_map_peek_value(
it: &mut Peekable<btree_map::IntoIter<KeyType, Value>>,
needle: KeyType,
it: &mut Peekable<vec::IntoIter<(Value, Value)>>,
needle: Value,
) -> Option<Value> {
loop {
match it.peek() {
None => return None,
Some(item) => {
let key: &KeyType = &item.0;
let key: &Value = &item.0;
match key.cmp(&needle) {
Ordering::Less => {
it.next();
@@ -131,9 +118,9 @@ macro_rules! assert_sorted_keys {
( $key1:expr, $key2:expr, $( $keys:expr, )* ) => {
{
use $crate::values::{IntoCborKey, KeyType};
let k1: KeyType = $key1.into_cbor_key();
let k2: KeyType = $key2.into_cbor_key();
use $crate::values::{IntoCborValue, Value};
let k1: Value = $key1.into_cbor_value();
let k2: Value = $key2.into_cbor_value();
assert!(
k1 < k2,
"{:?} < {:?} failed. The destructure_cbor_map! macro requires keys in sorted order.",
@@ -145,6 +132,23 @@ macro_rules! assert_sorted_keys {
};
}
/// Creates a CBOR Value of type Map with the specified key-value pairs.
///
/// Keys and values are expressions and converted into CBOR Keys and Values.
/// The syntax for these pairs is `key_expression => value_expression,`.
/// Duplicate keys will lead to invalid CBOR, i.e. writing these values fails.
/// Keys do not have to be sorted.
///
/// Example usage:
///
/// ```rust
/// # extern crate alloc;
/// # use cbor::cbor_map;
/// let map = cbor_map! {
/// 0x01 => false,
/// "02" => -3,
/// };
/// ```
#[macro_export]
macro_rules! cbor_map {
// trailing comma case
@@ -156,16 +160,36 @@ macro_rules! cbor_map {
{
// The import is unused if the list is empty.
#[allow(unused_imports)]
use $crate::values::{IntoCborKey, IntoCborValue};
let mut _map = ::alloc::collections::BTreeMap::new();
use $crate::values::IntoCborValue;
let mut _map = ::alloc::vec::Vec::new();
$(
_map.insert($key.into_cbor_key(), $value.into_cbor_value());
_map.push(($key.into_cbor_value(), $value.into_cbor_value()));
)*
$crate::values::Value::Map(_map)
}
};
}
/// Creates a CBOR Value of type Map with key-value pairs where values can be Options.
///
/// Keys and values are expressions and converted into CBOR Keys and Value Options.
/// The map entry is included iff the Value is not an Option or Option is Some.
/// The syntax for these pairs is `key_expression => value_expression,`.
/// Duplicate keys will lead to invalid CBOR, i.e. writing these values fails.
/// Keys do not have to be sorted.
///
/// Example usage:
///
/// ```rust
/// # extern crate alloc;
/// # use cbor::cbor_map_options;
/// let missing_value: Option<bool> = None;
/// let map = cbor_map_options! {
/// 0x01 => Some(false),
/// "02" => -3,
/// "not in map" => missing_value,
/// };
/// ```
#[macro_export]
macro_rules! cbor_map_options {
// trailing comma case
@@ -177,13 +201,13 @@ macro_rules! cbor_map_options {
{
// The import is unused if the list is empty.
#[allow(unused_imports)]
use $crate::values::{IntoCborKey, IntoCborValueOption};
let mut _map = ::alloc::collections::BTreeMap::<_, $crate::values::Value>::new();
use $crate::values::{IntoCborValue, IntoCborValueOption};
let mut _map = ::alloc::vec::Vec::<(_, $crate::values::Value)>::new();
$(
{
let opt: Option<$crate::values::Value> = $value.into_cbor_value_option();
if let Some(val) = opt {
_map.insert($key.into_cbor_key(), val);
_map.push(($key.into_cbor_value(), val));
}
}
)*
@@ -192,13 +216,25 @@ macro_rules! cbor_map_options {
};
}
/// Creates a CBOR Value of type Map from a Vec<(Value, Value)>.
#[macro_export]
macro_rules! cbor_map_btree {
( $tree:expr ) => {
$crate::values::Value::Map($tree)
};
macro_rules! cbor_map_collection {
( $tree:expr ) => {{
$crate::values::Value::from($tree)
}};
}
/// Creates a CBOR Value of type Array with the given elements.
///
/// Elements are expressions and converted into CBOR Values. Elements are comma-separated.
///
/// Example usage:
///
/// ```rust
/// # extern crate alloc;
/// # use cbor::cbor_array;
/// let array = cbor_array![1, "2"];
/// ```
#[macro_export]
macro_rules! cbor_array {
// trailing comma case
@@ -216,6 +252,7 @@ macro_rules! cbor_array {
};
}
/// Creates a CBOR Value of type Array from a Vec<Value>.
#[macro_export]
macro_rules! cbor_array_vec {
( $vec:expr ) => {{
@@ -224,6 +261,7 @@ macro_rules! cbor_array_vec {
}};
}
/// Creates a CBOR Value of type Simple with value true.
#[macro_export]
macro_rules! cbor_true {
( ) => {
@@ -231,6 +269,7 @@ macro_rules! cbor_true {
};
}
/// Creates a CBOR Value of type Simple with value false.
#[macro_export]
macro_rules! cbor_false {
( ) => {
@@ -238,6 +277,7 @@ macro_rules! cbor_false {
};
}
/// Creates a CBOR Value of type Simple with value null.
#[macro_export]
macro_rules! cbor_null {
( ) => {
@@ -245,6 +285,7 @@ macro_rules! cbor_null {
};
}
/// Creates a CBOR Value of type Simple with the undefined value.
#[macro_export]
macro_rules! cbor_undefined {
( ) => {
@@ -252,6 +293,7 @@ macro_rules! cbor_undefined {
};
}
/// Creates a CBOR Value of type Simple with the given bool value.
#[macro_export]
macro_rules! cbor_bool {
( $x:expr ) => {
@@ -259,37 +301,47 @@ macro_rules! cbor_bool {
};
}
// For key types, we construct a KeyType and call .into(), which will automatically convert it to a
// KeyType or a Value depending on the context.
/// Creates a CBOR Value of type Unsigned with the given numeric value.
#[macro_export]
macro_rules! cbor_unsigned {
( $x:expr ) => {
$crate::cbor_key_unsigned!($x).into()
$crate::values::Value::Unsigned($x)
};
}
/// Creates a CBOR Value of type Unsigned or Negative with the given numeric value.
#[macro_export]
macro_rules! cbor_int {
( $x:expr ) => {
$crate::cbor_key_int!($x).into()
$crate::values::Value::integer($x)
};
}
/// Creates a CBOR Value of type Text String with the given string.
#[macro_export]
macro_rules! cbor_text {
( $x:expr ) => {
$crate::cbor_key_text!($x).into()
$crate::values::Value::TextString($x.into())
};
}
/// Creates a CBOR Value of type Byte String with the given slice or vector.
#[macro_export]
macro_rules! cbor_bytes {
( $x:expr ) => {
$crate::cbor_key_bytes!($x).into()
$crate::values::Value::ByteString($x)
};
}
// Macro to use with a literal, e.g. cbor_bytes_lit!(b"foo")
/// Creates a CBOR Value of type Byte String with the given byte string literal.
///
/// Example usage:
///
/// ```rust
/// # extern crate alloc;
/// # use cbor::cbor_bytes_lit;
/// let byte_array = cbor_bytes_lit!(b"foo");
/// ```
#[macro_export]
macro_rules! cbor_bytes_lit {
( $x:expr ) => {
@@ -297,39 +349,9 @@ macro_rules! cbor_bytes_lit {
};
}
// Some explicit macros are also available for contexts where the type is not explicit.
#[macro_export]
macro_rules! cbor_key_unsigned {
( $x:expr ) => {
$crate::values::KeyType::Unsigned($x)
};
}
#[macro_export]
macro_rules! cbor_key_int {
( $x:expr ) => {
$crate::values::KeyType::integer($x)
};
}
#[macro_export]
macro_rules! cbor_key_text {
( $x:expr ) => {
$crate::values::KeyType::TextString($x.into())
};
}
#[macro_export]
macro_rules! cbor_key_bytes {
( $x:expr ) => {
$crate::values::KeyType::ByteString($x)
};
}
#[cfg(test)]
mod test {
use super::super::values::{KeyType, SimpleValue, Value};
use alloc::collections::BTreeMap;
use super::super::values::{SimpleValue, Value};
#[test]
fn test_cbor_simple_values() {
@@ -347,23 +369,20 @@ mod test {
#[test]
fn test_cbor_int_unsigned() {
assert_eq!(cbor_key_int!(0), KeyType::Unsigned(0));
assert_eq!(cbor_key_int!(1), KeyType::Unsigned(1));
assert_eq!(cbor_key_int!(123456), KeyType::Unsigned(123456));
assert_eq!(cbor_int!(0), Value::Unsigned(0));
assert_eq!(cbor_int!(1), Value::Unsigned(1));
assert_eq!(cbor_int!(123456), Value::Unsigned(123456));
assert_eq!(
cbor_key_int!(std::i64::MAX),
KeyType::Unsigned(std::i64::MAX as u64)
cbor_int!(std::i64::MAX),
Value::Unsigned(std::i64::MAX as u64)
);
}
#[test]
fn test_cbor_int_negative() {
assert_eq!(cbor_key_int!(-1), KeyType::Negative(-1));
assert_eq!(cbor_key_int!(-123456), KeyType::Negative(-123456));
assert_eq!(
cbor_key_int!(std::i64::MIN),
KeyType::Negative(std::i64::MIN)
);
assert_eq!(cbor_int!(-1), Value::Negative(-1));
assert_eq!(cbor_int!(-123456), Value::Negative(-123456));
assert_eq!(cbor_int!(std::i64::MIN), Value::Negative(std::i64::MIN));
}
#[test]
@@ -381,16 +400,16 @@ mod test {
std::u64::MAX,
];
let b = Value::Array(vec![
Value::KeyValue(KeyType::Negative(std::i64::MIN)),
Value::KeyValue(KeyType::Negative(std::i32::MIN as i64)),
Value::KeyValue(KeyType::Negative(-123456)),
Value::KeyValue(KeyType::Negative(-1)),
Value::KeyValue(KeyType::Unsigned(0)),
Value::KeyValue(KeyType::Unsigned(1)),
Value::KeyValue(KeyType::Unsigned(123456)),
Value::KeyValue(KeyType::Unsigned(std::i32::MAX as u64)),
Value::KeyValue(KeyType::Unsigned(std::i64::MAX as u64)),
Value::KeyValue(KeyType::Unsigned(std::u64::MAX)),
Value::Negative(std::i64::MIN),
Value::Negative(std::i32::MIN as i64),
Value::Negative(-123456),
Value::Negative(-1),
Value::Unsigned(0),
Value::Unsigned(1),
Value::Unsigned(123456),
Value::Unsigned(std::i32::MAX as u64),
Value::Unsigned(std::i64::MAX as u64),
Value::Unsigned(std::u64::MAX),
]);
assert_eq!(a, b);
}
@@ -410,20 +429,17 @@ mod test {
cbor_map! {2 => 3},
];
let b = Value::Array(vec![
Value::KeyValue(KeyType::Negative(-123)),
Value::KeyValue(KeyType::Unsigned(456)),
Value::Negative(-123),
Value::Unsigned(456),
Value::Simple(SimpleValue::TrueValue),
Value::Simple(SimpleValue::NullValue),
Value::KeyValue(KeyType::TextString(String::from("foo"))),
Value::KeyValue(KeyType::ByteString(b"bar".to_vec())),
Value::TextString(String::from("foo")),
Value::ByteString(b"bar".to_vec()),
Value::Array(Vec::new()),
Value::Array(vec![
Value::KeyValue(KeyType::Unsigned(0)),
Value::KeyValue(KeyType::Unsigned(1)),
]),
Value::Map(BTreeMap::new()),
Value::Array(vec![Value::Unsigned(0), Value::Unsigned(1)]),
Value::Map(Vec::new()),
Value::Map(
[(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))]
[(Value::Unsigned(2), Value::Unsigned(3))]
.iter()
.cloned()
.collect(),
@@ -443,10 +459,10 @@ mod test {
fn test_cbor_array_vec_int() {
let a = cbor_array_vec!(vec![1, 2, 3, 4]);
let b = Value::Array(vec![
Value::KeyValue(KeyType::Unsigned(1)),
Value::KeyValue(KeyType::Unsigned(2)),
Value::KeyValue(KeyType::Unsigned(3)),
Value::KeyValue(KeyType::Unsigned(4)),
Value::Unsigned(1),
Value::Unsigned(2),
Value::Unsigned(3),
Value::Unsigned(4),
]);
assert_eq!(a, b);
}
@@ -455,9 +471,9 @@ mod test {
fn test_cbor_array_vec_text() {
let a = cbor_array_vec!(vec!["a", "b", "c"]);
let b = Value::Array(vec![
Value::KeyValue(KeyType::TextString(String::from("a"))),
Value::KeyValue(KeyType::TextString(String::from("b"))),
Value::KeyValue(KeyType::TextString(String::from("c"))),
Value::TextString(String::from("a")),
Value::TextString(String::from("b")),
Value::TextString(String::from("c")),
]);
assert_eq!(a, b);
}
@@ -466,9 +482,9 @@ mod test {
fn test_cbor_array_vec_bytes() {
let a = cbor_array_vec!(vec![b"a", b"b", b"c"]);
let b = Value::Array(vec![
Value::KeyValue(KeyType::ByteString(b"a".to_vec())),
Value::KeyValue(KeyType::ByteString(b"b".to_vec())),
Value::KeyValue(KeyType::ByteString(b"c".to_vec())),
Value::ByteString(b"a".to_vec()),
Value::ByteString(b"b".to_vec()),
Value::ByteString(b"c".to_vec()),
]);
assert_eq!(a, b);
}
@@ -489,40 +505,28 @@ mod test {
};
let b = Value::Map(
[
(Value::Negative(-1), Value::Negative(-23)),
(Value::Unsigned(4), Value::Unsigned(56)),
(
KeyType::Negative(-1),
Value::KeyValue(KeyType::Negative(-23)),
),
(KeyType::Unsigned(4), Value::KeyValue(KeyType::Unsigned(56))),
(
KeyType::TextString(String::from("foo")),
Value::TextString(String::from("foo")),
Value::Simple(SimpleValue::TrueValue),
),
(
KeyType::ByteString(b"bar".to_vec()),
Value::ByteString(b"bar".to_vec()),
Value::Simple(SimpleValue::NullValue),
),
(Value::Unsigned(5), Value::TextString(String::from("foo"))),
(Value::Unsigned(6), Value::ByteString(b"bar".to_vec())),
(Value::Unsigned(7), Value::Array(Vec::new())),
(
KeyType::Unsigned(5),
Value::KeyValue(KeyType::TextString(String::from("foo"))),
Value::Unsigned(8),
Value::Array(vec![Value::Unsigned(0), Value::Unsigned(1)]),
),
(Value::Unsigned(9), Value::Map(Vec::new())),
(
KeyType::Unsigned(6),
Value::KeyValue(KeyType::ByteString(b"bar".to_vec())),
),
(KeyType::Unsigned(7), Value::Array(Vec::new())),
(
KeyType::Unsigned(8),
Value::Array(vec![
Value::KeyValue(KeyType::Unsigned(0)),
Value::KeyValue(KeyType::Unsigned(1)),
]),
),
(KeyType::Unsigned(9), Value::Map(BTreeMap::new())),
(
KeyType::Unsigned(10),
Value::Unsigned(10),
Value::Map(
[(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))]
[(Value::Unsigned(2), Value::Unsigned(3))]
.iter()
.cloned()
.collect(),
@@ -560,40 +564,28 @@ mod test {
};
let b = Value::Map(
[
(Value::Negative(-1), Value::Negative(-23)),
(Value::Unsigned(4), Value::Unsigned(56)),
(
KeyType::Negative(-1),
Value::KeyValue(KeyType::Negative(-23)),
),
(KeyType::Unsigned(4), Value::KeyValue(KeyType::Unsigned(56))),
(
KeyType::TextString(String::from("foo")),
Value::TextString(String::from("foo")),
Value::Simple(SimpleValue::TrueValue),
),
(
KeyType::ByteString(b"bar".to_vec()),
Value::ByteString(b"bar".to_vec()),
Value::Simple(SimpleValue::NullValue),
),
(Value::Unsigned(5), Value::TextString(String::from("foo"))),
(Value::Unsigned(6), Value::ByteString(b"bar".to_vec())),
(Value::Unsigned(7), Value::Array(Vec::new())),
(
KeyType::Unsigned(5),
Value::KeyValue(KeyType::TextString(String::from("foo"))),
Value::Unsigned(8),
Value::Array(vec![Value::Unsigned(0), Value::Unsigned(1)]),
),
(Value::Unsigned(9), Value::Map(Vec::new())),
(
KeyType::Unsigned(6),
Value::KeyValue(KeyType::ByteString(b"bar".to_vec())),
),
(KeyType::Unsigned(7), Value::Array(Vec::new())),
(
KeyType::Unsigned(8),
Value::Array(vec![
Value::KeyValue(KeyType::Unsigned(0)),
Value::KeyValue(KeyType::Unsigned(1)),
]),
),
(KeyType::Unsigned(9), Value::Map(BTreeMap::new())),
(
KeyType::Unsigned(10),
Value::Unsigned(10),
Value::Map(
[(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))]
[(Value::Unsigned(2), Value::Unsigned(3))]
.iter()
.cloned()
.collect(),
@@ -608,30 +600,20 @@ mod test {
}
#[test]
fn test_cbor_map_btree_empty() {
let a = cbor_map_btree!(BTreeMap::new());
let b = Value::Map(BTreeMap::new());
fn test_cbor_map_collection_empty() {
let a = cbor_map_collection!(Vec::<(_, _)>::new());
let b = Value::Map(Vec::new());
assert_eq!(a, b);
}
#[test]
fn test_cbor_map_btree_foo() {
let a = cbor_map_btree!(
[(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))]
.iter()
.cloned()
.collect()
);
let b = Value::Map(
[(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))]
.iter()
.cloned()
.collect(),
);
fn test_cbor_map_collection_foo() {
let a = cbor_map_collection!(vec![(Value::Unsigned(2), Value::Unsigned(3))]);
let b = Value::Map(vec![(Value::Unsigned(2), Value::Unsigned(3))]);
assert_eq!(a, b);
}
fn extract_map(cbor_value: Value) -> BTreeMap<KeyType, Value> {
fn extract_map(cbor_value: Value) -> Vec<(Value, Value)> {
match cbor_value {
Value::Map(map) => map,
_ => panic!("Expected CBOR map."),

View File

@@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::values::{Constants, KeyType, SimpleValue, Value};
use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_btree, cbor_text, cbor_unsigned};
use alloc::collections::BTreeMap;
use super::values::{Constants, SimpleValue, Value};
use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_collection, cbor_text, cbor_unsigned};
use alloc::str;
use alloc::vec::Vec;
@@ -23,7 +22,6 @@ pub enum DecoderError {
UnsupportedMajorType,
UnknownAdditionalInfo,
IncompleteCborData,
IncorrectMapKeyType,
TooMuchNesting,
InvalidUtf8,
ExtranousData,
@@ -135,7 +133,7 @@ impl<'a> Reader<'a> {
if signed_size < 0 {
Err(DecoderError::OutOfRangeIntegerValue)
} else {
Ok(Value::KeyValue(KeyType::Negative(-(size_value as i64) - 1)))
Ok(Value::Negative(-(size_value as i64) - 1))
}
}
@@ -174,23 +172,19 @@ impl<'a> Reader<'a> {
size_value: u64,
remaining_depth: i8,
) -> Result<Value, DecoderError> {
let mut value_map = BTreeMap::new();
let mut value_map = Vec::new();
let mut last_key_option = None;
for _ in 0..size_value {
let key_value = self.decode_complete_data_item(remaining_depth - 1)?;
if let Value::KeyValue(key) = key_value {
if let Some(last_key) = last_key_option {
if last_key >= key {
return Err(DecoderError::OutOfOrderKey);
}
let key = self.decode_complete_data_item(remaining_depth - 1)?;
if let Some(last_key) = last_key_option {
if last_key >= key {
return Err(DecoderError::OutOfOrderKey);
}
last_key_option = Some(key.clone());
value_map.insert(key, self.decode_complete_data_item(remaining_depth - 1)?);
} else {
return Err(DecoderError::IncorrectMapKeyType);
}
last_key_option = Some(key.clone());
value_map.push((key, self.decode_complete_data_item(remaining_depth - 1)?));
}
Ok(cbor_map_btree!(value_map))
Ok(cbor_map_collection!(value_map))
}
fn decode_to_simple_value(
@@ -615,19 +609,6 @@ mod test {
}
}
#[test]
fn test_read_unsupported_map_key_format_error() {
// While CBOR can handle all types as map keys, we only support a subset.
let bad_map_cbor = vec![
0xa2, // map of 2 pairs
0x82, 0x01, 0x02, // invalid key : [1, 2]
0x02, // value : 2
0x61, 0x64, // key : "d"
0x03, // value : 3
];
assert_eq!(read(&bad_map_cbor), Err(DecoderError::IncorrectMapKeyType));
}
#[test]
fn test_read_unknown_additional_info_error() {
let cases = vec![

View File

@@ -12,32 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use alloc::collections::BTreeMap;
use super::writer::write;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp::Ordering;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub enum Value {
KeyValue(KeyType),
Array(Vec<Value>),
Map(BTreeMap<KeyType, Value>),
// TAG is omitted
Simple(SimpleValue),
}
// The specification recommends to limit the available keys.
// Currently supported are both integer and string types.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum KeyType {
Unsigned(u64),
// We only use 63 bits of information here.
Negative(i64),
ByteString(Vec<u8>),
TextString(String),
Array(Vec<Value>),
Map(Vec<(Value, Value)>),
// TAG is omitted
Simple(SimpleValue),
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SimpleValue {
FalseValue = 20,
TrueValue = 21,
@@ -58,6 +51,15 @@ impl Constants {
}
impl Value {
// For simplicity, this only takes i64. Construct directly for the last bit.
pub fn integer(int: i64) -> Value {
if int >= 0 {
Value::Unsigned(int as u64)
} else {
Value::Negative(int)
}
}
pub fn bool_value(b: bool) -> Value {
if b {
Value::Simple(SimpleValue::TrueValue)
@@ -67,8 +69,13 @@ impl Value {
}
pub fn type_label(&self) -> u8 {
// TODO use enum discriminant instead when stable
// https://github.com/rust-lang/rust/issues/60553
match self {
Value::KeyValue(key) => key.type_label(),
Value::Unsigned(_) => 0,
Value::Negative(_) => 1,
Value::ByteString(_) => 2,
Value::TextString(_) => 3,
Value::Array(_) => 4,
Value::Map(_) => 5,
Value::Simple(_) => 7,
@@ -76,29 +83,11 @@ impl Value {
}
}
impl KeyType {
// For simplicity, this only takes i64. Construct directly for the last bit.
pub fn integer(int: i64) -> KeyType {
if int >= 0 {
KeyType::Unsigned(int as u64)
} else {
KeyType::Negative(int)
}
}
pub fn type_label(&self) -> u8 {
match self {
KeyType::Unsigned(_) => 0,
KeyType::Negative(_) => 1,
KeyType::ByteString(_) => 2,
KeyType::TextString(_) => 3,
}
}
}
impl Ord for KeyType {
fn cmp(&self, other: &KeyType) -> Ordering {
use super::values::KeyType::{ByteString, Negative, TextString, Unsigned};
impl Ord for Value {
fn cmp(&self, other: &Value) -> Ordering {
use super::values::Value::{
Array, ByteString, Map, Negative, Simple, TextString, Unsigned,
};
let self_type_value = self.type_label();
let other_type_value = other.type_label();
if self_type_value != other_type_value {
@@ -109,17 +98,35 @@ impl Ord for KeyType {
(Negative(n1), Negative(n2)) => n1.cmp(n2).reverse(),
(ByteString(b1), ByteString(b2)) => b1.len().cmp(&b2.len()).then(b1.cmp(b2)),
(TextString(t1), TextString(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)),
_ => unreachable!(),
(Array(a1), Array(a2)) if a1.len() != a2.len() => a1.len().cmp(&a2.len()),
(Map(m1), Map(m2)) if m1.len() != m2.len() => m1.len().cmp(&m2.len()),
(Simple(s1), Simple(s2)) => s1.cmp(s2),
(v1, v2) => {
// This case could handle all of the above as well. Checking individually is faster.
let mut encoding1 = Vec::new();
write(v1.clone(), &mut encoding1);
let mut encoding2 = Vec::new();
write(v2.clone(), &mut encoding2);
encoding1.cmp(&encoding2)
}
}
}
}
impl PartialOrd for KeyType {
fn partial_cmp(&self, other: &KeyType) -> Option<Ordering> {
impl PartialOrd for Value {
fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Eq for Value {}
impl PartialEq for Value {
fn eq(&self, other: &Value) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl SimpleValue {
pub fn from_integer(int: u64) -> Option<SimpleValue> {
match int {
@@ -132,54 +139,51 @@ impl SimpleValue {
}
}
impl From<u64> for KeyType {
impl From<u64> for Value {
fn from(unsigned: u64) -> Self {
KeyType::Unsigned(unsigned)
Value::Unsigned(unsigned)
}
}
impl From<i64> for KeyType {
impl From<i64> for Value {
fn from(i: i64) -> Self {
KeyType::integer(i)
Value::integer(i)
}
}
impl From<i32> for KeyType {
impl From<i32> for Value {
fn from(i: i32) -> Self {
KeyType::integer(i as i64)
Value::integer(i as i64)
}
}
impl From<Vec<u8>> for KeyType {
impl From<Vec<u8>> for Value {
fn from(bytes: Vec<u8>) -> Self {
KeyType::ByteString(bytes)
Value::ByteString(bytes)
}
}
impl From<&[u8]> for KeyType {
impl From<&[u8]> for Value {
fn from(bytes: &[u8]) -> Self {
KeyType::ByteString(bytes.to_vec())
Value::ByteString(bytes.to_vec())
}
}
impl From<String> for KeyType {
impl From<String> for Value {
fn from(text: String) -> Self {
KeyType::TextString(text)
Value::TextString(text)
}
}
impl From<&str> for KeyType {
impl From<&str> for Value {
fn from(text: &str) -> Self {
KeyType::TextString(text.to_string())
Value::TextString(text.to_string())
}
}
impl<T> From<T> for Value
where
KeyType: From<T>,
{
fn from(t: T) -> Self {
Value::KeyValue(KeyType::from(t))
impl From<Vec<(Value, Value)>> for Value {
fn from(map: Vec<(Value, Value)>) -> Self {
Value::Map(map)
}
}
@@ -189,19 +193,6 @@ impl From<bool> for Value {
}
}
pub trait IntoCborKey {
fn into_cbor_key(self) -> KeyType;
}
impl<T> IntoCborKey for T
where
KeyType: From<T>,
{
fn into_cbor_key(self) -> KeyType {
KeyType::from(self)
}
}
pub trait IntoCborValue {
fn into_cbor_value(self) -> Value;
}
@@ -239,32 +230,69 @@ where
#[cfg(test)]
mod test {
use crate::{cbor_key_bytes, cbor_key_int, cbor_key_text};
use super::*;
use crate::{cbor_array, cbor_bool, cbor_bytes, cbor_int, cbor_map, cbor_text};
#[test]
fn test_key_type_ordering() {
assert!(cbor_key_int!(0) < cbor_key_int!(23));
assert!(cbor_key_int!(23) < cbor_key_int!(24));
assert!(cbor_key_int!(24) < cbor_key_int!(1000));
assert!(cbor_key_int!(1000) < cbor_key_int!(1000000));
assert!(cbor_key_int!(1000000) < cbor_key_int!(std::i64::MAX));
assert!(cbor_key_int!(std::i64::MAX) < cbor_key_int!(-1));
assert!(cbor_key_int!(-1) < cbor_key_int!(-23));
assert!(cbor_key_int!(-23) < cbor_key_int!(-24));
assert!(cbor_key_int!(-24) < cbor_key_int!(-1000));
assert!(cbor_key_int!(-1000) < cbor_key_int!(-1000000));
assert!(cbor_key_int!(-1000000) < cbor_key_int!(std::i64::MIN));
assert!(cbor_key_int!(std::i64::MIN) < cbor_key_bytes!(vec![]));
assert!(cbor_key_bytes!(vec![]) < cbor_key_bytes!(vec![0x00]));
assert!(cbor_key_bytes!(vec![0x00]) < cbor_key_bytes!(vec![0x01]));
assert!(cbor_key_bytes!(vec![0x01]) < cbor_key_bytes!(vec![0xFF]));
assert!(cbor_key_bytes!(vec![0xFF]) < cbor_key_bytes!(vec![0x00, 0x00]));
assert!(cbor_key_bytes!(vec![0x00, 0x00]) < cbor_key_text!(""));
assert!(cbor_key_text!("") < cbor_key_text!("a"));
assert!(cbor_key_text!("a") < cbor_key_text!("b"));
assert!(cbor_key_text!("b") < cbor_key_text!("aa"));
assert!(cbor_key_int!(1) < cbor_key_bytes!(vec![0x00]));
assert!(cbor_key_int!(1) < cbor_key_text!("s"));
assert!(cbor_key_int!(-1) < cbor_key_text!("s"));
fn test_value_ordering() {
assert!(cbor_int!(0) < cbor_int!(23));
assert!(cbor_int!(23) < cbor_int!(24));
assert!(cbor_int!(24) < cbor_int!(1000));
assert!(cbor_int!(1000) < cbor_int!(1000000));
assert!(cbor_int!(1000000) < cbor_int!(std::i64::MAX));
assert!(cbor_int!(std::i64::MAX) < cbor_int!(-1));
assert!(cbor_int!(-1) < cbor_int!(-23));
assert!(cbor_int!(-23) < cbor_int!(-24));
assert!(cbor_int!(-24) < cbor_int!(-1000));
assert!(cbor_int!(-1000) < cbor_int!(-1000000));
assert!(cbor_int!(-1000000) < cbor_int!(std::i64::MIN));
assert!(cbor_int!(std::i64::MIN) < cbor_bytes!(vec![]));
assert!(cbor_bytes!(vec![]) < cbor_bytes!(vec![0x00]));
assert!(cbor_bytes!(vec![0x00]) < cbor_bytes!(vec![0x01]));
assert!(cbor_bytes!(vec![0x01]) < cbor_bytes!(vec![0xFF]));
assert!(cbor_bytes!(vec![0xFF]) < cbor_bytes!(vec![0x00, 0x00]));
assert!(cbor_bytes!(vec![0x00, 0x00]) < cbor_text!(""));
assert!(cbor_text!("") < cbor_text!("a"));
assert!(cbor_text!("a") < cbor_text!("b"));
assert!(cbor_text!("b") < cbor_text!("aa"));
assert!(cbor_text!("aa") < cbor_array![]);
assert!(cbor_array![] < cbor_array![0]);
assert!(cbor_array![0] < cbor_array![-1]);
assert!(cbor_array![1] < cbor_array![b""]);
assert!(cbor_array![b""] < cbor_array![""]);
assert!(cbor_array![""] < cbor_array![cbor_array![]]);
assert!(cbor_array![cbor_array![]] < cbor_array![cbor_map! {}]);
assert!(cbor_array![cbor_map! {}] < cbor_array![false]);
assert!(cbor_array![false] < cbor_array![0, 0]);
assert!(cbor_array![0, 0] < cbor_map! {});
assert!(cbor_map! {} < cbor_map! {0 => 0});
assert!(cbor_map! {0 => 0} < cbor_map! {0 => 1});
assert!(cbor_map! {0 => 1} < cbor_map! {1 => 0});
assert!(cbor_map! {1 => 0} < cbor_map! {-1 => 0});
assert!(cbor_map! {-1 => 0} < cbor_map! {b"" => 0});
assert!(cbor_map! {b"" => 0} < cbor_map! {"" => 0});
assert!(cbor_map! {"" => 0} < cbor_map! {cbor_array![] => 0});
assert!(cbor_map! {cbor_array![] => 0} < cbor_map! {cbor_map!{} => 0});
assert!(cbor_map! {cbor_map!{} => 0} < cbor_map! {false => 0});
assert!(cbor_map! {false => 0} < cbor_map! {0 => 0, 0 => 0});
assert!(cbor_map! {0 => 0, 0 => 0} < cbor_bool!(false));
assert!(cbor_bool!(false) < cbor_bool!(true));
assert!(cbor_bool!(true) < Value::Simple(SimpleValue::NullValue));
assert!(Value::Simple(SimpleValue::NullValue) < Value::Simple(SimpleValue::Undefined));
assert!(cbor_int!(1) < cbor_bytes!(vec![0x00]));
assert!(cbor_int!(1) < cbor_text!("s"));
assert!(cbor_int!(1) < cbor_array![]);
assert!(cbor_int!(1) < cbor_map! {});
assert!(cbor_int!(1) < cbor_bool!(false));
assert!(cbor_int!(-1) < cbor_text!("s"));
assert!(cbor_int!(-1) < cbor_array![]);
assert!(cbor_int!(-1) < cbor_map! {});
assert!(cbor_int!(-1) < cbor_bool!(false));
assert!(cbor_bytes!(vec![0x00]) < cbor_array![]);
assert!(cbor_bytes!(vec![0x00]) < cbor_map! {});
assert!(cbor_bytes!(vec![0x00]) < cbor_bool!(false));
assert!(cbor_text!("s") < cbor_map! {});
assert!(cbor_text!("s") < cbor_bool!(false));
assert!(cbor_array![] < cbor_bool!(false));
}
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::values::{Constants, KeyType, Value};
use super::values::{Constants, Value};
use alloc::vec::Vec;
pub fn write(value: Value, encoded_cbor: &mut Vec<u8>) -> bool {
@@ -35,31 +35,36 @@ impl<'a> Writer<'a> {
if remaining_depth < 0 {
return false;
}
let type_label = value.type_label();
match value {
Value::KeyValue(KeyType::Unsigned(unsigned)) => self.start_item(0, unsigned),
Value::KeyValue(KeyType::Negative(negative)) => {
self.start_item(1, -(negative + 1) as u64)
}
Value::KeyValue(KeyType::ByteString(byte_string)) => {
self.start_item(2, byte_string.len() as u64);
Value::Unsigned(unsigned) => self.start_item(type_label, unsigned),
Value::Negative(negative) => self.start_item(type_label, -(negative + 1) as u64),
Value::ByteString(byte_string) => {
self.start_item(type_label, byte_string.len() as u64);
self.encoded_cbor.extend(byte_string);
}
Value::KeyValue(KeyType::TextString(text_string)) => {
self.start_item(3, text_string.len() as u64);
Value::TextString(text_string) => {
self.start_item(type_label, text_string.len() as u64);
self.encoded_cbor.extend(text_string.into_bytes());
}
Value::Array(array) => {
self.start_item(4, array.len() as u64);
self.start_item(type_label, array.len() as u64);
for el in array {
if !self.encode_cbor(el, remaining_depth - 1) {
return false;
}
}
}
Value::Map(map) => {
self.start_item(5, map.len() as u64);
Value::Map(mut map) => {
map.sort_by(|a, b| a.0.cmp(&b.0));
let map_len = map.len();
map.dedup_by(|a, b| a.0.eq(&b.0));
if map_len != map.len() {
return false;
}
self.start_item(type_label, map_len as u64);
for (k, v) in map {
if !self.encode_cbor(Value::KeyValue(k), remaining_depth - 1) {
if !self.encode_cbor(k, remaining_depth - 1) {
return false;
}
if !self.encode_cbor(v, remaining_depth - 1) {
@@ -67,7 +72,7 @@ impl<'a> Writer<'a> {
}
}
}
Value::Simple(simple_value) => self.start_item(7, simple_value as u64),
Value::Simple(simple_value) => self.start_item(type_label, simple_value as u64),
}
true
}
@@ -209,9 +214,16 @@ mod test {
#[test]
fn test_write_map() {
let value_map = cbor_map! {
"aa" => "AA",
"e" => "E",
"" => ".",
0 => "a",
23 => "b",
24 => "c",
std::u8::MAX as i64 => "d",
256 => "e",
std::u16::MAX as i64 => "f",
65536 => "g",
std::u32::MAX as i64 => "h",
4294967296_i64 => "i",
std::i64::MAX => "j",
-1 => "k",
-24 => "l",
-25 => "m",
@@ -224,16 +236,9 @@ mod test {
b"a" => 2,
b"bar" => 3,
b"foo" => 4,
0 => "a",
23 => "b",
24 => "c",
std::u8::MAX as i64 => "d",
256 => "e",
std::u16::MAX as i64 => "f",
65536 => "g",
std::u32::MAX as i64 => "h",
4294967296_i64 => "i",
std::i64::MAX => "j",
"" => ".",
"e" => "E",
"aa" => "AA",
};
let expected_cbor = vec![
0xb8, 0x19, // map of 25 pairs:
@@ -288,6 +293,67 @@ mod test {
assert_eq!(write_return(value_map), Some(expected_cbor));
}
#[test]
fn test_write_map_sorted() {
let sorted_map = cbor_map! {
0 => "a",
1 => "b",
-1 => "c",
-2 => "d",
b"a" => "e",
b"b" => "f",
"" => "g",
"c" => "h",
};
let unsorted_map = cbor_map! {
1 => "b",
-2 => "d",
b"b" => "f",
"c" => "h",
"" => "g",
b"a" => "e",
-1 => "c",
0 => "a",
};
assert_eq!(write_return(sorted_map), write_return(unsorted_map));
}
#[test]
fn test_write_map_duplicates() {
let duplicate0 = cbor_map! {
0 => "a",
-1 => "c",
b"a" => "e",
"c" => "g",
0 => "b",
};
assert_eq!(write_return(duplicate0), None);
let duplicate1 = cbor_map! {
0 => "a",
-1 => "c",
b"a" => "e",
"c" => "g",
-1 => "d",
};
assert_eq!(write_return(duplicate1), None);
let duplicate2 = cbor_map! {
0 => "a",
-1 => "c",
b"a" => "e",
"c" => "g",
b"a" => "f",
};
assert_eq!(write_return(duplicate2), None);
let duplicate3 = cbor_map! {
0 => "a",
-1 => "c",
b"a" => "e",
"c" => "g",
"c" => "h",
};
assert_eq!(write_return(duplicate3), None);
}
#[test]
fn test_write_map_with_array() {
let value_map = cbor_map! {

View File

@@ -25,5 +25,4 @@ regex = { version = "1", optional = true }
[features]
std = ["cbor/std", "hex", "rand", "ring", "untrusted", "serde", "serde_json", "regex"]
derive_debug = []
with_ctap1 = []

View File

@@ -18,11 +18,10 @@ use core::ops::Mul;
use subtle::{self, Choice, ConditionallySelectable, CtOption};
// An exponent on the elliptic curve, that is an element modulo the curve order N.
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
// TODO: remove this Default once https://github.com/dalek-cryptography/subtle/issues/63 is
// resolved.
#[derive(Default)]
#[cfg_attr(feature = "derive_debug", derive(Debug))]
pub struct ExponentP256 {
int: Int256,
}
@@ -92,11 +91,10 @@ impl Mul for &ExponentP256 {
}
// A non-zero exponent on the elliptic curve.
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
// TODO: remove this Default once https://github.com/dalek-cryptography/subtle/issues/63 is
// resolved.
#[derive(Default)]
#[cfg_attr(feature = "derive_debug", derive(Debug))]
pub struct NonZeroExponentP256 {
e: ExponentP256,
}

View File

@@ -111,7 +111,6 @@ impl Mul for &GFP256 {
}
}
#[cfg(feature = "derive_debug")]
impl core::fmt::Debug for GFP256 {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "GFP256::{:?}", self.int)

View File

@@ -636,7 +636,6 @@ impl SubAssign<&Int256> for Int256 {
}
}
#[cfg(feature = "derive_debug")]
impl core::fmt::Debug for Int256 {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "Int256 {{ digits: {:08x?} }}", self.digits)

View File

@@ -542,7 +542,6 @@ impl Add for &PointProjective {
}
}
#[cfg(feature = "derive_debug")]
impl core::fmt::Debug for PointP256 {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("PointP256")
@@ -552,7 +551,6 @@ impl core::fmt::Debug for PointP256 {
}
}
#[cfg(feature = "derive_debug")]
impl PartialEq for PointP256 {
fn eq(&self, other: &PointP256) -> bool {
self.x == other.x && self.y == other.y

View File

@@ -17,8 +17,6 @@ use super::ec::int256;
use super::ec::int256::Int256;
use super::ec::point::PointP256;
use super::rng256::Rng256;
use super::sha256::Sha256;
use super::Hash256;
pub const NBYTES: usize = int256::NBYTES;
@@ -26,7 +24,7 @@ pub struct SecKey {
a: NonZeroExponentP256,
}
#[cfg_attr(feature = "derive_debug", derive(Clone, PartialEq, Debug))]
#[derive(Clone, Debug, PartialEq)]
pub struct PubKey {
p: PointP256,
}
@@ -62,13 +60,15 @@ impl SecKey {
// - https://www.secg.org/sec1-v2.pdf
}
// DH key agreement method defined in the FIDO2 specification, Section 5.5.4. "Getting
// sharedSecret from Authenticator"
pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] {
/// Performs the handshake using the Diffie Hellman key agreement.
///
/// This function generates the Z in the PIN protocol v1 specification.
/// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#pinProto1
pub fn exchange_x(&self, other: &PubKey) -> [u8; 32] {
let p = self.exchange_raw(other);
let mut x: [u8; 32] = [Default::default(); 32];
p.getx().to_int().to_bin(&mut x);
Sha256::hash(&x)
x
}
}
@@ -83,11 +83,13 @@ impl PubKey {
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> {
PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y))
.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]) {
self.p.getx().to_int().to_bin(x);
self.p.gety().to_int().to_bin(y);
@@ -119,7 +121,7 @@ mod test {
/** Test that the exchanged key is the same on both sides **/
#[test]
fn test_exchange_x_sha256_is_symmetric() {
fn test_exchange_x_is_symmetric() {
let mut rng = ThreadRng256 {};
for _ in 0..ITERATIONS {
@@ -127,12 +129,12 @@ mod test {
let pk_a = sk_a.genpk();
let sk_b = SecKey::gensk(&mut rng);
let pk_b = sk_b.genpk();
assert_eq!(sk_a.exchange_x_sha256(&pk_b), sk_b.exchange_x_sha256(&pk_a));
assert_eq!(sk_a.exchange_x(&pk_b), sk_b.exchange_x(&pk_a));
}
}
#[test]
fn test_exchange_x_sha256_bytes_is_symmetric() {
fn test_exchange_x_bytes_is_symmetric() {
let mut rng = ThreadRng256 {};
for _ in 0..ITERATIONS {
@@ -146,7 +148,7 @@ mod test {
let pk_a = PubKey::from_bytes_uncompressed(&pk_bytes_a).unwrap();
let pk_b = PubKey::from_bytes_uncompressed(&pk_bytes_b).unwrap();
assert_eq!(sk_a.exchange_x_sha256(&pk_b), sk_b.exchange_x_sha256(&pk_a));
assert_eq!(sk_a.exchange_x(&pk_b), sk_b.exchange_x(&pk_a));
}
}

View File

@@ -21,14 +21,16 @@ use super::rng256::Rng256;
use super::{Hash256, HashBlockSize64Bytes};
use alloc::vec;
use alloc::vec::Vec;
#[cfg(test)]
use arrayref::array_mut_ref;
#[cfg(feature = "std")]
use arrayref::array_ref;
use arrayref::{array_mut_ref, mut_array_refs};
use cbor::{cbor_bytes, cbor_map_options};
use arrayref::mut_array_refs;
use core::marker::PhantomData;
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "derive_debug", derive(Debug))]
pub const NBYTES: usize = int256::NBYTES;
#[derive(Clone, Debug, PartialEq)]
pub struct SecKey {
k: NonZeroExponentP256,
}
@@ -38,6 +40,7 @@ pub struct Signature {
s: NonZeroExponentP256,
}
#[derive(Clone)]
pub struct PubKey {
p: PointP256,
}
@@ -58,10 +61,11 @@ impl SecKey {
}
}
// ECDSA signature based on a RNG to generate a suitable randomization parameter.
// Under the hood, rejection sampling is used to make sure that the randomization parameter is
// uniformly distributed.
// The provided RNG must be cryptographically secure; otherwise this method is insecure.
/// 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. 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
where
H: Hash256,
@@ -77,8 +81,7 @@ impl SecKey {
}
}
// Deterministic ECDSA signature based on RFC 6979 to generate a suitable randomization
// parameter.
/// Creates a deterministic ECDSA signature based on RFC 6979.
pub fn sign_rfc6979<H>(&self, msg: &[u8]) -> Signature
where
H: Hash256 + HashBlockSize64Bytes,
@@ -101,8 +104,10 @@ impl SecKey {
}
}
// Try signing a curve element given a randomization parameter k. If no signature can be
// obtained from this k, None is returned and the caller should try again with another value.
/// Try signing a curve element given a randomization parameter k.
///
/// 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> {
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.
@@ -214,7 +219,6 @@ impl Signature {
}
impl PubKey {
pub const ES256_ALGORITHM: i64 = -7;
#[cfg(feature = "with_ctap1")]
const UNCOMPRESSED_LENGTH: usize = 1 + 2 * int256::NBYTES;
@@ -242,35 +246,10 @@ impl PubKey {
representation
}
// Encodes the key according to CBOR Object Signing and Encryption, defined in RFC 8152.
pub fn to_cose_key(&self) -> Option<Vec<u8>> {
const EC2_KEY_TYPE: i64 = 2;
const P_256_CURVE: i64 = 1;
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
}
/// Writes the coordinates into the passed in arrays.
pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) {
self.p.getx().to_int().to_bin(x);
self.p.gety().to_int().to_bin(y);
}
#[cfg(feature = "std")]

View File

@@ -0,0 +1,226 @@
// Copyright 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::hmac::hmac_256;
use super::{Hash256, HashBlockSize64Bytes};
const HASH_SIZE: usize = 32;
/// Computes the HKDF with empty salt and 256 bit (one block) output.
///
/// # Arguments
///
/// * `ikm` - Input keying material
/// * `info` - Optional context and application specific information
///
/// This implementation is equivalent to the below hkdf, with `salt` set to the
/// default block of zeros and the output length l as 32.
pub fn hkdf_empty_salt_256<H>(ikm: &[u8], info: &[u8]) -> [u8; HASH_SIZE]
where
H: Hash256 + HashBlockSize64Bytes,
{
// Salt is a zero block here.
let prk = hmac_256::<H>(&[0; HASH_SIZE], ikm);
// l is implicitly the block size, so we iterate exactly once.
let mut t = info.to_vec();
t.push(1);
hmac_256::<H>(&prk, t.as_slice())
}
/// Computes the HKDF.
///
/// # Arguments
///
/// * `salt` - Optional salt value (a non-secret random value)
/// * `ikm` - Input keying material
/// * `l` - Length of output keying material in octets
/// * `info` - Optional context and application specific information
///
/// Defined in RFC: https://tools.ietf.org/html/rfc5869
///
/// `salt` and `info` can be be empty. `salt` then defaults to one block of
/// zeros of size `HASH_SIZE`. Argument order is taken from:
/// https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#pinProto2
#[cfg(test)]
pub fn hkdf<H>(salt: &[u8], ikm: &[u8], l: u8, info: &[u8]) -> Vec<u8>
where
H: Hash256 + HashBlockSize64Bytes,
{
let prk = if salt.is_empty() {
hmac_256::<H>(&[0; HASH_SIZE], ikm)
} else {
hmac_256::<H>(salt, ikm)
};
let mut t = vec![];
let mut okm = vec![];
for i in 0..(l as usize + HASH_SIZE - 1) / HASH_SIZE {
t.extend_from_slice(info);
t.push((i + 1) as u8);
t = hmac_256::<H>(&prk, t.as_slice()).to_vec();
okm.extend_from_slice(t.as_slice());
}
okm.truncate(l as usize);
okm
}
#[cfg(test)]
mod test {
use super::super::sha256::Sha256;
use super::*;
use arrayref::array_ref;
#[test]
fn test_hkdf_sha256_vectors() {
// Test vectors taken from https://tools.ietf.org/html/rfc5869.
let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
let salt = hex::decode("000102030405060708090a0b0c").unwrap();
let info = hex::decode("f0f1f2f3f4f5f6f7f8f9").unwrap();
let l = 42;
let okm = hex::decode(
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
)
.unwrap();
assert_eq!(
hkdf::<Sha256>(salt.as_slice(), ikm.as_slice(), l, info.as_slice()),
okm
);
let ikm = hex::decode(
"000102030405060708090a0b0c0d0e0f\
101112131415161718191a1b1c1d1e1f\
202122232425262728292a2b2c2d2e2f\
303132333435363738393a3b3c3d3e3f\
404142434445464748494a4b4c4d4e4f",
)
.unwrap();
let salt = hex::decode(
"606162636465666768696a6b6c6d6e6f\
707172737475767778797a7b7c7d7e7f\
808182838485868788898a8b8c8d8e8f\
909192939495969798999a9b9c9d9e9f\
a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
)
.unwrap();
let info = hex::decode(
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\
c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\
e0e1e2e3e4e5e6e7e8e9eaebecedeeef\
f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
)
.unwrap();
let l = 82;
let okm = hex::decode(
"b11e398dc80327a1c8e7f78c596a4934\
4f012eda2d4efad8a050cc4c19afa97c\
59045a99cac7827271cb41c65e590e09\
da3275600c2f09b8367793a9aca3db71\
cc30c58179ec3e87c14c01d5c1f3434f\
1d87",
)
.unwrap();
assert_eq!(
hkdf::<Sha256>(salt.as_slice(), ikm.as_slice(), l, info.as_slice()),
okm
);
let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
let salt = hex::decode("").unwrap();
let info = hex::decode("").unwrap();
let l = 42;
let okm = hex::decode(
"8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8",
)
.unwrap();
assert_eq!(
hkdf::<Sha256>(salt.as_slice(), ikm.as_slice(), l, info.as_slice()),
okm
);
}
#[test]
fn test_hkdf_empty_salt_256_sha256_vectors() {
// Test vectors generated by pycryptodome using:
// HKDF(b'0', 32, b'', SHA256, context=b'\x00').hex()
let test_okms = [
hex::decode("f9be72116cb97f41828210289caafeabde1f3dfb9723bf43538ab18f3666783a")
.unwrap(),
hex::decode("f50f964f5b94d62fd1da9356ab8662b0a0f5b8e36e277178b69b6ffecf50cf44")
.unwrap(),
hex::decode("fc8772ceb5592d67442dcb4353cdd28519e82d6e55b4cf664b5685252c2d2998")
.unwrap(),
hex::decode("62831b924839a180f53be5461eeea1b89dc21779f50142b5a54df0f0cc86d61a")
.unwrap(),
hex::decode("6991f00a12946a4e3b8315cdcf0132c2ca508fd17b769f08d1454d92d33733e0")
.unwrap(),
hex::decode("0f9bb7dddd1ec61f91d8c4f5369b5870f9d44c4ceabccca1b83f06fec115e4e3")
.unwrap(),
hex::decode("235367e2ab6cca2aba1a666825458dba6b272a215a2537c05feebe4b80dab709")
.unwrap(),
hex::decode("96e8edad661da48d1a133b38c255d33e05555bc9aa442579dea1cd8d8b8d2aef")
.unwrap(),
];
for (i, okm) in test_okms.iter().enumerate() {
// String of number i.
let ikm = i.to_string();
// Byte i.
let info = [i as u8];
assert_eq!(
&hkdf_empty_salt_256::<Sha256>(&ikm.as_bytes(), &info[..]),
array_ref!(okm, 0, 32)
);
}
}
#[test]
fn test_hkdf_length() {
let salt = [];
let mut input = Vec::new();
for l in 0..128 {
assert_eq!(
hkdf::<Sha256>(&salt, input.as_slice(), l, input.as_slice()).len(),
l as usize
);
input.push(b'A');
}
}
#[test]
fn test_hkdf_empty_salt() {
let salt = [];
let mut input = Vec::new();
for l in 0..128 {
assert_eq!(
hkdf::<Sha256>(&salt, input.as_slice(), l, input.as_slice()),
hkdf::<Sha256>(&[0; 32], input.as_slice(), l, input.as_slice())
);
input.push(b'A');
}
}
#[test]
fn test_hkdf_compare_implementations() {
let salt = [];
let l = 32;
let mut input = Vec::new();
for _ in 0..128 {
assert_eq!(
hkdf::<Sha256>(&salt, input.as_slice(), l, input.as_slice()),
hkdf_empty_salt_256::<Sha256>(input.as_slice(), input.as_slice())
);
input.push(b'A');
}
}
}

View File

@@ -22,6 +22,7 @@ pub mod cbc;
mod ec;
pub mod ecdh;
pub mod ecdsa;
pub mod hkdf;
pub mod hmac;
pub mod rng256;
pub mod sha256;

View File

@@ -11,6 +11,8 @@ cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
persistent_store = { path = "..", features = ["std"] }
rand_core = "0.5"
rand_pcg = "0.2"
strum = { version = "0.19", features = ["derive"] }
# Prevent this from interfering with workspaces

View File

@@ -0,0 +1,116 @@
// Copyright 2019-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.
use fuzz_store::{fuzz, StatKey, Stats};
use std::io::Write;
use std::io::{stdout, Read};
use std::path::Path;
fn usage(program: &str) {
println!(
r#"Usage: {} {{ [<artifact_file>] | <corpus_directory> <bucket_predicate>.. }}
If <artifact_file> is not provided, it is read from standard input.
When <bucket_predicate>.. are provided, only runs matching all predicates are shown. The format of
each <bucket_predicate> is <bucket_key>=<bucket_value>."#,
program
);
}
fn debug(data: &[u8]) {
println!("{:02x?}", data);
fuzz(data, true, None);
}
/// Bucket predicate.
struct Predicate {
/// Bucket key.
key: StatKey,
/// Bucket value.
value: usize,
}
impl std::str::FromStr for Predicate {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let predicate: Vec<&str> = input.split('=').collect();
if predicate.len() != 2 {
return Err("Predicate should have exactly one equal sign.".to_string());
}
let key = predicate[0]
.parse()
.map_err(|_| format!("Predicate key `{}` is not recognized.", predicate[0]))?;
let value: usize = predicate[1]
.parse()
.map_err(|_| format!("Predicate value `{}` is not a number.", predicate[1]))?;
if value != 0 && !value.is_power_of_two() {
return Err(format!(
"Predicate value `{}` is not a bucket.",
predicate[1]
));
}
Ok(Predicate { key, value })
}
}
fn analyze(corpus: &Path, predicates: Vec<Predicate>) {
let mut stats = Stats::default();
let mut count = 0;
let total = std::fs::read_dir(corpus).unwrap().count();
for entry in std::fs::read_dir(corpus).unwrap() {
let data = std::fs::read(entry.unwrap().path()).unwrap();
let mut stat = Stats::default();
fuzz(&data, false, Some(&mut stat));
if predicates
.iter()
.all(|p| stat.get_count(p.key, p.value).is_some())
{
stats.merge(&stat);
}
count += 1;
print!("\u{1b}[K{} / {}\r", count, total);
stdout().flush().unwrap();
}
// NOTE: To avoid reloading the corpus each time we want to check a different filter, we can
// start an interactive loop here taking filters as input and printing the filtered stats. We
// would keep all individual stats for each run in a vector.
print!("{}", stats);
}
fn main() {
let args: Vec<String> = std::env::args().collect();
// No arguments reads from stdin.
if args.len() <= 1 {
let stdin = std::io::stdin();
let mut data = Vec::new();
stdin.lock().read_to_end(&mut data).unwrap();
return debug(&data);
}
let path = Path::new(&args[1]);
// File argument assumes artifact.
if path.is_file() && args.len() == 2 {
return debug(&std::fs::read(path).unwrap());
}
// Directory argument assumes corpus.
if path.is_dir() {
match args[2..].iter().map(|x| x.parse()).collect() {
Ok(predicates) => return analyze(path, predicates),
Err(error) => eprintln!("Error: {}", error),
}
}
usage(&args[0]);
}

View File

@@ -17,5 +17,5 @@
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// TODO(ia0): Call fuzzing when implemented.
fuzz_store::fuzz(data, false, None);
});

View File

@@ -25,13 +25,12 @@
//! situation where coverage takes precedence over surjectivity is for the value of insert updates
//! where a pseudo-random generator is used to avoid wasting entropy.
// TODO(ia0): Remove when used.
#![allow(dead_code)]
mod histogram;
mod stats;
mod store;
pub use stats::{StatKey, Stats};
pub use store::fuzz;
/// Bit-level entropy source based on a byte slice shared reference.
///

View File

@@ -0,0 +1,426 @@
// Copyright 2019-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.
use crate::stats::{StatKey, Stats};
use crate::Entropy;
use persistent_store::{
BufferOptions, BufferStorage, Store, StoreDriver, StoreDriverOff, StoreDriverOn,
StoreInterruption, StoreInvariant, StoreOperation, StoreUpdate,
};
use rand_core::{RngCore, SeedableRng};
use rand_pcg::Pcg32;
use std::collections::HashMap;
use std::convert::TryInto;
// NOTE: We should be able to improve coverage by only checking the last operation. Because
// operations before the last could be checked with a shorter entropy.
// NOTE: Maybe we should split the fuzz target in smaller parts (like one per init). We should also
// name the fuzz targets with action names.
/// Checks the store against a sequence of manipulations.
///
/// The entropy to generate the sequence of manipulation should be provided in `data`. Debugging
/// information is printed if `debug` is set. Statistics are gathered if `stats` is set.
pub fn fuzz(data: &[u8], debug: bool, stats: Option<&mut Stats>) {
let mut fuzzer = Fuzzer::new(data, debug, stats);
let mut driver = fuzzer.init();
let store = loop {
if fuzzer.debug {
print!("{}", driver.storage());
}
if let StoreDriver::On(driver) = &driver {
if !fuzzer.init.is_dirty() {
driver.check().unwrap();
}
if fuzzer.debug {
println!("----------------------------------------------------------------------");
}
}
if fuzzer.entropy.is_empty() {
if fuzzer.debug {
println!("No more entropy.");
}
if fuzzer.init.is_dirty() {
return;
}
fuzzer.record(StatKey::FinishedLifetime, 0);
break driver.power_on().unwrap().extract_store();
}
driver = match driver {
StoreDriver::On(driver) => match fuzzer.apply(driver) {
Ok(x) => x,
Err(store) => {
if fuzzer.debug {
println!("No more lifetime.");
}
if fuzzer.init.is_dirty() {
return;
}
fuzzer.record(StatKey::FinishedLifetime, 1);
break store;
}
},
StoreDriver::Off(driver) => fuzzer.power_on(driver),
}
};
let virt_window = (store.format().num_pages() * store.format().virt_page_size()) as usize;
let init_lifetime = fuzzer.init.used_cycles() * virt_window;
let lifetime = store.lifetime().unwrap().used() - init_lifetime;
fuzzer.record(StatKey::UsedLifetime, lifetime);
fuzzer.record(StatKey::NumCompactions, lifetime / virt_window);
fuzzer.record_counters();
}
/// Fuzzing state.
struct Fuzzer<'a> {
/// Remaining fuzzing entropy.
entropy: Entropy<'a>,
/// Unlimited pseudo entropy.
///
/// This source is only used to generate the values of entries. This is a compromise to avoid
/// consuming fuzzing entropy for low additional coverage.
values: Pcg32,
/// The fuzzing mode.
init: Init,
/// Whether debugging is enabled.
debug: bool,
/// Whether statistics should be gathered.
stats: Option<&'a mut Stats>,
/// Statistics counters (only used when gathering statistics).
///
/// The counters are written to the statistics at the end of the fuzzing run, when their value
/// is final.
counters: HashMap<StatKey, usize>,
}
impl<'a> Fuzzer<'a> {
/// Creates an initial fuzzing state.
fn new(data: &'a [u8], debug: bool, stats: Option<&'a mut Stats>) -> Fuzzer<'a> {
let mut entropy = Entropy::new(data);
let seed = entropy.read_slice(16);
let values = Pcg32::from_seed(seed[..].try_into().unwrap());
let mut fuzzer = Fuzzer {
entropy,
values,
init: Init::Clean,
debug,
stats,
counters: HashMap::new(),
};
fuzzer.init_counters();
fuzzer.record(StatKey::Entropy, data.len());
fuzzer
}
/// Initializes the fuzzing state and returns the store driver.
fn init(&mut self) -> StoreDriver {
let mut options = BufferOptions {
word_size: 4,
page_size: 1 << self.entropy.read_range(5, 12),
max_word_writes: 2,
max_page_erases: self.entropy.read_range(0, 50000),
strict_mode: true,
};
let num_pages = self.entropy.read_range(3, 64);
self.record(StatKey::PageSize, options.page_size);
self.record(StatKey::MaxPageErases, options.max_page_erases);
self.record(StatKey::NumPages, num_pages);
if self.debug {
println!("page_size: {}", options.page_size);
println!("num_pages: {}", num_pages);
println!("max_cycle: {}", options.max_page_erases);
}
let storage_size = num_pages * options.page_size;
if self.entropy.read_bit() {
self.init = Init::Dirty;
let mut storage = vec![0xff; storage_size].into_boxed_slice();
let length = self.entropy.read_range(0, storage_size);
self.record(StatKey::DirtyLength, length);
for byte in &mut storage[0..length] {
*byte = self.entropy.read_byte();
}
if self.debug {
println!("Start with dirty storage.");
}
options.strict_mode = false;
let storage = BufferStorage::new(storage, options);
StoreDriver::Off(StoreDriverOff::new_dirty(storage))
} else if self.entropy.read_bit() {
let cycle = self.entropy.read_range(0, options.max_page_erases);
self.init = Init::Used { cycle };
if self.debug {
println!("Start with {} consumed erase cycles.", cycle);
}
self.record(StatKey::InitCycles, cycle);
let storage = vec![0xff; storage_size].into_boxed_slice();
let mut storage = BufferStorage::new(storage, options);
Store::init_with_cycle(&mut storage, cycle);
StoreDriver::Off(StoreDriverOff::new_dirty(storage))
} else {
StoreDriver::Off(StoreDriverOff::new(options, num_pages))
}
}
/// Powers a driver with possible interruption.
fn power_on(&mut self, driver: StoreDriverOff) -> StoreDriver {
if self.debug {
println!("Power on the store.");
}
self.increment(StatKey::PowerOnCount);
let interruption = self.interruption(driver.count_operations());
match driver.partial_power_on(interruption) {
Err((storage, _)) if self.init.is_dirty() => {
self.entropy.consume_all();
StoreDriver::Off(StoreDriverOff::new_dirty(storage))
}
Err(error) => self.crash(error),
Ok(driver) => driver,
}
}
/// Generates and applies an operation with possible interruption.
fn apply(&mut self, driver: StoreDriverOn) -> Result<StoreDriver, Store<BufferStorage>> {
let operation = self.operation(&driver);
if self.debug {
println!("{:?}", operation);
}
let interruption = self.interruption(driver.count_operations(&operation));
match driver.partial_apply(operation, interruption) {
Err((store, _)) if self.init.is_dirty() => {
self.entropy.consume_all();
Err(store)
}
Err((store, StoreInvariant::NoLifetime)) => Err(store),
Err((store, error)) => self.crash((store.extract_storage(), error)),
Ok((error, driver)) => {
if self.debug {
if let Some(error) = error {
println!("{:?}", error);
}
}
Ok(driver)
}
}
}
/// Reports a broken invariant and terminates fuzzing.
fn crash(&self, error: (BufferStorage, StoreInvariant)) -> ! {
let (storage, invariant) = error;
if self.debug {
print!("{}", storage);
}
panic!("{:?}", invariant);
}
/// Records a statistics if enabled.
fn record(&mut self, key: StatKey, value: usize) {
if let Some(stats) = &mut self.stats {
stats.add(key, value);
}
}
/// Increments a counter if statistics are enabled.
fn increment(&mut self, key: StatKey) {
if self.stats.is_some() {
*self.counters.get_mut(&key).unwrap() += 1;
}
}
/// Initializes all counters if statistics are enabled.
fn init_counters(&mut self) {
if self.stats.is_some() {
use StatKey::*;
self.counters.insert(PowerOnCount, 0);
self.counters.insert(TransactionCount, 0);
self.counters.insert(ClearCount, 0);
self.counters.insert(PrepareCount, 0);
self.counters.insert(InsertCount, 0);
self.counters.insert(RemoveCount, 0);
self.counters.insert(InterruptionCount, 0);
}
}
/// Records all counters if statistics are enabled.
fn record_counters(&mut self) {
if let Some(stats) = &mut self.stats {
for (&key, &value) in self.counters.iter() {
stats.add(key, value);
}
}
}
/// Generates a possibly invalid operation.
fn operation(&mut self, driver: &StoreDriverOn) -> StoreOperation {
let format = driver.model().format();
match self.entropy.read_range(0, 2) {
0 => {
// We also generate an invalid count (one past the maximum value) to test the error
// scenario. Since the test for the error scenario is monotonic, this is a good
// compromise to keep entropy bounded.
let count = self
.entropy
.read_range(0, format.max_updates() as usize + 1);
let mut updates = Vec::with_capacity(count);
for _ in 0..count {
updates.push(self.update());
}
self.increment(StatKey::TransactionCount);
StoreOperation::Transaction { updates }
}
1 => {
let min_key = self.key();
self.increment(StatKey::ClearCount);
StoreOperation::Clear { min_key }
}
2 => {
// We also generate an invalid length (one past the total capacity) to test the
// error scenario. See the explanation for transactions above for why it's enough.
let length = self
.entropy
.read_range(0, format.total_capacity() as usize + 1);
self.increment(StatKey::PrepareCount);
StoreOperation::Prepare { length }
}
_ => unreachable!(),
}
}
/// Generates a possibly invalid update.
fn update(&mut self) -> StoreUpdate<Vec<u8>> {
match self.entropy.read_range(0, 1) {
0 => {
let key = self.key();
let value = self.value();
self.increment(StatKey::InsertCount);
StoreUpdate::Insert { key, value }
}
1 => {
let key = self.key();
self.increment(StatKey::RemoveCount);
StoreUpdate::Remove { key }
}
_ => unreachable!(),
}
}
/// Generates a possibly invalid key.
fn key(&mut self) -> usize {
// Use 4096 as the canonical invalid key.
self.entropy.read_range(0, 4096)
}
/// Generates a possibly invalid value.
fn value(&mut self) -> Vec<u8> {
// Use 1024 as the canonical invalid length.
let length = self.entropy.read_range(0, 1024);
let mut value = vec![0; length];
self.values.fill_bytes(&mut value);
value
}
/// Generates an interruption.
///
/// The `max_delay` describes the number of storage operations.
fn interruption(&mut self, max_delay: Option<usize>) -> StoreInterruption {
if self.init.is_dirty() {
// We only test that the store can power on without crashing. If it would get
// interrupted then it's like powering up with a different initial state, which would be
// tested with another fuzzing input.
return StoreInterruption::none();
}
let max_delay = match max_delay {
Some(x) => x,
None => return StoreInterruption::none(),
};
let delay = self.entropy.read_range(0, max_delay);
if self.debug {
if delay == max_delay {
println!("Do not interrupt.");
} else {
println!("Interrupt after {} operations.", delay);
}
}
if delay < max_delay {
self.increment(StatKey::InterruptionCount);
}
let corrupt = Box::new(move |old: &mut [u8], new: &[u8]| {
let mut count = 0;
let mut total = 0;
for (old, new) in old.iter_mut().zip(new.iter()) {
for bit in 0..8 {
let mask = 1 << bit;
if *old & mask == *new & mask {
continue;
}
total += 1;
if self.entropy.read_bit() {
count += 1;
*old ^= mask;
}
}
}
if self.debug {
println!("Flip {} bits out of {}.", count, total);
}
});
StoreInterruption { delay, corrupt }
}
}
/// The initial fuzzing mode.
enum Init {
/// Fuzzing starts from a clean storage.
///
/// All invariants are checked.
Clean,
/// Fuzzing starts from a dirty storage.
///
/// Only crashing is checked.
Dirty,
/// Fuzzing starts from a simulated old storage.
///
/// All invariants are checked.
Used {
/// Number of simulated used cycles.
cycle: usize,
},
}
impl Init {
/// Returns whether fuzzing is in dirty mode.
fn is_dirty(&self) -> bool {
match self {
Init::Dirty => true,
_ => false,
}
}
/// Returns the number of used cycles.
///
/// This is zero if the storage was not artificially aged.
fn used_cycles(&self) -> usize {
match self {
Init::Used { cycle } => *cycle,
_ => 0,
}
}
}

View File

@@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Flash storage for testing.
//!
//! [`BufferStorage`] implements the flash [`Storage`] interface but doesn't interface with an
//! actual flash storage. Instead it uses a buffer in memory to represent the storage state.
use crate::{Storage, StorageError, StorageIndex, StorageResult};
use alloc::borrow::Borrow;
use alloc::boxed::Box;
@@ -23,9 +28,9 @@ use alloc::vec;
/// for tests and fuzzing, for which it has dedicated functionalities.
///
/// This storage tracks how many times words are written between page erase cycles, how many times
/// pages are erased, and whether an operation flips bits in the wrong direction (optional).
/// Operations panic if those conditions are broken. This storage also permits to interrupt
/// operations for inspection or to corrupt the operation.
/// pages are erased, and whether an operation flips bits in the wrong direction. Operations panic
/// if those conditions are broken (optional). This storage also permits to interrupt operations for
/// inspection or to corrupt the operation.
#[derive(Clone)]
pub struct BufferStorage {
/// Content of the storage.
@@ -59,8 +64,13 @@ pub struct BufferOptions {
/// How many times a page can be erased.
pub max_page_erases: usize,
/// Whether bits cannot be written from 0 to 1.
pub strict_write: bool,
/// Whether the storage should check the flash invariant.
///
/// When set, the following conditions would panic:
/// - A bit is written from 0 to 1.
/// - A word is written more than [`Self::max_word_writes`].
/// - A page is erased more than [`Self::max_page_erases`].
pub strict_mode: bool,
}
/// Corrupts a slice given actual and expected value.
@@ -105,15 +115,13 @@ impl BufferStorage {
///
/// Before each subsequent mutable operation (write or erase), the delay is decremented if
/// positive. If the delay is elapsed, the operation is saved and an error is returned.
/// Subsequent operations will panic until the interrupted operation is [corrupted] or the
/// interruption is [reset].
/// Subsequent operations will panic until either of:
/// - The interrupted operation is [corrupted](BufferStorage::corrupt_operation).
/// - The interruption is [reset](BufferStorage::reset_interruption).
///
/// # Panics
///
/// Panics if an interruption is already armed.
///
/// [corrupted]: struct.BufferStorage.html#method.corrupt_operation
/// [reset]: struct.BufferStorage.html#method.reset_interruption
pub fn arm_interruption(&mut self, delay: usize) {
self.interruption.arm(delay);
}
@@ -125,10 +133,8 @@ impl BufferStorage {
/// # Panics
///
/// Panics if any of the following conditions hold:
/// - An interruption was not [armed].
/// - An interruption was not [armed](BufferStorage::arm_interruption).
/// - An interruption was armed and it has triggered.
///
/// [armed]: struct.BufferStorage.html#method.arm_interruption
pub fn disarm_interruption(&mut self) -> usize {
self.interruption.get().err().unwrap()
}
@@ -137,16 +143,14 @@ impl BufferStorage {
///
/// # Panics
///
/// Panics if an interruption was not [armed].
///
/// [armed]: struct.BufferStorage.html#method.arm_interruption
/// Panics if an interruption was not [armed](BufferStorage::arm_interruption).
pub fn reset_interruption(&mut self) {
let _ = self.interruption.get();
}
/// Corrupts an interrupted operation.
///
/// Applies the [corruption function] to the storage. Counters are updated accordingly:
/// Applies the corruption function to the storage. Counters are updated accordingly:
/// - If a word is fully written, its counter is incremented regardless of whether other words
/// of the same operation have been fully written.
/// - If a page is fully erased, its counter is incremented (and its word counters are reset).
@@ -154,13 +158,10 @@ impl BufferStorage {
/// # Panics
///
/// Panics if any of the following conditions hold:
/// - An interruption was not [armed].
/// - An interruption was not [armed](BufferStorage::arm_interruption).
/// - An interruption was armed but did not trigger.
/// - The corruption function corrupts more bits than allowed.
/// - The interrupted operation itself would have panicked.
///
/// [armed]: struct.BufferStorage.html#method.arm_interruption
/// [corruption function]: type.BufferCorruptFunction.html
pub fn corrupt_operation(&mut self, corrupt: BufferCorruptFunction) {
let operation = self.interruption.get().unwrap();
let range = self.operation_range(&operation).unwrap();
@@ -212,9 +213,13 @@ impl BufferStorage {
///
/// # Panics
///
/// Panics if the maximum number of erase cycles per page is reached.
/// Panics if the [maximum number of erase cycles per page](BufferOptions::max_page_erases) is
/// reached.
fn incr_page_erases(&mut self, page: usize) {
assert!(self.page_erases[page] < self.max_page_erases());
// Check that pages are not erased too many times.
if self.options.strict_mode {
assert!(self.page_erases[page] < self.max_page_erases());
}
self.page_erases[page] += 1;
let num_words = self.page_size() / self.word_size();
for word in 0..num_words {
@@ -235,7 +240,8 @@ impl BufferStorage {
///
/// # Panics
///
/// Panics if the maximum number of writes per word is reached.
/// Panics if the [maximum number of writes per word](BufferOptions::max_word_writes) is
/// reached.
fn incr_word_writes(&mut self, index: usize, value: &[u8], complete: &[u8]) {
let word_size = self.word_size();
for i in 0..value.len() / word_size {
@@ -252,7 +258,10 @@ impl BufferStorage {
continue;
}
let word = index / word_size + i;
assert!(self.word_writes[word] < self.max_word_writes());
// Check that words are not written too many times.
if self.options.strict_mode {
assert!(self.word_writes[word] < self.max_word_writes());
}
self.word_writes[word] += 1;
}
}
@@ -306,8 +315,8 @@ impl Storage for BufferStorage {
self.interruption.tick(&operation)?;
// Check and update counters.
self.incr_word_writes(range.start, value, value);
// Check strict write.
if self.options.strict_write {
// Check that bits are correctly flipped.
if self.options.strict_mode {
for (byte, &val) in range.clone().zip(value.iter()) {
assert_eq!(self.storage[byte] & val, val);
}
@@ -472,7 +481,7 @@ mod tests {
page_size: 16,
max_word_writes: 2,
max_page_erases: 3,
strict_write: true,
strict_mode: true,
};
// Those words are decreasing bit patterns. Bits are only changed from 1 to 0 and at least one
// bit is changed.

View File

@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Store wrapper for testing.
//!
//! [`StoreDriver`] wraps a [`Store`] and compares its behavior with its associated [`StoreModel`].
use crate::format::{Format, Position};
#[cfg(test)]
use crate::StoreUpdate;
@@ -181,6 +185,12 @@ pub enum StoreInvariant {
},
}
impl From<StoreError> for StoreInvariant {
fn from(error: StoreError) -> StoreInvariant {
StoreInvariant::StoreError(error)
}
}
impl StoreDriver {
/// Provides read-only access to the storage.
pub fn storage(&self) -> &BufferStorage {
@@ -249,6 +259,10 @@ impl StoreDriverOff {
}
/// Powers on the store without interruption.
///
/// # Panics
///
/// Panics if the store cannot be powered on.
pub fn power_on(self) -> Result<StoreDriverOn, StoreInvariant> {
Ok(self
.partial_power_on(StoreInterruption::none())
@@ -301,31 +315,15 @@ impl StoreDriverOff {
})
}
/// Returns a mapping from delay time to number of modified bits.
/// Returns the number of storage operations to power on.
///
/// For example if the `i`-th value is `n`, it means that the `i`-th operation modifies `n` bits
/// in the storage. For convenience, the vector always ends with `0` for one past the last
/// operation. This permits to choose a random index in the vector and then a random set of bit
/// positions among the number of modified bits to simulate any possible corruption (including
/// no corruption with the last index).
pub fn delay_map(&self) -> Result<Vec<usize>, (usize, BufferStorage)> {
let mut result = Vec::new();
loop {
let delay = result.len();
let mut storage = self.storage.clone();
storage.arm_interruption(delay);
match Store::new(storage) {
Err((StoreError::StorageError, x)) => storage = x,
Err((StoreError::InvalidStorage, mut storage)) => {
storage.reset_interruption();
return Err((delay, storage));
}
Ok(_) | Err(_) => break,
}
result.push(count_modified_bits(&mut storage));
}
result.push(0);
Ok(result)
/// Returns `None` if the store cannot power on successfully.
pub fn count_operations(&self) -> Option<usize> {
let initial_delay = usize::MAX;
let mut storage = self.storage.clone();
storage.arm_interruption(initial_delay);
let mut store = Store::new(storage).ok()?;
Some(initial_delay - store.storage_mut().disarm_interruption())
}
}
@@ -412,29 +410,15 @@ impl StoreDriverOn {
})
}
/// Returns a mapping from delay time to number of modified bits.
/// Returns the number of storage operations to apply a store operation.
///
/// See the documentation of [`StoreDriverOff::delay_map`] for details.
///
/// [`StoreDriverOff::delay_map`]: struct.StoreDriverOff.html#method.delay_map
pub fn delay_map(
&self,
operation: &StoreOperation,
) -> Result<Vec<usize>, (usize, BufferStorage)> {
let mut result = Vec::new();
loop {
let delay = result.len();
let mut store = self.store.clone();
store.storage_mut().arm_interruption(delay);
match store.apply(operation).1 {
Err(StoreError::StorageError) => (),
Err(StoreError::InvalidStorage) => return Err((delay, store.extract_storage())),
Ok(()) | Err(_) => break,
}
result.push(count_modified_bits(store.storage_mut()));
}
result.push(0);
Ok(result)
/// Returns `None` if the store cannot apply the operation successfully.
pub fn count_operations(&self, operation: &StoreOperation) -> Option<usize> {
let initial_delay = usize::MAX;
let mut store = self.store.clone();
store.storage_mut().arm_interruption(initial_delay);
store.apply(operation).1.ok()?;
Some(initial_delay - store.storage_mut().disarm_interruption())
}
/// Powers off the store.
@@ -506,8 +490,8 @@ impl StoreDriverOn {
/// Checks that the store and model are in sync.
fn check_model(&self) -> Result<(), StoreInvariant> {
let mut model_content = self.model.content().clone();
for handle in self.store.iter().unwrap() {
let handle = handle.unwrap();
for handle in self.store.iter()? {
let handle = handle?;
let model_value = match model_content.remove(&handle.get_key()) {
None => {
return Err(StoreInvariant::OnlyInStore {
@@ -516,7 +500,7 @@ impl StoreDriverOn {
}
Some(x) => x,
};
let store_value = handle.get_value(&self.store).unwrap().into_boxed_slice();
let store_value = handle.get_value(&self.store)?.into_boxed_slice();
if store_value != model_value {
return Err(StoreInvariant::DifferentValue {
key: handle.get_key(),
@@ -528,7 +512,7 @@ impl StoreDriverOn {
if let Some(&key) = model_content.keys().next() {
return Err(StoreInvariant::OnlyInModel { key });
}
let store_capacity = self.store.capacity().unwrap().remaining();
let store_capacity = self.store.capacity()?.remaining();
let model_capacity = self.model.capacity().remaining();
if store_capacity != model_capacity {
return Err(StoreInvariant::DifferentCapacity {
@@ -544,8 +528,8 @@ impl StoreDriverOn {
let format = self.model.format();
let storage = self.store.storage();
let num_words = format.page_size() / format.word_size();
let head = self.store.head().unwrap();
let tail = self.store.tail().unwrap();
let head = self.store.head()?;
let tail = self.store.tail()?;
for page in 0..format.num_pages() {
// Check the erase cycle of the page.
let store_erase = head.cycle(format) + (page < head.page(format)) as Nat;
@@ -619,22 +603,3 @@ impl<'a> StoreInterruption<'a> {
}
}
}
/// Counts the number of bits modified by an interrupted operation.
///
/// # Panics
///
/// Panics if an interruption did not trigger.
fn count_modified_bits(storage: &mut BufferStorage) -> usize {
let mut modified_bits = 0;
storage.corrupt_operation(Box::new(|before, after| {
modified_bits = before
.iter()
.zip(after.iter())
.map(|(x, y)| (x ^ y).count_ones() as usize)
.sum();
}));
// We should never write the same slice or erase an erased page.
assert!(modified_bits > 0);
modified_bits
}

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Storage representation of a store.
#[macro_use]
mod bitfield;
@@ -20,18 +22,20 @@ use self::bitfield::Length;
use self::bitfield::{count_zeros, num_bits, Bit, Checksum, ConstField, Field};
use crate::{usize_to_nat, Nat, Storage, StorageIndex, StoreError, StoreResult, StoreUpdate};
use alloc::vec::Vec;
use core::borrow::Borrow;
use core::cmp::min;
use core::convert::TryFrom;
/// Internal representation of a word in flash.
///
/// Currently, the store only supports storages where a word is 32 bits.
/// Currently, the store only supports storages where a word is 32 bits, i.e. the [word
/// size](Storage::word_size) is 4 bytes.
type WORD = u32;
/// Abstract representation of a word in flash.
///
/// This type is kept abstract to avoid possible confusion with `Nat` if they happen to have the
/// same representation. This is because they have different semantics, `Nat` represents natural
/// This type is kept abstract to avoid possible confusion with [`Nat`] if they happen to have the
/// same representation. This is because they have different semantics, [`Nat`] represents natural
/// numbers while `Word` represents sequences of bits (and thus has no arithmetic).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Word(WORD);
@@ -46,7 +50,7 @@ impl Word {
///
/// # Panics
///
/// Panics if `slice.len() != WORD_SIZE`.
/// Panics if `slice.len()` is not [`WORD_SIZE`] bytes.
pub fn from_slice(slice: &[u8]) -> Word {
Word(WORD::from_le_bytes(<WordSlice>::try_from(slice).unwrap()))
}
@@ -59,47 +63,49 @@ impl Word {
/// Size of a word in bytes.
///
/// Currently, the store only supports storages where a word is 4 bytes.
/// Currently, the store only supports storages where the [word size](Storage::word_size) is 4
/// bytes.
const WORD_SIZE: Nat = core::mem::size_of::<WORD>() as Nat;
/// Minimum number of words per page.
///
/// Currently, the store only supports storages where pages have at least 8 words.
const MIN_NUM_WORDS_PER_PAGE: Nat = 8;
/// Currently, the store only supports storages where pages have at least 8 [words](WORD_SIZE), i.e.
/// the [page size](Storage::page_size) is at least 32 bytes.
const MIN_PAGE_SIZE: Nat = 8;
/// Maximum size of a page in bytes.
///
/// Currently, the store only supports storages where pages are between 8 and 1024 [words].
///
/// [words]: constant.WORD_SIZE.html
/// Currently, the store only supports storages where pages have at most 1024 [words](WORD_SIZE),
/// i.e. the [page size](Storage::page_size) is at most 4096 bytes.
const MAX_PAGE_SIZE: Nat = 4096;
/// Maximum number of erase cycles.
///
/// Currently, the store only supports storages where the maximum number of erase cycles fits on 16
/// bits.
/// Currently, the store only supports storages where the [maximum number of erase
/// cycles](Storage::max_page_erases) fits in 16 bits, i.e. it is at most 65535.
const MAX_ERASE_CYCLE: Nat = 65535;
/// Minimum number of pages.
///
/// Currently, the store only supports storages with at least 3 pages.
/// Currently, the store only supports storages where the [number of pages](Storage::num_pages) is
/// at least 3.
const MIN_NUM_PAGES: Nat = 3;
/// Maximum page index.
///
/// Thus the maximum number of pages is one more than this number. Currently, the store only
/// supports storages where the number of pages is between 3 and 64.
/// Currently, the store only supports storages where the [number of pages](Storage::num_pages) is
/// at most 64, i.e. the maximum page index is 63.
const MAX_PAGE_INDEX: Nat = 63;
/// Maximum key index.
///
/// Thus the number of keys is one more than this number. Currently, the store only supports 4096
/// keys.
/// Currently, the store only supports 4096 keys, i.e. the maximum key index is 4095.
const MAX_KEY_INDEX: Nat = 4095;
/// Maximum length in bytes of a user payload.
///
/// Currently, the store only supports values smaller than 1024 bytes.
/// Currently, the store only supports values at most 1023 bytes long. This may be further reduced
/// depending on the [page size](Storage::page_size), see [`Format::max_value_len`].
const MAX_VALUE_LEN: Nat = 1023;
/// Maximum number of updates per transaction.
@@ -108,9 +114,15 @@ const MAX_VALUE_LEN: Nat = 1023;
const MAX_UPDATES: Nat = 31;
/// Maximum number of words per virtual page.
const MAX_VIRT_PAGE_SIZE: Nat = div_ceil(MAX_PAGE_SIZE, WORD_SIZE) - CONTENT_WORD;
///
/// A virtual page has [`CONTENT_WORD`] less [words](WORD_SIZE) than the storage [page
/// size](Storage::page_size). Those words are used to store the page header. Since a page has at
/// least [8](MIN_PAGE_SIZE) words, a virtual page has at least 6 words.
const MAX_VIRT_PAGE_SIZE: Nat = MAX_PAGE_SIZE / WORD_SIZE - CONTENT_WORD;
/// Word with all bits set to one.
///
/// After a page is erased, all words are equal to this value.
const ERASED_WORD: Word = Word(!(0 as WORD));
/// Helpers for a given storage configuration.
@@ -120,33 +132,31 @@ pub struct Format {
///
/// # Invariant
///
/// - Words divide a page evenly.
/// - There are at least 8 words in a page.
/// - There are at most `MAX_PAGE_SIZE` bytes in a page.
/// - [Words](WORD_SIZE) divide a page evenly.
/// - There are at least [`MIN_PAGE_SIZE`] words in a page.
/// - There are at most [`MAX_PAGE_SIZE`] bytes in a page.
page_size: Nat,
/// The number of pages in the storage.
///
/// # Invariant
///
/// - There are at least 3 pages.
/// - There are at most `MAX_PAGE_INDEX + 1` pages.
/// - There are at least [`MIN_NUM_PAGES`] pages.
/// - There are at most [`MAX_PAGE_INDEX`] + 1 pages.
num_pages: Nat,
/// The maximum number of times a page can be erased.
///
/// # Invariant
///
/// - A page can be erased at most `MAX_ERASE_CYCLE` times.
/// - A page can be erased at most [`MAX_ERASE_CYCLE`] times.
max_page_erases: Nat,
}
impl Format {
/// Extracts the format from a storage.
///
/// Returns `None` if the storage is not [supported].
///
/// [supported]: struct.Format.html#method.is_storage_supported
/// Returns `None` if the storage is not [supported](Format::is_storage_supported).
pub fn new<S: Storage>(storage: &S) -> Option<Format> {
if Format::is_storage_supported(storage) {
Some(Format {
@@ -162,21 +172,12 @@ impl Format {
/// Returns whether a storage is supported.
///
/// A storage is supported if the following conditions hold:
/// - The size of a word is [`WORD_SIZE`] bytes.
/// - The size of a word evenly divides the size of a page.
/// - A page contains at least [`MIN_NUM_WORDS_PER_PAGE`] words.
/// - A page contains at most [`MAX_PAGE_SIZE`] bytes.
/// - There are at least [`MIN_NUM_PAGES`] pages.
/// - There are at most [`MAX_PAGE_INDEX`]` + 1` pages.
/// - A word can be written at least twice between erase cycles.
/// - The maximum number of erase cycles is at most [`MAX_ERASE_CYCLE`].
///
/// [`WORD_SIZE`]: constant.WORD_SIZE.html
/// [`MIN_NUM_WORDS_PER_PAGE`]: constant.MIN_NUM_WORDS_PER_PAGE.html
/// [`MAX_PAGE_SIZE`]: constant.MAX_PAGE_SIZE.html
/// [`MIN_NUM_PAGES`]: constant.MIN_NUM_PAGES.html
/// [`MAX_PAGE_INDEX`]: constant.MAX_PAGE_INDEX.html
/// [`MAX_ERASE_CYCLE`]: constant.MAX_ERASE_CYCLE.html
/// - The [`Storage::word_size`] is [`WORD_SIZE`] bytes.
/// - The [`Storage::word_size`] evenly divides the [`Storage::page_size`].
/// - The [`Storage::page_size`] is between [`MIN_PAGE_SIZE`] words and [`MAX_PAGE_SIZE`] bytes.
/// - The [`Storage::num_pages`] is between [`MIN_NUM_PAGES`] and [`MAX_PAGE_INDEX`] + 1.
/// - The [`Storage::max_word_writes`] is at least 2.
/// - The [`Storage::max_page_erases`] is at most [`MAX_ERASE_CYCLE`].
fn is_storage_supported<S: Storage>(storage: &S) -> bool {
let word_size = usize_to_nat(storage.word_size());
let page_size = usize_to_nat(storage.page_size());
@@ -185,7 +186,7 @@ impl Format {
let max_page_erases = usize_to_nat(storage.max_page_erases());
word_size == WORD_SIZE
&& page_size % word_size == 0
&& (MIN_NUM_WORDS_PER_PAGE * word_size <= page_size && page_size <= MAX_PAGE_SIZE)
&& (MIN_PAGE_SIZE * word_size <= page_size && page_size <= MAX_PAGE_SIZE)
&& (MIN_NUM_PAGES <= num_pages && num_pages <= MAX_PAGE_INDEX + 1)
&& max_word_writes >= 2
&& max_page_erases <= MAX_ERASE_CYCLE
@@ -198,28 +199,28 @@ impl Format {
/// The size of a page in bytes.
///
/// We have `MIN_NUM_WORDS_PER_PAGE * self.word_size() <= self.page_size() <= MAX_PAGE_SIZE`.
/// This is at least [`MIN_PAGE_SIZE`] [words](WORD_SIZE) and at most [`MAX_PAGE_SIZE`] bytes.
pub fn page_size(&self) -> Nat {
self.page_size
}
/// The number of pages in the storage, denoted by `N`.
/// The number of pages in the storage, denoted by N.
///
/// We have `MIN_NUM_PAGES <= N <= MAX_PAGE_INDEX + 1`.
/// We have [`MIN_NUM_PAGES`] ≤ N ≤ [`MAX_PAGE_INDEX`] + 1.
pub fn num_pages(&self) -> Nat {
self.num_pages
}
/// The maximum page index.
///
/// We have `2 <= self.max_page() <= MAX_PAGE_INDEX`.
/// This is at least [`MIN_NUM_PAGES`] - 1 and at most [`MAX_PAGE_INDEX`].
pub fn max_page(&self) -> Nat {
self.num_pages - 1
}
/// The maximum number of times a page can be erased, denoted by `E`.
/// The maximum number of times a page can be erased, denoted by E.
///
/// We have `E <= MAX_ERASE_CYCLE`.
/// We have E ≤ [`MAX_ERASE_CYCLE`].
pub fn max_page_erases(&self) -> Nat {
self.max_page_erases
}
@@ -234,19 +235,18 @@ impl Format {
MAX_UPDATES
}
/// The size of a virtual page in words, denoted by `Q`.
/// The size of a virtual page in words, denoted by Q.
///
/// A virtual page is stored in a physical page after the page header.
///
/// We have `MIN_NUM_WORDS_PER_PAGE - 2 <= Q <= MAX_VIRT_PAGE_SIZE`.
/// We have [`MIN_PAGE_SIZE`] - 2 Q ≤ [`MAX_VIRT_PAGE_SIZE`].
pub fn virt_page_size(&self) -> Nat {
self.page_size() / self.word_size() - CONTENT_WORD
}
/// The maximum length in bytes of a user payload.
///
/// We have `(MIN_NUM_WORDS_PER_PAGE - 3) * self.word_size() <= self.max_value_len() <=
/// MAX_VALUE_LEN`.
/// This is at least [`MIN_PAGE_SIZE`] - 3 [words](WORD_SIZE) and at most [`MAX_VALUE_LEN`].
pub fn max_value_len(&self) -> Nat {
min(
(self.virt_page_size() - 1) * self.word_size(),
@@ -254,57 +254,50 @@ impl Format {
)
}
/// The maximum prefix length in words, denoted by `M`.
/// The maximum prefix length in words, denoted by M.
///
/// A prefix is the first words of a virtual page that belong to the last entry of the previous
/// virtual page. This happens because entries may overlap up to 2 virtual pages.
///
/// We have `MIN_NUM_WORDS_PER_PAGE - 3 <= M < Q`.
/// We have [`MIN_PAGE_SIZE`] - 3 M < Q.
pub fn max_prefix_len(&self) -> Nat {
self.bytes_to_words(self.max_value_len())
}
/// The total virtual capacity in words, denoted by `V`.
/// The total virtual capacity in words, denoted by V.
///
/// We have `V = (N - 1) * (Q - 1) - M`.
/// We have V = (N - 1) × (Q - 1) - M.
///
/// We can show `V >= (N - 2) * (Q - 1)` with the following steps:
/// - `M <= Q - 1` from `M < Q` from [`M`] definition
/// - `-M >= -(Q - 1)` from above
/// - `V >= (N - 1) * (Q - 1) - (Q - 1)` from `V` definition
///
/// [`M`]: struct.Format.html#method.max_prefix_len
/// We can show V (N - 2) × (Q - 1) with the following steps:
/// - M Q - 1 from M < Q from [M](Format::max_prefix_len)'s definition
/// - -M -(Q - 1) from above
/// - V (N - 1) × (Q - 1) - (Q - 1) from V's definition
pub fn virt_size(&self) -> Nat {
(self.num_pages() - 1) * (self.virt_page_size() - 1) - self.max_prefix_len()
}
/// The total user capacity in words, denoted by `C`.
/// The total user capacity in words, denoted by C.
///
/// We have `C = V - N = (N - 1) * (Q - 2) - M - 1`.
/// We have C = V - N = (N - 1) × (Q - 2) - M - 1.
///
/// We can show `C >= (N - 2) * (Q - 2) - 2` with the following steps:
/// - `V >= (N - 2) * (Q - 1)` from [`V`] definition
/// - `C >= (N - 2) * (Q - 1) - N` from `C` definition
/// - `(N - 2) * (Q - 1) - N = (N - 2) * (Q - 2) - 2` by calculus
///
/// [`V`]: struct.Format.html#method.virt_size
/// We can show C (N - 2) × (Q - 2) - 2 with the following steps:
/// - V (N - 2) × (Q - 1) from [V](Format::virt_size)'s definition
/// - C (N - 2) × (Q - 1) - N from C's definition
/// - (N - 2) × (Q - 1) - N = (N - 2) × (Q - 2) - 2 by calculus
pub fn total_capacity(&self) -> Nat {
// From the virtual capacity, we reserve N - 1 words for `Erase` entries and 1 word for a
// `Clear` entry.
self.virt_size() - self.num_pages()
}
/// The total virtual lifetime in words, denoted by `L`.
/// The total virtual lifetime in words, denoted by L.
///
/// We have `L = (E * N + N - 1) * Q`.
/// We have L = (E × N + N - 1) × Q.
pub fn total_lifetime(&self) -> Position {
Position::new(self, self.max_page_erases(), self.num_pages() - 1, 0)
}
/// Returns the word position of the first entry of a page.
///
/// The init info of the page must be provided to know where the first entry of the page
/// starts.
pub fn page_head(&self, init: InitInfo, page: Nat) -> Position {
Position::new(self, init.cycle, page, init.prefix)
}
@@ -335,12 +328,12 @@ impl Format {
}
/// Builds the storage representation of an init info.
pub fn build_init(&self, init: InitInfo) -> WordSlice {
pub fn build_init(&self, init: InitInfo) -> StoreResult<WordSlice> {
let mut word = ERASED_WORD;
INIT_CYCLE.set(&mut word, init.cycle);
INIT_PREFIX.set(&mut word, init.prefix);
WORD_CHECKSUM.set(&mut word, 0);
word.as_slice()
INIT_CYCLE.set(&mut word, init.cycle)?;
INIT_PREFIX.set(&mut word, init.prefix)?;
WORD_CHECKSUM.set(&mut word, 0)?;
Ok(word.as_slice())
}
/// Returns the storage index of the compact info of a page.
@@ -368,36 +361,36 @@ impl Format {
}
/// Builds the storage representation of a compact info.
pub fn build_compact(&self, compact: CompactInfo) -> WordSlice {
pub fn build_compact(&self, compact: CompactInfo) -> StoreResult<WordSlice> {
let mut word = ERASED_WORD;
COMPACT_TAIL.set(&mut word, compact.tail);
WORD_CHECKSUM.set(&mut word, 0);
word.as_slice()
COMPACT_TAIL.set(&mut word, compact.tail)?;
WORD_CHECKSUM.set(&mut word, 0)?;
Ok(word.as_slice())
}
/// Builds the storage representation of an internal entry.
pub fn build_internal(&self, internal: InternalEntry) -> WordSlice {
pub fn build_internal(&self, internal: InternalEntry) -> StoreResult<WordSlice> {
let mut word = ERASED_WORD;
match internal {
InternalEntry::Erase { page } => {
ID_ERASE.set(&mut word);
ERASE_PAGE.set(&mut word, page);
ID_ERASE.set(&mut word)?;
ERASE_PAGE.set(&mut word, page)?;
}
InternalEntry::Clear { min_key } => {
ID_CLEAR.set(&mut word);
CLEAR_MIN_KEY.set(&mut word, min_key);
ID_CLEAR.set(&mut word)?;
CLEAR_MIN_KEY.set(&mut word, min_key)?;
}
InternalEntry::Marker { count } => {
ID_MARKER.set(&mut word);
MARKER_COUNT.set(&mut word, count);
ID_MARKER.set(&mut word)?;
MARKER_COUNT.set(&mut word, count)?;
}
InternalEntry::Remove { key } => {
ID_REMOVE.set(&mut word);
REMOVE_KEY.set(&mut word, key);
ID_REMOVE.set(&mut word)?;
REMOVE_KEY.set(&mut word, key)?;
}
}
WORD_CHECKSUM.set(&mut word, 0);
word.as_slice()
WORD_CHECKSUM.set(&mut word, 0)?;
Ok(word.as_slice())
}
/// Parses the first word of an entry from its storage representation.
@@ -459,31 +452,31 @@ impl Format {
}
/// Builds the storage representation of a user entry.
pub fn build_user(&self, key: Nat, value: &[u8]) -> Vec<u8> {
pub fn build_user(&self, key: Nat, value: &[u8]) -> StoreResult<Vec<u8>> {
let length = usize_to_nat(value.len());
let word_size = self.word_size();
let footer = self.bytes_to_words(length);
let mut result = vec![0xff; ((1 + footer) * word_size) as usize];
result[word_size as usize..][..length as usize].copy_from_slice(value);
let mut word = ERASED_WORD;
ID_HEADER.set(&mut word);
ID_HEADER.set(&mut word)?;
if footer > 0 && is_erased(&result[(footer * word_size) as usize..]) {
HEADER_FLIPPED.set(&mut word);
*result.last_mut().unwrap() = 0x7f;
}
HEADER_LENGTH.set(&mut word, length);
HEADER_KEY.set(&mut word, key);
HEADER_LENGTH.set(&mut word, length)?;
HEADER_KEY.set(&mut word, key)?;
HEADER_CHECKSUM.set(
&mut word,
count_zeros(&result[(footer * word_size) as usize..]),
);
)?;
result[..word_size as usize].copy_from_slice(&word.as_slice());
result
Ok(result)
}
/// Sets the padding bit in the first word of a user entry.
pub fn set_padding(&self, word: &mut Word) {
ID_PADDING.set(word);
pub fn set_padding(&self, word: &mut Word) -> StoreResult<()> {
ID_PADDING.set(word)
}
/// Sets the deleted bit in the first word of a user entry.
@@ -492,13 +485,16 @@ impl Format {
}
/// Returns the capacity required by a transaction.
pub fn transaction_capacity(&self, updates: &[StoreUpdate]) -> Nat {
pub fn transaction_capacity<ByteSlice: Borrow<[u8]>>(
&self,
updates: &[StoreUpdate<ByteSlice>],
) -> Nat {
match updates.len() {
// An empty transaction doesn't consume anything.
0 => 0,
// Transactions with a single update are optimized by avoiding a marker entry.
1 => match &updates[0] {
StoreUpdate::Insert { value, .. } => self.entry_size(value),
StoreUpdate::Insert { value, .. } => self.entry_size(value.borrow()),
// Transactions with a single update which is a removal don't consume anything.
StoreUpdate::Remove { .. } => 0,
},
@@ -508,9 +504,9 @@ impl Format {
}
/// Returns the capacity of an update.
fn update_capacity(&self, update: &StoreUpdate) -> Nat {
fn update_capacity<ByteSlice: Borrow<[u8]>>(&self, update: &StoreUpdate<ByteSlice>) -> Nat {
match update {
StoreUpdate::Insert { value, .. } => self.entry_size(value),
StoreUpdate::Insert { value, .. } => self.entry_size(value.borrow()),
StoreUpdate::Remove { .. } => 1,
}
}
@@ -523,7 +519,10 @@ impl Format {
/// Checks if a transaction is valid and returns its sorted keys.
///
/// Returns `None` if the transaction is invalid.
pub fn transaction_valid(&self, updates: &[StoreUpdate]) -> Option<Vec<Nat>> {
pub fn transaction_valid<ByteSlice: Borrow<[u8]>>(
&self,
updates: &[StoreUpdate<ByteSlice>],
) -> Option<Vec<Nat>> {
if usize_to_nat(updates.len()) > self.max_updates() {
return None;
}
@@ -550,7 +549,7 @@ impl Format {
///
/// # Preconditions
///
/// - `bytes + self.word_size()` does not overflow.
/// - `bytes` + [`Self::word_size`] does not overflow.
pub fn bytes_to_words(&self, bytes: Nat) -> Nat {
div_ceil(bytes, self.word_size())
}
@@ -564,7 +563,7 @@ const COMPACT_WORD: Nat = 1;
/// The word index of the content of a page.
///
/// Since a page is at least 8 words, there is always at least 6 words of content.
/// This is also the length in words of the page header.
const CONTENT_WORD: Nat = 2;
/// The checksum for a single word.
@@ -711,21 +710,21 @@ bitfield! {
/// The position of a word in the virtual storage.
///
/// With the notations defined in `Format`, let:
/// - `w` a virtual word offset in a page which is between `0` and `Q - 1`
/// - `p` a page offset which is between `0` and `N - 1`
/// - `c` the number of erase cycles of a page which is between `0` and `E`
/// With the notations defined in [`Format`], let:
/// - w denote a word offset in a virtual page, thus between 0 and Q - 1
/// - p denote a page offset, thus between 0 and N - 1
/// - c denote the number of times a page was erased, thus between 0 and E
///
/// Then the position of a word is `(c*N + p)*Q + w`. This position monotonically increases and
/// The position of a word is (c × N + p) × Q + w. This position monotonically increases and
/// represents the consumed lifetime of the storage.
///
/// This type is kept abstract to avoid possible confusion with `Nat` and `Word` if they happen to
/// have the same representation. Here is an overview of their semantics:
/// This type is kept abstract to avoid possible confusion with [`Nat`] and [`Word`] if they happen
/// to have the same representation. Here is an overview of their semantics:
///
/// | Name | Semantics | Arithmetic operations | Bit-wise operations |
/// | ---------- | --------------------------- | --------------------- | ------------------- |
/// | `Nat` | Natural numbers | Yes (no overflow) | No |
/// | `Word` | Word in flash | No | Yes |
/// | [`Nat`] | Natural numbers | Yes (no overflow) | No |
/// | [`Word`] | Word in flash | No | Yes |
/// | `Position` | Position in virtual storage | Yes (no overflow) | No |
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Position(Nat);
@@ -756,9 +755,9 @@ impl Position {
/// Create a word position given its coordinates.
///
/// The coordinates of a word are:
/// - Its word index in its page.
/// - Its word index in its virtual page.
/// - Its page index in the storage.
/// - The number of times that page was erased.
/// - The number of times its page was erased.
pub fn new(format: &Format, cycle: Nat, page: Nat, word: Nat) -> Position {
Position((cycle * format.num_pages() + page) * format.virt_page_size() + word)
}
@@ -921,11 +920,11 @@ pub fn is_erased(slice: &[u8]) -> bool {
/// Divides then takes ceiling.
///
/// Returns `ceil(x / m)` in mathematical notations (not Rust code).
/// Returns ⌈x / m⌉, i.e. the lowest natural number r such that r ≥ x / m.
///
/// # Preconditions
///
/// - `x + m` does not overflow.
/// - x + m does not overflow.
const fn div_ceil(x: Nat, m: Nat) -> Nat {
(x + m - 1) / m
}
@@ -1077,4 +1076,15 @@ mod tests {
0xff800000
);
}
#[test]
fn position_offsets_fit_in_a_halfword() {
// The store stores in RAM the entry positions as their offset from the head. Those offsets
// are represented as u16. The bound below is a large over-approximation of the maximal
// offset. We first make sure it fits in a u16.
const MAX_POS: Nat = (MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE;
assert!(MAX_POS <= u16::MAX as Nat);
// We also check the actual value for up-to-date documentation, since it's a constant.
assert_eq!(MAX_POS, 0xff80);
}
}

View File

@@ -42,15 +42,20 @@ impl Field {
/// Sets the value of a bit field.
///
/// # Preconditions
/// # Errors
///
/// - The value must fit in the bit field: `num_bits(value) < self.len`.
/// - The value must only change bits from 1 to 0: `self.get(*word) & value == value`.
pub fn set(&self, word: &mut Word, value: Nat) {
debug_assert_eq!(value & self.mask(), value);
pub fn set(&self, word: &mut Word, value: Nat) -> StoreResult<()> {
if value & self.mask() != value {
return Err(StoreError::InvalidStorage);
}
let mask = !(self.mask() << self.pos);
word.0 &= mask | (value << self.pos);
debug_assert_eq!(self.get(*word), value);
if self.get(*word) != value {
return Err(StoreError::InvalidStorage);
}
Ok(())
}
/// Returns a bit mask the length of the bit field.
@@ -82,8 +87,8 @@ impl ConstField {
}
/// Sets the bit field to its value.
pub fn set(&self, word: &mut Word) {
self.field.set(word, self.value);
pub fn set(&self, word: &mut Word) -> StoreResult<()> {
self.field.set(word, self.value)
}
}
@@ -135,15 +140,15 @@ impl Checksum {
/// Sets the checksum to the external increment value.
///
/// # Preconditions
/// # Errors
///
/// - The bits of the checksum bit field should be set to one: `self.field.get(*word) ==
/// self.field.mask()`.
/// - The checksum value should fit in the checksum bit field: `num_bits(word.count_zeros() +
/// value) < self.field.len`.
pub fn set(&self, word: &mut Word, value: Nat) {
pub fn set(&self, word: &mut Word, value: Nat) -> StoreResult<()> {
debug_assert_eq!(self.field.get(*word), self.field.mask());
self.field.set(word, word.0.count_zeros() + value);
self.field.set(word, word.0.count_zeros() + value)
}
}
@@ -290,7 +295,7 @@ mod tests {
assert_eq!(field.get(Word(0x000000f8)), 0x1f);
assert_eq!(field.get(Word(0x0000ff37)), 6);
let mut word = Word(0xffffffff);
field.set(&mut word, 3);
field.set(&mut word, 3).unwrap();
assert_eq!(word, Word(0xffffff1f));
}
@@ -305,7 +310,7 @@ mod tests {
assert!(field.check(Word(0x00000048)));
assert!(field.check(Word(0x0000ff4f)));
let mut word = Word(0xffffffff);
field.set(&mut word);
field.set(&mut word).unwrap();
assert_eq!(word, Word(0xffffff4f));
}
@@ -333,7 +338,7 @@ mod tests {
assert_eq!(field.get(Word(0x00ffff67)), Ok(4));
assert_eq!(field.get(Word(0x7fffff07)), Err(StoreError::InvalidStorage));
let mut word = Word(0x0fffffff);
field.set(&mut word, 4);
field.set(&mut word, 4).unwrap();
assert_eq!(word, Word(0x0fffff47));
}

View File

@@ -0,0 +1,345 @@
// Copyright 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.
//! Support for fragmented entries.
//!
//! This module permits to handle entries larger than the [maximum value
//! length](Store::max_value_length) by storing ordered consecutive fragments in a sequence of keys.
//! The first keys hold fragments of maximal length, followed by a possibly partial fragment. The
//! remaining keys are not used.
use crate::{Storage, Store, StoreError, StoreHandle, StoreResult, StoreUpdate};
use alloc::vec::Vec;
use core::ops::Range;
/// Represents a sequence of keys.
#[allow(clippy::len_without_is_empty)]
pub trait Keys {
/// Returns the number of keys.
fn len(&self) -> usize;
/// Returns the position of a key in the sequence.
fn pos(&self, key: usize) -> Option<usize>;
/// Returns the key of a position in the sequence.
///
/// # Preconditions
///
/// The position must be within the length: `pos` < [`Self::len`].
fn key(&self, pos: usize) -> usize;
}
impl Keys for Range<usize> {
fn len(&self) -> usize {
self.end - self.start
}
fn pos(&self, key: usize) -> Option<usize> {
if self.start <= key && key < self.end {
Some(key - self.start)
} else {
None
}
}
fn key(&self, pos: usize) -> usize {
debug_assert!(pos < Keys::len(self));
self.start + pos
}
}
/// Reads the concatenated value of a sequence of keys.
pub fn read(store: &Store<impl Storage>, keys: &impl Keys) -> StoreResult<Option<Vec<u8>>> {
let handles = get_handles(store, keys)?;
if handles.is_empty() {
return Ok(None);
}
let mut result = Vec::with_capacity(handles.len() * store.max_value_length());
for handle in handles {
result.extend(handle.get_value(store)?);
}
Ok(Some(result))
}
/// Reads a range from the concatenated value of a sequence of keys.
///
/// This is equivalent to calling [`read`] then taking the range except that:
/// - Only the needed chunks are read.
/// - The range is truncated to fit in the value.
pub fn read_range(
store: &Store<impl Storage>,
keys: &impl Keys,
range: Range<usize>,
) -> StoreResult<Option<Vec<u8>>> {
let range_len = match range.end.checked_sub(range.start) {
None => return Err(StoreError::InvalidArgument),
Some(x) => x,
};
let handles = get_handles(store, keys)?;
if handles.is_empty() {
return Ok(None);
}
let mut result = Vec::with_capacity(range_len);
let mut offset = 0;
for handle in handles {
let start = range.start.saturating_sub(offset);
let length = handle.get_length(store)?;
let end = core::cmp::min(range.end.saturating_sub(offset), length);
offset += length;
if start < end {
result.extend(&handle.get_value(store)?[start..end]);
}
}
Ok(Some(result))
}
/// Writes a value to a sequence of keys as chunks.
pub fn write(store: &mut Store<impl Storage>, keys: &impl Keys, value: &[u8]) -> StoreResult<()> {
let handles = get_handles(store, keys)?;
let keys_len = keys.len();
let mut updates = Vec::with_capacity(keys_len);
let mut chunks = value.chunks(store.max_value_length());
for pos in 0..keys_len {
let key = keys.key(pos);
match (handles.get(pos), chunks.next()) {
// No existing handle and no new chunk: nothing to do.
(None, None) => (),
// Existing handle and no new chunk: remove old handle.
(Some(_), None) => updates.push(StoreUpdate::Remove { key }),
// Existing handle with same value as new chunk: nothing to do.
(Some(handle), Some(value)) if handle.get_value(store)? == value => (),
// New chunk: Write (or overwrite) the new value.
(_, Some(value)) => updates.push(StoreUpdate::Insert { key, value }),
}
}
if chunks.next().is_some() {
// The value is too long.
return Err(StoreError::InvalidArgument);
}
store.transaction(&updates)
}
/// Deletes the value of a sequence of keys.
pub fn delete(store: &mut Store<impl Storage>, keys: &impl Keys) -> StoreResult<()> {
let updates: Vec<StoreUpdate<Vec<u8>>> = get_handles(store, keys)?
.iter()
.map(|handle| StoreUpdate::Remove {
key: handle.get_key(),
})
.collect();
store.transaction(&updates)
}
/// Returns the handles of a sequence of keys.
///
/// The handles are truncated to the keys that are present.
fn get_handles(store: &Store<impl Storage>, keys: &impl Keys) -> StoreResult<Vec<StoreHandle>> {
let keys_len = keys.len();
let mut handles: Vec<Option<StoreHandle>> = vec![None; keys_len as usize];
for handle in store.iter()? {
let handle = handle?;
let pos = match keys.pos(handle.get_key()) {
Some(pos) => pos,
None => continue,
};
if pos >= keys_len {
return Err(StoreError::InvalidArgument);
}
if let Some(old_handle) = &handles[pos] {
if old_handle.get_key() != handle.get_key() {
// The user provided a non-injective `pos` function.
return Err(StoreError::InvalidArgument);
} else {
return Err(StoreError::InvalidStorage);
}
}
handles[pos] = Some(handle);
}
let num_handles = handles.iter().filter(|x| x.is_some()).count();
let mut result = Vec::with_capacity(num_handles);
for (i, handle) in handles.into_iter().enumerate() {
match (i < num_handles, handle) {
(true, Some(handle)) => result.push(handle),
(false, None) => (),
// We should have `num_handles` Somes followed by Nones.
_ => return Err(StoreError::InvalidStorage),
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::MINIMAL;
#[test]
fn read_empty_entry() {
let store = MINIMAL.new_store();
assert_eq!(read(&store, &(0..4)), Ok(None));
}
#[test]
fn read_single_chunk() {
let mut store = MINIMAL.new_store();
let value = b"hello".to_vec();
assert_eq!(store.insert(0, &value), Ok(()));
assert_eq!(read(&store, &(0..4)), Ok(Some(value)));
}
#[test]
fn read_multiple_chunks() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
assert_eq!(read(&store, &(0..4)), Ok(Some(value)));
}
#[test]
fn read_range_first_chunk() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
assert_eq!(
read_range(&store, &(0..4), 0..10),
Ok(Some((0..10).collect()))
);
assert_eq!(
read_range(&store, &(0..4), 10..20),
Ok(Some((10..20).collect()))
);
assert_eq!(
read_range(&store, &(0..4), 40..52),
Ok(Some((40..52).collect()))
);
}
#[test]
fn read_range_second_chunk() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
assert_eq!(read_range(&store, &(0..4), 52..53), Ok(Some(vec![52])));
assert_eq!(read_range(&store, &(0..4), 53..54), Ok(Some(vec![53])));
assert_eq!(read_range(&store, &(0..4), 59..60), Ok(Some(vec![59])));
}
#[test]
fn read_range_both_chunks() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
assert_eq!(
read_range(&store, &(0..4), 40..60),
Ok(Some((40..60).collect()))
);
assert_eq!(
read_range(&store, &(0..4), 0..60),
Ok(Some((0..60).collect()))
);
}
#[test]
fn read_range_outside() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
assert_eq!(
read_range(&store, &(0..4), 40..100),
Ok(Some((40..60).collect()))
);
assert_eq!(read_range(&store, &(0..4), 60..100), Ok(Some(vec![])));
}
#[test]
fn write_single_chunk() {
let mut store = MINIMAL.new_store();
let value = b"hello".to_vec();
assert_eq!(write(&mut store, &(0..4), &value), Ok(()));
assert_eq!(store.find(0), Ok(Some(value)));
assert_eq!(store.find(1), Ok(None));
assert_eq!(store.find(2), Ok(None));
assert_eq!(store.find(3), Ok(None));
}
#[test]
fn write_multiple_chunks() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(write(&mut store, &(0..4), &value), Ok(()));
assert_eq!(store.find(0), Ok(Some((0..52).collect())));
assert_eq!(store.find(1), Ok(Some((52..60).collect())));
assert_eq!(store.find(2), Ok(None));
assert_eq!(store.find(3), Ok(None));
}
#[test]
fn overwrite_less_chunks() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
let value: Vec<_> = (42..69).collect();
assert_eq!(write(&mut store, &(0..4), &value), Ok(()));
assert_eq!(store.find(0), Ok(Some((42..69).collect())));
assert_eq!(store.find(1), Ok(None));
assert_eq!(store.find(2), Ok(None));
assert_eq!(store.find(3), Ok(None));
}
#[test]
fn overwrite_needed_chunks() {
let mut store = MINIMAL.new_store();
let mut value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
// Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data.
let mut lifetime = 2 + 60 / 4;
assert_eq!(store.lifetime().unwrap().used(), lifetime);
// Update the value.
value.extend(60..80);
assert_eq!(write(&mut store, &(0..4), &value), Ok(()));
// Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data.
lifetime += 1 + (80 - 52) / 4;
assert_eq!(store.lifetime().unwrap().used(), lifetime);
}
#[test]
fn delete_empty() {
let mut store = MINIMAL.new_store();
assert_eq!(delete(&mut store, &(0..4)), Ok(()));
assert_eq!(store.find(0), Ok(None));
assert_eq!(store.find(1), Ok(None));
assert_eq!(store.find(2), Ok(None));
assert_eq!(store.find(3), Ok(None));
}
#[test]
fn delete_chunks() {
let mut store = MINIMAL.new_store();
let value: Vec<_> = (0..60).collect();
assert_eq!(store.insert(0, &value[..52]), Ok(()));
assert_eq!(store.insert(1, &value[52..]), Ok(()));
assert_eq!(delete(&mut store, &(0..4)), Ok(()));
assert_eq!(store.find(0), Ok(None));
assert_eq!(store.find(1), Ok(None));
assert_eq!(store.find(2), Ok(None));
assert_eq!(store.find(3), Ok(None));
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019-2020 Google LLC
// Copyright 2019-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.
@@ -12,191 +12,191 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO(ia0): Add links once the code is complete.
// The documentation is easier to read from a browser:
// - Run: cargo doc --document-private-items --features=std
// - Open: target/doc/persistent_store/index.html
//! Store abstraction for flash storage
//!
//! # Specification
//!
//! The store provides a partial function from keys to values on top of a storage
//! interface. The store total capacity depends on the size of the storage. Store
//! updates may be bundled in transactions. Mutable operations are atomic, including
//! when interrupted.
//! The [store](Store) provides a partial function from keys to values on top of a
//! [storage](Storage) interface. The store total [capacity](Store::capacity) depends on the size of
//! the storage. Store [updates](StoreUpdate) may be bundled in [transactions](Store::transaction).
//! Mutable operations are atomic, including when interrupted.
//!
//! The store is flash-efficient in the sense that it uses the storage lifetime
//! efficiently. For each page, all words are written at least once between erase
//! cycles and all erase cycles are used. However, not all written words are user
//! content: lifetime is also consumed with metadata and compaction.
//! The store is flash-efficient in the sense that it uses the storage [lifetime](Store::lifetime)
//! efficiently. For each page, all words are written at least once between erase cycles and all
//! erase cycles are used. However, not all written words are user content: Lifetime is also
//! consumed with metadata and compaction.
//!
//! The store is extendable with other entries than key-values. It is essentially a
//! framework providing access to the storage lifetime. The partial function is
//! simply the most common usage and can be used to encode other usages.
//! The store is extendable with other entries than key-values. It is essentially a framework
//! providing access to the storage lifetime. The partial function is simply the most common usage
//! and can be used to encode other usages.
//!
//! ## Definitions
//!
//! An _entry_ is a pair of a key and a value. A _key_ is a number between 0
//! and 4095. A _value_ is a byte slice with a length between 0 and 1023 bytes (for
//! large enough pages).
//! An _entry_ is a pair of a key and a value. A _key_ is a number between 0 and
//! [4095](format::MAX_KEY_INDEX). A _value_ is a byte slice with a length between 0 and
//! [1023](format::Format::max_value_len) bytes (for large enough pages).
//!
//! The store provides the following _updates_:
//! - Given a key and a value, `Insert` updates the store such that the value is
//! - Given a key and a value, [`StoreUpdate::Insert`] updates the store such that the value is
//! associated with the key. The values for other keys are left unchanged.
//! - Given a key, `Remove` updates the store such that no value is associated with
//! the key. The values for other keys are left unchanged. Additionally, if there
//! was a value associated with the key, the value is wiped from the storage
//! (all its bits are set to 0).
//! - Given a key, [`StoreUpdate::Remove`] updates the store such that no value is associated with
//! the key. The values for other keys are left unchanged. Additionally, if there was a value
//! associated with the key, the value is wiped from the storage (all its bits are set to 0).
//!
//! The store provides the following _read-only operations_:
//! - `Iter` iterates through the store returning all entries exactly once. The
//! iteration order is not specified but stable between mutable operations.
//! - `Capacity` returns how many words can be stored before the store is full.
//! - `Lifetime` returns how many words can be written before the storage lifetime
//! is consumed.
//! - [`Store::iter`] iterates through the store returning all entries exactly once. The iteration
//! order is not specified but stable between mutable operations.
//! - [`Store::capacity`] returns how many words can be stored before the store is full.
//! - [`Store::lifetime`] returns how many words can be written before the storage lifetime is
//! consumed.
//!
//! The store provides the following _mutable operations_:
//! - Given a set of independent updates, `Transaction` applies the sequence of
//! updates.
//! - Given a threshold, `Clear` removes all entries with a key greater or equal
//! to the threshold.
//! - Given a length in words, `Prepare` makes one step of compaction unless that
//! many words can be written without compaction. This operation has no effect
//! on the store but may still mutate its storage. In particular, the store has
//! the same capacity but a possibly reduced lifetime.
//! - Given a set of independent updates, [`Store::transaction`] applies the sequence of updates.
//! - Given a threshold, [`Store::clear`] removes all entries with a key greater or equal to the
//! threshold.
//! - Given a length in words, [`Store::prepare`] makes one step of compaction unless that many
//! words can be written without compaction. This operation has no effect on the store but may
//! still mutate its storage. In particular, the store has the same capacity but a possibly
//! reduced lifetime.
//!
//! A mutable operation is _atomic_ if, when power is lost during the operation, the
//! store is either updated (as if the operation succeeded) or left unchanged (as if
//! the operation did not occur). If the store is left unchanged, lifetime may still
//! be consumed.
//! A mutable operation is _atomic_ if, when power is lost during the operation, the store is either
//! updated (as if the operation succeeded) or left unchanged (as if the operation did not occur).
//! If the store is left unchanged, lifetime may still be consumed.
//!
//! The store relies on the following _storage interface_:
//! - It is possible to read a byte slice. The slice won't span multiple pages.
//! - It is possible to write a word slice. The slice won't span multiple pages.
//! - It is possible to erase a page.
//! - The pages are sequentially indexed from 0. If the actual underlying storage
//! is segmented, then the storage layer should translate those indices to
//! actual page addresses.
//! - It is possible to [read](Storage::read_slice) a byte slice. The slice won't span multiple
//! pages.
//! - It is possible to [write](Storage::write_slice) a word slice. The slice won't span multiple
//! pages.
//! - It is possible to [erase](Storage::erase_page) a page.
//! - The pages are sequentially indexed from 0. If the actual underlying storage is segmented,
//! then the storage layer should translate those indices to actual page addresses.
//!
//! The store has a _total capacity_ of `C = (N - 1) * (P - 4) - M - 1` words, where
//! `P` is the number of words per page, `N` is the number of pages, and `M` is the
//! maximum length in words of a value (256 for large enough pages). The capacity
//! used by each mutable operation is given below (a transient word only uses
//! capacity during the operation):
//! - `Insert` uses `1 + ceil(len / 4)` words where `len` is the length of the
//! value in bytes. If an entry was replaced, the words used by its insertion
//! are freed.
//! - `Remove` doesn't use capacity if alone in the transaction and 1 transient
//! word otherwise. If an entry was deleted, the words used by its insertion are
//! freed.
//! - `Transaction` uses 1 transient word. In addition, the updates of the
//! transaction use and free words as described above.
//! - `Clear` doesn't use capacity and frees the words used by the insertion of
//! the deleted entries.
//! - `Prepare` doesn't use capacity.
//! The store has a _total capacity_ of C = (N - 1) × (P - 4) - M - 1 words, where:
//! - P is the number of words per page
//! - [N](format::Format::num_pages) is the number of pages
//! - [M](format::Format::max_prefix_len) is the maximum length in words of a value (256 for large
//! enough pages)
//!
//! The _total lifetime_ of the store is below `L = ((E + 1) * N - 1) * (P - 2)` and
//! above `L - M` words, where `E` is the maximum number of erase cycles. The
//! lifetime is used when capacity is used, including transiently, as well as when
//! compaction occurs. Compaction frequency and lifetime consumption are positively
//! correlated to the store load factor (the ratio of used capacity to total capacity).
//! The capacity used by each mutable operation is given below (a transient word only uses capacity
//! during the operation):
//!
//! It is possible to approximate the cost of transient words in terms of capacity:
//! `L` transient words are equivalent to `C - x` words of capacity where `x` is the
//! average capacity (including transient) of operations.
//! | Operation/Update | Used capacity | Freed capacity | Transient capacity |
//! | ----------------------- | ---------------- | ----------------- | ------------------ |
//! | [`StoreUpdate::Insert`] | 1 + value length | overwritten entry | 0 |
//! | [`StoreUpdate::Remove`] | 0 | deleted entry | see below\* |
//! | [`Store::transaction`] | 0 + updates | 0 + updates | 1 |
//! | [`Store::clear`] | 0 | deleted entries | 0 |
//! | [`Store::prepare`] | 0 | 0 | 0 |
//!
//! \*0 if the update is alone in the transaction, otherwise 1.
//!
//! The _total lifetime_ of the store is below L = ((E + 1) × N - 1) × (P - 2) and above L - M
//! words, where E is the maximum number of erase cycles. The lifetime is used when capacity is
//! used, including transiently, as well as when compaction occurs. Compaction frequency and
//! lifetime consumption are positively correlated to the store load factor (the ratio of used
//! capacity to total capacity).
//!
//! It is possible to approximate the cost of transient words in terms of capacity: L transient
//! words are equivalent to C - x words of capacity where x is the average capacity (including
//! transient) of operations.
//!
//! ## Preconditions
//!
//! The following assumptions need to hold, or the store may behave in unexpected ways:
//! - A word can be written twice between erase cycles.
//! - A page can be erased `E` times after the first boot of the store.
//! - When power is lost while writing a slice or erasing a page, the next read
//! returns a slice where a subset (possibly none or all) of the bits that
//! should have been modified have been modified.
//! - Reading a slice is deterministic. When power is lost while writing a slice
//! or erasing a slice (erasing a page containing that slice), reading that
//! slice repeatedly returns the same result (until it is overwritten or its
//! page is erased).
//! - To decide whether a page has been erased, it is enough to test if all its
//! bits are equal to 1.
//! - When power is lost while writing a slice or erasing a page, that operation
//! does not count towards the limits. However, completing that write or erase
//! operation would count towards the limits, as if the number of writes per
//! word and number of erase cycles could be fractional.
//! - The storage is only modified by the store. Note that completely erasing the
//! storage is supported, essentially losing all content and lifetime tracking.
//! It is preferred to use `Clear` with a threshold of 0 to keep the lifetime
//! tracking.
//! - A word can be written [twice](Storage::max_word_writes) between erase cycles.
//! - A page can be erased [E](Storage::max_page_erases) times after the first boot of the store.
//! - When power is lost while writing a slice or erasing a page, the next read returns a slice
//! where a subset (possibly none or all) of the bits that should have been modified have been
//! modified.
//! - Reading a slice is deterministic. When power is lost while writing a slice or erasing a
//! slice (erasing a page containing that slice), reading that slice repeatedly returns the same
//! result (until it is overwritten or its page is erased).
//! - To decide whether a page has been erased, it is enough to test if all its bits are equal
//! to 1.
//! - When power is lost while writing a slice or erasing a page, that operation does not count
//! towards the limits. However, completing that write or erase operation would count towards
//! the limits, as if the number of writes per word and number of erase cycles could be
//! fractional.
//! - The storage is only modified by the store. Note that completely erasing the storage is
//! supported, essentially losing all content and lifetime tracking. It is preferred to use
//! [`Store::clear`] with a threshold of 0 to keep the lifetime tracking.
//!
//! The store properties may still hold outside some of those assumptions, but with
//! an increasing chance of failure.
//! The store properties may still hold outside some of those assumptions, but with an increasing
//! chance of failure.
//!
//! # Implementation
//!
//! We define the following constants:
//! - `E < 65536` the number of times a page can be erased.
//! - `3 <= N < 64` the number of pages in the storage.
//! - `8 <= P <= 1024` the number of words in a page.
//! - `Q = P - 2` the number of words in a virtual page.
//! - `K = 4096` the maximum number of keys.
//! - `M = min(Q - 1, 256)` the maximum length in words of a value.
//! - `V = (N - 1) * (Q - 1) - M` the virtual capacity.
//! - `C = V - N` the user capacity.
//! - [E](format::Format::max_page_erases) ≤ [65535](format::MAX_ERASE_CYCLE) the number of times
//! a page can be erased.
//! - 3 ≤ [N](format::Format::num_pages) < 64 the number of pages in the storage.
//! - 8 ≤ P ≤ 1024 the number of words in a page.
//! - [Q](format::Format::virt_page_size) = P - 2 the number of words in a virtual page.
//! - [M](format::Format::max_prefix_len) = min(Q - 1, 256) the maximum length in words of a
//! value.
//! - [V](format::Format::virt_size) = (N - 1) × (Q - 1) - M the virtual capacity.
//! - [C](format::Format::total_capacity) = V - N the user capacity.
//!
//! We build a virtual storage from the physical storage using the first 2 words of
//! each page:
//! We build a virtual storage from the physical storage using the first 2 words of each page:
//! - The first word contains the number of times the page has been erased.
//! - The second word contains the starting word to which this page is being moved
//! during compaction.
//! - The second word contains the starting word to which this page is being moved during
//! compaction.
//!
//! The virtual storage has a length of `(E + 1) * N * Q` words and represents the
//! lifetime of the store. (We reserve the last `Q + M` words to support adding
//! emergency lifetime.) This virtual storage has a linear address space.
//! The virtual storage has a length of (E + 1) × N × Q words and represents the lifetime of the
//! store. (We reserve the last Q + M words to support adding emergency lifetime.) This virtual
//! storage has a linear address space.
//!
//! We define a set of overlapping windows of `N * Q` words at each `Q`-aligned
//! boundary. We call `i` the window spanning from `i * Q` to `(i + N) * Q`. Only
//! those windows actually exist in the underlying storage. We use compaction to
//! shift the current window from `i` to `i + 1`, preserving the content of the
//! store.
//! We define a set of overlapping windows of N × Q words at each Q-aligned boundary. We call i the
//! window spanning from i × Q to (i + N) × Q. Only those windows actually exist in the underlying
//! storage. We use compaction to shift the current window from i to i + 1, preserving the content
//! of the store.
//!
//! For a given state of the virtual storage, we define `h_i` as the position of the
//! first entry of the window `i`. We call it the head of the window `i`. Because
//! entries are at most `M + 1` words, they can overlap on the next page only by `M`
//! words. So we have `i * Q <= h_i <= i * Q + M` . Since there are no entries
//! before the first page, we have `h_0 = 0`.
//! For a given state of the virtual storage, we define h\_i as the position of the first entry of
//! the window i. We call it the head of the window i. Because entries are at most M + 1 words, they
//! can overlap on the next page only by M words. So we have i × Q ≤ h_i ≤ i × Q + M . Since there
//! are no entries before the first page, we have h\_0 = 0.
//!
//! We define `t_i` as one past the last entry of the window `i`. If there are no
//! entries in that window, we have `t_i = h_i`. We call `t_i` the tail of the
//! window `i`. We define the compaction invariant as `t_i - h_i <= V`.
//! We define t\_i as one past the last entry of the window i. If there are no entries in that
//! window, we have t\_i = h\_i. We call t\_i the tail of the window i. We define the compaction
//! invariant as t\_i - h\_i V.
//!
//! We define `|x|` as the capacity used before position `x`. We have `|x| <= x`. We
//! define the capacity invariant as `|t_i| - |h_i| <= C`.
//! We define |x| as the capacity used before position x. We have |x| x. We define the capacity
//! invariant as |t\_i| - |h\_i| C.
//!
//! Using this virtual storage, entries are appended to the tail as long as there is
//! both virtual capacity to preserve the compaction invariant and capacity to
//! preserve the capacity invariant. When virtual capacity runs out, the first page
//! of the window is compacted and the window is shifted.
//! Using this virtual storage, entries are appended to the tail as long as there is both virtual
//! capacity to preserve the compaction invariant and capacity to preserve the capacity invariant.
//! When virtual capacity runs out, the first page of the window is compacted and the window is
//! shifted.
//!
//! Entries are identified by a prefix of bits. The prefix has to contain at least
//! one bit set to zero to differentiate from the tail. Entries can be one of:
//! - Padding: A word whose first bit is set to zero. The rest is arbitrary. This
//! entry is used to mark words partially written after an interrupted operation
//! as padding such that they are ignored by future operations.
//! - Header: A word whose second bit is set to zero. It contains the following fields:
//! - A bit indicating whether the entry is deleted.
//! - A bit indicating whether the value is word-aligned and has all bits set
//! to 1 in its last word. The last word of an entry is used to detect that
//! an entry has been fully written. As such it must contain at least one
//! bit equal to zero.
//! - The key of the entry.
//! - The length in bytes of the value. The value follows the header. The
//! entry is word-aligned if the value is not.
//! - The checksum of the first and last word of the entry.
//! - Erase: A word used during compaction. It contains the page to be erased and
//! a checksum.
//! - Clear: A word used during the `Clear` operation. It contains the threshold
//! and a checksum.
//! - Marker: A word used during the `Transaction` operation. It contains the
//! number of updates following the marker and a checksum.
//! - Remove: A word used during the `Transaction` operation. It contains the key
//! of the entry to be removed and a checksum.
//! Entries are identified by a prefix of bits. The prefix has to contain at least one bit set to
//! zero to differentiate from the tail. Entries can be one of:
//! - [Padding](format::ID_PADDING): A word whose first bit is set to zero. The rest is arbitrary.
//! This entry is used to mark words partially written after an interrupted operation as padding
//! such that they are ignored by future operations.
//! - [Header](format::ID_HEADER): A word whose second bit is set to zero. It contains the
//! following fields:
//! - A [bit](format::HEADER_DELETED) indicating whether the entry is deleted.
//! - A [bit](format::HEADER_FLIPPED) indicating whether the value is word-aligned and has all
//! bits set to 1 in its last word. The last word of an entry is used to detect that an
//! entry has been fully written. As such it must contain at least one bit equal to zero.
//! - The [key](format::HEADER_KEY) of the entry.
//! - The [length](format::HEADER_LENGTH) in bytes of the value. The value follows the header.
//! The entry is word-aligned if the value is not.
//! - The [checksum](format::HEADER_CHECKSUM) of the first and last word of the entry.
//! - [Erase](format::ID_ERASE): A word used during compaction. It contains the
//! [page](format::ERASE_PAGE) to be erased and a [checksum](format::WORD_CHECKSUM).
//! - [Clear](format::ID_CLEAR): A word used during the clear operation. It contains the
//! [threshold](format::CLEAR_MIN_KEY) and a [checksum](format::WORD_CHECKSUM).
//! - [Marker](format::ID_MARKER): A word used during a transaction. It contains the [number of
//! updates](format::MARKER_COUNT) following the marker and a [checksum](format::WORD_CHECKSUM).
//! - [Remove](format::ID_REMOVE): A word used inside a transaction. It contains the
//! [key](format::REMOVE_KEY) of the entry to be removed and a
//! [checksum](format::WORD_CHECKSUM).
//!
//! Checksums are the number of bits equal to 0.
//!
@@ -204,107 +204,105 @@
//!
//! ## Compaction
//!
//! It should always be possible to fully compact the store, after what the
//! remaining capacity should be available in the current window (restoring the
//! compaction invariant). We consider all notations on the virtual storage after
//! the full compaction. We will use the `|x|` notation although we update the state
//! of the virtual storage. This is fine because compaction doesn't change the
//! status of an existing word.
//! It should always be possible to fully compact the store, after what the remaining capacity
//! should be available in the current window (restoring the compaction invariant). We consider all
//! notations on the virtual storage after the full compaction. We will use the |x| notation
//! although we update the state of the virtual storage. This is fine because compaction doesn't
//! change the status of an existing word.
//!
//! We want to show that the next `N - 1` compactions won't move the tail past the
//! last page of their window, with `I` the initial window:
//! We want to show that the next N - 1 compactions won't move the tail past the last page of their
//! window, with I the initial window:
//!
//! ```text
//! forall 1 <= i <= N - 1, t_{I + i} <= (I + i + N - 1) * Q
//! ```
//! | | | | |
//! | ----------------:| ----------:|:-:|:------------------- |
//! | ∀(1 ≤ i ≤ N - 1) | t\_{I + i} | ≤ | (I + i + N - 1) × Q |
//!
//! We assume `i` between `1` and `N - 1`.
//! We assume i between 1 and N - 1.
//!
//! One step of compaction advances the tail by how many words were used in the
//! first page of the window with the last entry possibly overlapping on the next
//! page.
//! One step of compaction advances the tail by how many words were used in the first page of the
//! window with the last entry possibly overlapping on the next page.
//!
//! ```text
//! forall j, t_{j + 1} = t_j + |h_{j + 1}| - |h_j| + 1
//! ```
//! | | | | |
//! | --:| ----------:|:-:|:------------------------------------ |
//! | ∀j | t\_{j + 1} | = | t\_j + \|h\_{j + 1}\| - \|h\_j\| + 1 |
//!
//! By induction, we have:
//!
//! ```text
//! t_{I + i} <= t_I + |h_{I + i}| - |h_I| + i
//! ```
//! | | | |
//! | ----------:|:-:|:------------------------------------ |
//! | t\_{I + i} | ≤ | t\_I + \|h\_{I + i}\| - \|h\_I\| + i |
//!
//! We have the following properties:
//!
//! ```text
//! t_I <= h_I + V
//! |h_{I + i}| - |h_I| <= h_{I + i} - h_I
//! h_{I + i} <= (I + i) * Q + M
//! ```
//! | | | |
//! | -------------------------:|:-:|:----------------- |
//! | t\_I | | h\_I + V |
//! | \|h\_{I + i}\| - \|h\_I\| | ≤ | h\_{I + i} - h\_I |
//! | h\_{I + i} | ≤ | (I + i) × Q + M |
//!
//! Replacing into our previous equality, we can conclude:
//!
//! ```text
//! t_{I + i} = t_I + |h_{I + i}| - |h_I| + i
//! <= h_I + V + (I + i) * Q + M - h_I + i
//! = (N - 1) * (Q - 1) - M + (I + i) * Q + M + i
//! = (N - 1) * (Q - 1) + (I + i) * Q + i
//! = (I + i + N - 1) * Q + i - (N - 1)
//! <= (I + i + N - 1) * Q
//! ```
//! | | | |
//! | ----------:|:-:| ------------------------------------------- |
//! | t\_{I + i} | = | t_I + \|h_{I + i}\| - \|h_I\| + i |
//! | | ≤ | h\_I + V + (I + i) * Q + M - h\_I + i |
//! | | = | (N - 1) × (Q - 1) - M + (I + i) × Q + M + i |
//! | | = | (N - 1) × (Q - 1) + (I + i) × Q + i |
//! | | = | (I + i + N - 1) × Q + i - (N - 1) |
//! | | ≤ | (I + i + N - 1) × Q |
//!
//! We also want to show that after `N - 1` compactions, the remaining capacity is
//! available without compaction.
//! We also want to show that after N - 1 compactions, the remaining capacity is available without
//! compaction.
//!
//! ```text
//! V - (t_{I + N - 1} - h_{I + N - 1}) >= // The available words in the window.
//! C - (|t_{I + N - 1}| - |h_{I + N - 1}|) // The remaining capacity.
//! + 1 // Reserved for Clear.
//! ```
//! | | | |
//! | -:| --------------------------------------------- | --------------------------------- |
//! | | V - (t\_{I + N - 1} - h\_{I + N - 1}) | The available words in the window |
//! | ≥ | C - (\|t\_{I + N - 1}\| - \|h\_{I + N - 1}\|) | The remaining capacity |
//! | + | 1 | Reserved for clear |
//!
//! We can replace the definition of `C` and simplify:
//! We can replace the definition of C and simplify:
//!
//! ```text
//! V - (t_{I + N - 1} - h_{I + N - 1}) >= V - N - (|t_{I + N - 1}| - |h_{I + N - 1}|) + 1
//! iff t_{I + N - 1} - h_{I + N - 1} <= |t_{I + N - 1}| - |h_{I + N - 1}| + N - 1
//! ```
//! | | | | |
//! | ---:| -------------------------------------:|:-:|:----------------------------------------------------- |
//! | | V - (t\_{I + N - 1} - h\_{I + N - 1}) | ≥ | V - N - (\|t\_{I + N - 1}\| - \|h\_{I + N - 1}\|) + 1 |
//! | iff | t\_{I + N - 1} - h\_{I + N - 1} | ≤ | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| + N - 1 |
//!
//! We have the following properties:
//!
//! ```text
//! t_{I + N - 1} = t_I + |h_{I + N - 1}| - |h_I| + N - 1
//! |t_{I + N - 1}| - |h_{I + N - 1}| = |t_I| - |h_I| // Compaction preserves capacity.
//! |h_{I + N - 1}| - |t_I| <= h_{I + N - 1} - t_I
//! ```
//!
//! | | | | |
//! | ---------------------------------------:|:-:|:-------------------------------------------- |:------ |
//! | t\_{I + N - 1} | = | t\_I + \|h\_{I + N - 1}\| - \|h\_I\| + N - 1 | |
//! | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| | = | \|t\_I\| - \|h\_I\| | Compaction preserves capacity |
//! | \|h\_{I + N - 1}\| - \|t\_I\| | ≤ | h\_{I + N - 1} - t\_I | |
//!
//! From which we conclude:
//!
//! ```text
//! t_{I + N - 1} - h_{I + N - 1} <= |t_{I + N - 1}| - |h_{I + N - 1}| + N - 1
//! iff t_I + |h_{I + N - 1}| - |h_I| + N - 1 - h_{I + N - 1} <= |t_I| - |h_I| + N - 1
//! iff t_I + |h_{I + N - 1}| - h_{I + N - 1} <= |t_I|
//! iff |h_{I + N - 1}| - |t_I| <= h_{I + N - 1} - t_I
//! ```
//! | | | | |
//! | ---:| -------------------------------:|:-:|:----------------------------------------------- |
//! | | t\_{I + N - 1} - h\_{I + N - 1} | ≤ | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| + N - 1 |
//! | iff | t\_I + \|h\_{I + N - 1}\| - \|h\_I\| + N - 1 - h\_{I + N - 1} | ≤ | \|t\_I\| - \|h\_I\| + N - 1 |
//! | iff | t\_I + \|h\_{I + N - 1}\| - h\_{I + N - 1} | ≤ | \|t\_I\| |
//! | iff | \|h\_{I + N - 1}\| - \|t\_I\| | ≤ | h\_{I + N - 1} - t\_I |
//!
//!
//! ## Checksum
//!
//! The main property we want is that all partially written/erased words are either
//! the initial word, the final word, or invalid.
//! The main property we want is that all partially written/erased words are either the initial
//! word, the final word, or invalid.
//!
//! We say that a bit sequence `TARGET` is reachable from a bit sequence `SOURCE` if
//! both have the same length and `SOURCE & TARGET == TARGET` where `&` is the
//! bitwise AND operation on bit sequences of that length. In other words, when
//! `SOURCE` has a bit equal to 0 then `TARGET` also has that bit equal to 0.
//! We say that a bit sequence `TARGET` is reachable from a bit sequence `SOURCE` if both have the
//! same length and `SOURCE & TARGET == TARGET` where `&` is the bitwise AND operation on bit
//! sequences of that length. In other words, when `SOURCE` has a bit equal to 0 then `TARGET` also
//! has that bit equal to 0.
//!
//! The only written entries start with `101` or `110` and are written from an
//! erased word. Marking an entry as padding or deleted is a single bit operation,
//! so the property trivially holds. For those cases, the proof relies on the fact
//! that there is exactly one bit equal to 0 in the 3 first bits. Either the 3 first
//! bits are still `111` in which case we expect the remaining bits to be equal
//! to 1. Otherwise we can use the checksum of the given type of entry because those
//! 2 types of entries are not reachable from each other. Here is a visualization of
//! the partitioning based on the first 3 bits:
//! The only written entries start with `101` or `110` and are written from an erased word. Marking
//! an entry as padding or deleted is a single bit operation, so the property trivially holds. For
//! those cases, the proof relies on the fact that there is exactly one bit equal to 0 in the 3
//! first bits. Either the 3 first bits are still `111` in which case we expect the remaining bits
//! to be equal to 1. Otherwise we can use the checksum of the given type of entry because those 2
//! types of entries are not reachable from each other. Here is a visualization of the partitioning
//! based on the first 3 bits:
//!
//! | First 3 bits | Description | How to check |
//! | ------------:| ------------------ | ---------------------------- |
@@ -314,49 +312,48 @@
//! | `100` | Deleted user entry | No check, atomically written |
//! | `0??` | Padding entry | No check, atomically written |
//!
//! To show that valid entries of a given type are not reachable from each other, we
//! show 3 lemmas:
//! To show that valid entries of a given type are not reachable from each other, we show 3 lemmas:
//!
//! 1. A bit sequence is not reachable from another if its number of bits equal to
//! 0 is smaller.
//! 1. A bit sequence is not reachable from another if its number of bits equal to 0 is smaller.
//! 2. A bit sequence is not reachable from another if they have the same number of bits equals to
//! 0 and are different.
//! 3. A bit sequence is not reachable from another if it is bigger when they are interpreted as
//! numbers in binary representation.
//!
//! 2. A bit sequence is not reachable from another if they have the same number of
//! bits equals to 0 and are different.
//!
//! 3. A bit sequence is not reachable from another if it is bigger when they are
//! interpreted as numbers in binary representation.
//!
//! From those lemmas we consider the 2 cases. If both entries have the same number
//! of bits equal to 0, they are either equal or not reachable from each other
//! because of the second lemma. If they don't have the same number of bits equal to
//! 0, then the one with less bits equal to 0 is not reachable from the other
//! because of the first lemma and the one with more bits equal to 0 is not
//! reachable from the other because of the third lemma and the definition of the
//! checksum.
//! From those lemmas we consider the 2 cases. If both entries have the same number of bits equal to
//! 0, they are either equal or not reachable from each other because of the second lemma. If they
//! don't have the same number of bits equal to 0, then the one with less bits equal to 0 is not
//! reachable from the other because of the first lemma and the one with more bits equal to 0 is not
//! reachable from the other because of the third lemma and the definition of the checksum.
//!
//! # Fuzzing
//!
//! For any sequence of operations and interruptions starting from an erased
//! storage, the store is checked against its model and some internal invariant at
//! each step.
//! For any sequence of operations and interruptions starting from an erased storage, the store is
//! checked against its model and some internal invariant at each step.
//!
//! For any sequence of operations and interruptions starting from an arbitrary
//! storage, the store is checked not to crash.
//! For any sequence of operations and interruptions starting from an arbitrary storage, the store
//! is checked not to crash.
#![cfg_attr(not(feature = "std"), no_std)]
#![feature(try_trait)]
#[macro_use]
extern crate alloc;
#[cfg(feature = "std")]
mod buffer;
#[cfg(feature = "std")]
mod driver;
mod format;
pub mod fragment;
#[cfg(feature = "std")]
mod model;
mod storage;
mod store;
#[cfg(test)]
mod test;
#[cfg(feature = "std")]
pub use self::buffer::{BufferCorruptFunction, BufferOptions, BufferStorage};
#[cfg(feature = "std")]
pub use self::driver::{

View File

@@ -12,13 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Store specification.
use crate::format::Format;
use crate::{usize_to_nat, StoreError, StoreRatio, StoreResult, StoreUpdate};
use std::collections::HashMap;
/// Models the mutable operations of a store.
///
/// The model doesn't model the storage and read-only operations. This is done by the driver.
/// The model doesn't model the storage and read-only operations. This is done by the
/// [driver](crate::StoreDriver).
#[derive(Clone, Debug)]
pub struct StoreModel {
/// Represents the content of the store.
@@ -34,7 +37,7 @@ pub enum StoreOperation {
/// Applies a transaction.
Transaction {
/// The list of updates to be applied.
updates: Vec<StoreUpdate>,
updates: Vec<StoreUpdate<Vec<u8>>>,
},
/// Deletes all keys above a threshold.
@@ -89,7 +92,7 @@ impl StoreModel {
}
/// Applies a transaction.
fn transaction(&mut self, updates: Vec<StoreUpdate>) -> StoreResult<()> {
fn transaction(&mut self, updates: Vec<StoreUpdate<Vec<u8>>>) -> StoreResult<()> {
// Fail if the transaction is invalid.
if self.format.transaction_valid(&updates).is_none() {
return Err(StoreError::InvalidArgument);

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Flash storage abstraction.
/// Represents a byte position in a storage.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct StorageIndex {
@@ -65,12 +67,14 @@ pub trait Storage {
/// The following pre-conditions must hold:
/// - The `index` must designate `value.len()` bytes in the storage.
/// - Both `index` and `value.len()` must be word-aligned.
/// - The written words should not have been written too many times since last page erasure.
/// - The written words should not have been written [too many](Self::max_word_writes) times
/// since the last page erasure.
fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()>;
/// Erases a page of the storage.
///
/// The `page` must be in the storage.
/// The `page` must be in the storage, i.e. less than [`Storage::num_pages`]. And the page
/// should not have been erased [too many](Self::max_page_erases) times.
fn erase_page(&mut self, page: usize) -> StorageResult<()>;
}

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Store implementation.
use crate::format::{
is_erased, CompactInfo, Format, Header, InitInfo, InternalEntry, Padding, ParsedWord, Position,
Word, WordState,
@@ -23,8 +25,12 @@ use crate::{usize_to_nat, Nat, Storage, StorageError, StorageIndex};
pub use crate::{
BufferStorage, StoreDriver, StoreDriverOff, StoreDriverOn, StoreInterruption, StoreInvariant,
};
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::borrow::Borrow;
use core::cmp::{max, min, Ordering};
use core::convert::TryFrom;
use core::option::NoneError;
#[cfg(feature = "std")]
use std::collections::HashSet;
@@ -51,17 +57,14 @@ pub enum StoreError {
///
/// The consequences depend on the storage failure. In particular, the operation may or may not
/// have succeeded, and the storage may have become invalid. Before doing any other operation,
/// the store should be [recovered]. The operation may then be retried if idempotent.
///
/// [recovered]: struct.Store.html#method.recover
/// the store should be [recovered](Store::recover). The operation may then be retried if
/// idempotent.
StorageError,
/// Storage is invalid.
///
/// The storage should be erased and the store [recovered]. The store would be empty and have
/// lost track of lifetime.
///
/// [recovered]: struct.Store.html#method.recover
/// The storage should be erased and the store [recovered](Store::recover). The store would be
/// empty and have lost track of lifetime.
InvalidStorage,
}
@@ -75,20 +78,26 @@ impl From<StorageError> for StoreError {
}
}
impl From<NoneError> for StoreError {
fn from(error: NoneError) -> StoreError {
match error {
NoneError => StoreError::InvalidStorage,
}
}
}
/// Result of store operations.
pub type StoreResult<T> = Result<T, StoreError>;
/// Progression ratio for store metrics.
///
/// This is used for the [capacity] and [lifetime] metrics. Those metrics are measured in words.
/// This is used for the [`Store::capacity`] and [`Store::lifetime`] metrics. Those metrics are
/// measured in words.
///
/// # Invariant
///
/// - The used value does not exceed the total: `used <= total`.
///
/// [capacity]: struct.Store.html#method.capacity
/// [lifetime]: struct.Store.html#method.lifetime
#[derive(Copy, Clone, PartialEq, Eq)]
/// - The used value does not exceed the total: `used` ≤ `total`.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct StoreRatio {
/// How much of the metric is used.
pub(crate) used: Nat,
@@ -136,11 +145,20 @@ impl StoreHandle {
self.key as usize
}
/// Returns the value length of the entry.
///
/// # Errors
///
/// Returns [`StoreError::InvalidArgument`] if the entry has been deleted or compacted.
pub fn get_length<S: Storage>(&self, store: &Store<S>) -> StoreResult<usize> {
store.get_length(self)
}
/// Returns the value of the entry.
///
/// # Errors
///
/// Returns `InvalidArgument` if the entry has been deleted or compacted.
/// Returns [`StoreError::InvalidArgument`] if the entry has been deleted or compacted.
pub fn get_value<S: Storage>(&self, store: &Store<S>) -> StoreResult<Vec<u8>> {
store.get_value(self)
}
@@ -148,15 +166,15 @@ impl StoreHandle {
/// Represents an update to the store as part of a transaction.
#[derive(Clone, Debug)]
pub enum StoreUpdate {
pub enum StoreUpdate<ByteSlice: Borrow<[u8]>> {
/// Inserts or replaces an entry in the store.
Insert { key: usize, value: Vec<u8> },
Insert { key: usize, value: ByteSlice },
/// Removes an entry from the store.
Remove { key: usize },
}
impl StoreUpdate {
impl<ByteSlice: Borrow<[u8]>> StoreUpdate<ByteSlice> {
/// Returns the key affected by the update.
pub fn key(&self) -> usize {
match *self {
@@ -168,12 +186,14 @@ impl StoreUpdate {
/// Returns the value written by the update.
pub fn value(&self) -> Option<&[u8]> {
match self {
StoreUpdate::Insert { value, .. } => Some(value),
StoreUpdate::Insert { value, .. } => Some(value.borrow()),
StoreUpdate::Remove { .. } => None,
}
}
}
pub type StoreIter<'a> = Box<dyn Iterator<Item = StoreResult<StoreHandle>> + 'a>;
/// Implements a store with a map interface over a storage.
#[derive(Clone)]
pub struct Store<S: Storage> {
@@ -182,6 +202,14 @@ pub struct Store<S: Storage> {
/// The storage configuration.
format: Format,
/// The position of the first word in the store.
head: Option<Position>,
/// The list of the position of the user entries.
///
/// The position is encoded as the word offset from the [head](Store::head).
entries: Option<Vec<u16>>,
}
impl<S: Storage> Store<S> {
@@ -193,13 +221,19 @@ impl<S: Storage> Store<S> {
///
/// # Errors
///
/// Returns `InvalidArgument` if the storage is not supported.
/// Returns [`StoreError::InvalidArgument`] if the storage is not
/// [supported](Format::is_storage_supported).
pub fn new(storage: S) -> Result<Store<S>, (StoreError, S)> {
let format = match Format::new(&storage) {
None => return Err((StoreError::InvalidArgument, storage)),
Some(x) => x,
};
let mut store = Store { storage, format };
let mut store = Store {
storage,
format,
head: None,
entries: None,
};
if let Err(error) = store.recover() {
return Err((error, store.storage));
}
@@ -207,31 +241,35 @@ impl<S: Storage> Store<S> {
}
/// Iterates over the entries.
pub fn iter<'a>(&'a self) -> StoreResult<StoreIter<'a, S>> {
StoreIter::new(self)
pub fn iter<'a>(&'a self) -> StoreResult<StoreIter<'a>> {
let head = self.head?;
Ok(Box::new(self.entries.as_ref()?.iter().map(
move |&offset| {
let pos = head + offset as Nat;
match self.parse_entry(&mut pos.clone())? {
ParsedEntry::User(Header {
key, length: len, ..
}) => Ok(StoreHandle { key, pos, len }),
_ => Err(StoreError::InvalidStorage),
}
},
)))
}
/// Returns the current capacity in words.
/// Returns the current and total capacity in words.
///
/// The capacity represents the size of what is stored.
pub fn capacity(&self) -> StoreResult<StoreRatio> {
let total = self.format.total_capacity();
let mut used = 0;
let mut pos = self.head()?;
let end = pos + self.format.virt_size();
while pos < end {
let entry_pos = pos;
match self.parse_entry(&mut pos)? {
ParsedEntry::Tail => break,
ParsedEntry::Padding => (),
ParsedEntry::User(_) => used += pos - entry_pos,
_ => return Err(StoreError::InvalidStorage),
}
for handle in self.iter()? {
let handle = handle?;
used += 1 + self.format.bytes_to_words(handle.len);
}
Ok(StoreRatio { used, total })
}
/// Returns the current lifetime in words.
/// Returns the current and total lifetime in words.
///
/// The lifetime represents the age of the storage. The limit is an over-approximation by at
/// most the maximum length of a value (the actual limit depends on the length of the prefix of
@@ -246,18 +284,22 @@ impl<S: Storage> Store<S> {
///
/// # Errors
///
/// Returns `InvalidArgument` in the following circumstances:
/// - There are too many updates.
/// Returns [`StoreError::InvalidArgument`] in the following circumstances:
/// - There are [too many](Format::max_updates) updates.
/// - The updates overlap, i.e. their keys are not disjoint.
/// - The updates are invalid, e.g. key out of bound or value too long.
pub fn transaction(&mut self, updates: &[StoreUpdate]) -> StoreResult<()> {
/// - The updates are invalid, e.g. key [out of bound](Format::max_key) or value [too
/// long](Format::max_value_len).
pub fn transaction<ByteSlice: Borrow<[u8]>>(
&mut self,
updates: &[StoreUpdate<ByteSlice>],
) -> StoreResult<()> {
let count = usize_to_nat(updates.len());
if count == 0 {
return Ok(());
}
if count == 1 {
match updates[0] {
StoreUpdate::Insert { key, ref value } => return self.insert(key, value),
StoreUpdate::Insert { key, ref value } => return self.insert(key, value.borrow()),
StoreUpdate::Remove { key } => return self.remove(key),
}
}
@@ -270,7 +312,9 @@ impl<S: Storage> Store<S> {
self.reserve(self.format.transaction_capacity(updates))?;
// Write the marker entry.
let marker = self.tail()?;
let entry = self.format.build_internal(InternalEntry::Marker { count });
let entry = self
.format
.build_internal(InternalEntry::Marker { count })?;
self.write_slice(marker, &entry)?;
self.init_page(marker, marker)?;
// Write the updates.
@@ -278,7 +322,7 @@ impl<S: Storage> Store<S> {
for update in updates {
let length = match *update {
StoreUpdate::Insert { key, ref value } => {
let entry = self.format.build_user(usize_to_nat(key), value);
let entry = self.format.build_user(usize_to_nat(key), value.borrow())?;
let word_size = self.format.word_size();
let footer = usize_to_nat(entry.len()) / word_size - 1;
self.write_slice(tail, &entry[..(footer * word_size) as usize])?;
@@ -287,7 +331,7 @@ impl<S: Storage> Store<S> {
}
StoreUpdate::Remove { key } => {
let key = usize_to_nat(key);
let remove = self.format.build_internal(InternalEntry::Remove { key });
let remove = self.format.build_internal(InternalEntry::Remove { key })?;
self.write_slice(tail, &remove)?;
0
}
@@ -307,7 +351,9 @@ impl<S: Storage> Store<S> {
if min_key > self.format.max_key() {
return Err(StoreError::InvalidArgument);
}
let clear = self.format.build_internal(InternalEntry::Clear { min_key });
let clear = self
.format
.build_internal(InternalEntry::Clear { min_key })?;
// We always have one word available. We can't use `reserve` because this is internal
// capacity, not user capacity.
while self.immediate_capacity()? < 1 {
@@ -373,7 +419,7 @@ impl<S: Storage> Store<S> {
if key > self.format.max_key() || value_len > self.format.max_value_len() {
return Err(StoreError::InvalidArgument);
}
let entry = self.format.build_user(key, value);
let entry = self.format.build_user(key, value)?;
let entry_len = usize_to_nat(entry.len());
self.reserve(entry_len / self.format.word_size())?;
let tail = self.tail()?;
@@ -381,6 +427,7 @@ impl<S: Storage> Store<S> {
let footer = entry_len / word_size - 1;
self.write_slice(tail, &entry[..(footer * word_size) as usize])?;
self.write_slice(tail + footer, &entry[(footer * word_size) as usize..])?;
self.push_entry(tail)?;
self.insert_init(tail, footer, key)
}
@@ -398,7 +445,8 @@ impl<S: Storage> Store<S> {
/// Removes an entry given a handle.
pub fn remove_handle(&mut self, handle: &StoreHandle) -> StoreResult<()> {
self.check_handle(handle)?;
self.delete_pos(handle.pos, self.format.bytes_to_words(handle.len))
self.delete_pos(handle.pos, self.format.bytes_to_words(handle.len))?;
self.remove_entry(handle.pos)
}
/// Returns the maximum length in bytes of a value.
@@ -406,6 +454,17 @@ impl<S: Storage> Store<S> {
self.format.max_value_len() as usize
}
/// Returns the length of the value of an entry given its handle.
fn get_length(&self, handle: &StoreHandle) -> StoreResult<usize> {
self.check_handle(handle)?;
let mut pos = handle.pos;
match self.parse_entry(&mut pos)? {
ParsedEntry::User(header) => Ok(header.length as usize),
ParsedEntry::Padding => Err(StoreError::InvalidArgument),
_ => Err(StoreError::InvalidStorage),
}
}
/// Returns the value of an entry given its handle.
fn get_value(&self, handle: &StoreHandle) -> StoreResult<Vec<u8>> {
self.check_handle(handle)?;
@@ -437,7 +496,7 @@ impl<S: Storage> Store<S> {
let init_info = self.format.build_init(InitInfo {
cycle: 0,
prefix: 0,
});
})?;
self.storage_write_slice(index, &init_info)
}
@@ -460,7 +519,9 @@ impl<S: Storage> Store<S> {
/// Recovers a possible compaction interrupted while copying the entries.
fn recover_compaction(&mut self) -> StoreResult<()> {
let head_page = self.head()?.page(&self.format);
let head = self.get_extremum_page_head(Ordering::Less)?;
self.head = Some(head);
let head_page = head.page(&self.format);
match self.parse_compact(head_page)? {
WordState::Erased => Ok(()),
WordState::Partial => self.compact(),
@@ -470,14 +531,15 @@ impl<S: Storage> Store<S> {
/// Recover a possible interrupted operation which is not a compaction.
fn recover_operation(&mut self) -> StoreResult<()> {
let mut pos = self.head()?;
self.entries = Some(Vec::new());
let mut pos = self.head?;
let mut prev_pos = pos;
let end = pos + self.format.virt_size();
while pos < end {
let entry_pos = pos;
match self.parse_entry(&mut pos)? {
ParsedEntry::Tail => break,
ParsedEntry::User(_) => (),
ParsedEntry::User(_) => self.push_entry(entry_pos)?,
ParsedEntry::Padding => {
self.wipe_span(entry_pos + 1, pos - entry_pos - 1)?;
}
@@ -610,7 +672,7 @@ impl<S: Storage> Store<S> {
///
/// In particular, the handle has not been compacted.
fn check_handle(&self, handle: &StoreHandle) -> StoreResult<()> {
if handle.pos < self.head()? {
if handle.pos < self.head? {
Err(StoreError::InvalidArgument)
} else {
Ok(())
@@ -640,20 +702,22 @@ impl<S: Storage> Store<S> {
/// Compacts one page.
fn compact(&mut self) -> StoreResult<()> {
let head = self.head()?;
let head = self.head?;
if head.cycle(&self.format) >= self.format.max_page_erases() {
return Err(StoreError::NoLifetime);
}
let tail = max(self.tail()?, head.next_page(&self.format));
let index = self.format.index_compact(head.page(&self.format));
let compact_info = self.format.build_compact(CompactInfo { tail: tail - head });
let compact_info = self
.format
.build_compact(CompactInfo { tail: tail - head })?;
self.storage_write_slice(index, &compact_info)?;
self.compact_copy()
}
/// Continues a compaction after its compact page info has been written.
fn compact_copy(&mut self) -> StoreResult<()> {
let mut head = self.head()?;
let mut head = self.head?;
let page = head.page(&self.format);
let end = head.next_page(&self.format);
let mut tail = match self.parse_compact(page)? {
@@ -667,8 +731,12 @@ impl<S: Storage> Store<S> {
let pos = head;
match self.parse_entry(&mut head)? {
ParsedEntry::Tail => break,
// This can happen if we copy to the next page. We actually reached the tail but we
// read what we just copied.
ParsedEntry::Partial if head > end => break,
ParsedEntry::User(_) => (),
_ => continue,
ParsedEntry::Padding => continue,
_ => return Err(StoreError::InvalidStorage),
};
let length = head - pos;
// We have to copy the slice for 2 reasons:
@@ -676,11 +744,13 @@ impl<S: Storage> Store<S> {
// 2. We can't pass a flash slice to the kernel. This should get fixed with
// https://github.com/tock/tock/issues/1274.
let entry = self.read_slice(pos, length * self.format.word_size());
self.remove_entry(pos)?;
self.write_slice(tail, &entry)?;
self.push_entry(tail)?;
self.init_page(tail, tail + (length - 1))?;
tail += length;
}
let erase = self.format.build_internal(InternalEntry::Erase { page });
let erase = self.format.build_internal(InternalEntry::Erase { page })?;
self.write_slice(tail, &erase)?;
self.init_page(tail, tail)?;
self.compact_erase(tail)
@@ -688,14 +758,31 @@ impl<S: Storage> Store<S> {
/// Continues a compaction after its erase entry has been written.
fn compact_erase(&mut self, erase: Position) -> StoreResult<()> {
let page = match self.parse_entry(&mut erase.clone())? {
// Read the page to erase from the erase entry.
let mut page = match self.parse_entry(&mut erase.clone())? {
ParsedEntry::Internal(InternalEntry::Erase { page }) => page,
_ => return Err(StoreError::InvalidStorage),
};
// Erase the page.
self.storage_erase_page(page)?;
let head = self.head()?;
// Update the head.
page = (page + 1) % self.format.num_pages();
let init = match self.parse_init(page)? {
WordState::Valid(x) => x,
_ => return Err(StoreError::InvalidStorage),
};
let head = self.format.page_head(init, page);
if let Some(entries) = &mut self.entries {
let head_offset = u16::try_from(head - self.head?).ok()?;
for entry in entries {
*entry = entry.checked_sub(head_offset)?;
}
}
self.head = Some(head);
// Wipe the overlapping entry from the erased page.
let pos = head.page_begin(&self.format);
self.wipe_span(pos, head - pos)?;
// Mark the erase entry as done.
self.set_padding(erase)?;
Ok(())
}
@@ -704,13 +791,13 @@ impl<S: Storage> Store<S> {
fn transaction_apply(&mut self, sorted_keys: &[Nat], marker: Position) -> StoreResult<()> {
self.delete_keys(&sorted_keys, marker)?;
self.set_padding(marker)?;
let end = self.head()? + self.format.virt_size();
let end = self.head? + self.format.virt_size();
let mut pos = marker + 1;
while pos < end {
let entry_pos = pos;
match self.parse_entry(&mut pos)? {
ParsedEntry::Tail => break,
ParsedEntry::User(_) => (),
ParsedEntry::User(_) => self.push_entry(entry_pos)?,
ParsedEntry::Internal(InternalEntry::Remove { .. }) => {
self.set_padding(entry_pos)?
}
@@ -727,37 +814,38 @@ impl<S: Storage> Store<S> {
ParsedEntry::Internal(InternalEntry::Clear { min_key }) => min_key,
_ => return Err(StoreError::InvalidStorage),
};
let mut pos = self.head()?;
let end = pos + self.format.virt_size();
while pos < end {
let entry_pos = pos;
match self.parse_entry(&mut pos)? {
ParsedEntry::Internal(InternalEntry::Clear { .. }) if entry_pos == clear => break,
ParsedEntry::User(header) if header.key >= min_key => {
self.delete_pos(entry_pos, pos - entry_pos - 1)?;
}
ParsedEntry::Padding | ParsedEntry::User(_) => (),
_ => return Err(StoreError::InvalidStorage),
}
}
self.delete_if(clear, |key| key >= min_key)?;
self.set_padding(clear)?;
Ok(())
}
/// Deletes a set of entries up to a certain position.
fn delete_keys(&mut self, sorted_keys: &[Nat], end: Position) -> StoreResult<()> {
let mut pos = self.head()?;
while pos < end {
let entry_pos = pos;
match self.parse_entry(&mut pos)? {
ParsedEntry::Tail => break,
ParsedEntry::User(header) if sorted_keys.binary_search(&header.key).is_ok() => {
self.delete_pos(entry_pos, pos - entry_pos - 1)?;
}
ParsedEntry::Padding | ParsedEntry::User(_) => (),
self.delete_if(end, |key| sorted_keys.binary_search(&key).is_ok())
}
/// Deletes entries matching a predicate up to a certain position.
fn delete_if(&mut self, end: Position, delete: impl Fn(Nat) -> bool) -> StoreResult<()> {
let head = self.head?;
let mut entries = self.entries.take()?;
let mut i = 0;
while i < entries.len() {
let pos = head + entries[i] as Nat;
if pos >= end {
break;
}
let header = match self.parse_entry(&mut pos.clone())? {
ParsedEntry::User(x) => x,
_ => return Err(StoreError::InvalidStorage),
};
if delete(header.key) {
self.delete_pos(pos, self.format.bytes_to_words(header.length))?;
entries.swap_remove(i);
} else {
i += 1;
}
}
self.entries = Some(entries);
Ok(())
}
@@ -792,7 +880,7 @@ impl<S: Storage> Store<S> {
let init_info = self.format.build_init(InitInfo {
cycle: new_first.cycle(&self.format),
prefix: new_first.word(&self.format),
});
})?;
self.storage_write_slice(index, &init_info)?;
Ok(())
}
@@ -800,7 +888,7 @@ impl<S: Storage> Store<S> {
/// Sets the padding bit of a user header.
fn set_padding(&mut self, pos: Position) -> StoreResult<()> {
let mut word = Word::from_slice(self.read_word(pos));
self.format.set_padding(&mut word);
self.format.set_padding(&mut word)?;
self.write_slice(pos, &word.as_slice())?;
Ok(())
}
@@ -836,19 +924,20 @@ impl<S: Storage> Store<S> {
}
}
// There is always at least one initialized page.
best.ok_or(StoreError::InvalidStorage)
Ok(best?)
}
/// Returns the number of words that can be written without compaction.
fn immediate_capacity(&self) -> StoreResult<Nat> {
let tail = self.tail()?;
let end = self.head()? + self.format.virt_size();
let end = self.head? + self.format.virt_size();
Ok(end.get().saturating_sub(tail.get()))
}
/// Returns the position of the first word in the store.
#[cfg(feature = "std")]
pub(crate) fn head(&self) -> StoreResult<Position> {
self.get_extremum_page_head(Ordering::Less)
Ok(self.head?)
}
/// Returns one past the position of the last word in the store.
@@ -863,6 +952,30 @@ impl<S: Storage> Store<S> {
Ok(pos)
}
fn push_entry(&mut self, pos: Position) -> StoreResult<()> {
let entries = match &mut self.entries {
None => return Ok(()),
Some(x) => x,
};
let head = self.head?;
let offset = u16::try_from(pos - head).ok()?;
debug_assert!(!entries.contains(&offset));
entries.push(offset);
Ok(())
}
fn remove_entry(&mut self, pos: Position) -> StoreResult<()> {
let entries = match &mut self.entries {
None => return Ok(()),
Some(x) => x,
};
let head = self.head?;
let offset = u16::try_from(pos - head).ok()?;
let i = entries.iter().position(|x| *x == offset)?;
entries.swap_remove(i);
Ok(())
}
/// Parses the entry at a given position.
///
/// The position is updated to point to the next entry.
@@ -1061,7 +1174,7 @@ impl Store<BufferStorage> {
/// If the value has been partially compacted, only return the non-compacted part. Returns an
/// empty value if it has been fully compacted.
pub fn inspect_value(&self, handle: &StoreHandle) -> Vec<u8> {
let head = self.head().unwrap();
let head = self.head.unwrap();
let length = self.format.bytes_to_words(handle.len);
if head <= handle.pos {
// The value has not been compacted.
@@ -1087,20 +1200,21 @@ impl Store<BufferStorage> {
store
.iter()
.unwrap()
.map(|x| x.unwrap())
.filter(|x| delete_key(x.key as usize))
.collect::<Vec<_>>()
.filter(|x| x.is_err() || delete_key(x.as_ref().unwrap().key as usize))
.collect::<Result<Vec<_>, _>>()
};
match *operation {
StoreOperation::Transaction { ref updates } => {
let keys: HashSet<usize> = updates.iter().map(|x| x.key()).collect();
let deleted = deleted(self, &|key| keys.contains(&key));
(deleted, self.transaction(updates))
}
StoreOperation::Clear { min_key } => {
let deleted = deleted(self, &|key| key >= min_key);
(deleted, self.clear(min_key))
match deleted(self, &|key| keys.contains(&key)) {
Ok(deleted) => (deleted, self.transaction(updates)),
Err(error) => (Vec::new(), Err(error)),
}
}
StoreOperation::Clear { min_key } => match deleted(self, &|key| key >= min_key) {
Ok(deleted) => (deleted, self.clear(min_key)),
Err(error) => (Vec::new(), Err(error)),
},
StoreOperation::Prepare { length } => (Vec::new(), self.prepare(length)),
}
}
@@ -1110,10 +1224,12 @@ impl Store<BufferStorage> {
let format = Format::new(storage).unwrap();
// Write the init info of the first page.
let mut index = format.index_init(0);
let init_info = format.build_init(InitInfo {
cycle: usize_to_nat(cycle),
prefix: 0,
});
let init_info = format
.build_init(InitInfo {
cycle: usize_to_nat(cycle),
prefix: 0,
})
.unwrap();
storage.write_slice(index, &init_info).unwrap();
// Pad the first word of the page. This makes the store looks used, otherwise we may confuse
// it with a partially initialized store.
@@ -1165,61 +1281,6 @@ enum ParsedEntry {
Tail,
}
/// Iterates over the entries of a store.
pub struct StoreIter<'a, S: Storage> {
/// The store being iterated.
store: &'a Store<S>,
/// The position of the next entry.
pos: Position,
/// Iteration stops when reaching this position.
end: Position,
}
impl<'a, S: Storage> StoreIter<'a, S> {
/// Creates an iterator over the entries of a store.
fn new(store: &'a Store<S>) -> StoreResult<StoreIter<'a, S>> {
let pos = store.head()?;
let end = pos + store.format.virt_size();
Ok(StoreIter { store, pos, end })
}
}
impl<'a, S: Storage> StoreIter<'a, S> {
/// Returns the next entry and advances the iterator.
fn transposed_next(&mut self) -> StoreResult<Option<StoreHandle>> {
if self.pos >= self.end {
return Ok(None);
}
while self.pos < self.end {
let entry_pos = self.pos;
match self.store.parse_entry(&mut self.pos)? {
ParsedEntry::Tail => break,
ParsedEntry::Padding => (),
ParsedEntry::User(header) => {
return Ok(Some(StoreHandle {
key: header.key,
pos: entry_pos,
len: header.length,
}))
}
_ => return Err(StoreError::InvalidStorage),
}
}
self.pos = self.end;
Ok(None)
}
}
impl<'a, S: Storage> Iterator for StoreIter<'a, S> {
type Item = StoreResult<StoreHandle>;
fn next(&mut self) -> Option<StoreResult<StoreHandle>> {
self.transposed_next().transpose()
}
}
/// Returns whether 2 slices are different.
///
/// Returns an error if `target` has a bit set to one for which `source` is set to zero.
@@ -1239,71 +1300,15 @@ fn is_write_needed(source: &[u8], target: &[u8]) -> StoreResult<bool> {
#[cfg(test)]
mod tests {
use super::*;
use crate::BufferOptions;
#[derive(Clone)]
struct Config {
word_size: usize,
page_size: usize,
num_pages: usize,
max_word_writes: usize,
max_page_erases: usize,
}
impl Config {
fn new_driver(&self) -> StoreDriverOff {
let options = BufferOptions {
word_size: self.word_size,
page_size: self.page_size,
max_word_writes: self.max_word_writes,
max_page_erases: self.max_page_erases,
strict_write: true,
};
StoreDriverOff::new(options, self.num_pages)
}
}
const MINIMAL: Config = Config {
word_size: 4,
page_size: 64,
num_pages: 5,
max_word_writes: 2,
max_page_erases: 9,
};
const NORDIC: Config = Config {
word_size: 4,
page_size: 0x1000,
num_pages: 20,
max_word_writes: 2,
max_page_erases: 10000,
};
const TITAN: Config = Config {
word_size: 4,
page_size: 0x800,
num_pages: 10,
max_word_writes: 2,
max_page_erases: 10000,
};
use crate::test::MINIMAL;
#[test]
fn nordic_capacity() {
let driver = NORDIC.new_driver().power_on().unwrap();
assert_eq!(driver.model().capacity().total, 19123);
}
#[test]
fn titan_capacity() {
let driver = TITAN.new_driver().power_on().unwrap();
assert_eq!(driver.model().capacity().total, 4315);
}
#[test]
fn minimal_virt_page_size() {
// Make sure a virtual page has 14 words. We use this property in the other tests below to
// know whether entries are spanning, starting, and ending pages.
assert_eq!(MINIMAL.new_driver().model().format().virt_page_size(), 14);
fn is_write_needed_ok() {
assert_eq!(is_write_needed(&[], &[]), Ok(false));
assert_eq!(is_write_needed(&[0], &[0]), Ok(false));
assert_eq!(is_write_needed(&[0], &[1]), Err(StoreError::InvalidStorage));
assert_eq!(is_write_needed(&[1], &[0]), Ok(true));
assert_eq!(is_write_needed(&[1], &[1]), Ok(false));
}
#[test]
@@ -1438,4 +1443,22 @@ mod tests {
driver = driver.power_off().power_on().unwrap();
driver.check().unwrap();
}
#[test]
fn entries_ok() {
let mut driver = MINIMAL.new_driver().power_on().unwrap();
// The store is initially empty.
assert!(driver.store().entries.as_ref().unwrap().is_empty());
// Inserted elements are added.
const LEN: usize = 6;
driver.insert(0, &[0x38; (LEN - 1) * 4]).unwrap();
driver.insert(1, &[0x5c; 4]).unwrap();
assert_eq!(driver.store().entries, Some(vec![0, LEN as u16]));
// Deleted elements are removed.
driver.remove(0).unwrap();
assert_eq!(driver.store().entries, Some(vec![LEN as u16]));
}
}

View File

@@ -0,0 +1,84 @@
// Copyright 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 crate::{BufferOptions, BufferStorage, Store, StoreDriverOff};
#[derive(Clone)]
pub struct Config {
word_size: usize,
page_size: usize,
num_pages: usize,
max_word_writes: usize,
max_page_erases: usize,
}
impl Config {
pub fn new_driver(&self) -> StoreDriverOff {
let options = BufferOptions {
word_size: self.word_size,
page_size: self.page_size,
max_word_writes: self.max_word_writes,
max_page_erases: self.max_page_erases,
strict_mode: true,
};
StoreDriverOff::new(options, self.num_pages)
}
pub fn new_store(&self) -> Store<BufferStorage> {
self.new_driver().power_on().unwrap().extract_store()
}
}
pub const MINIMAL: Config = Config {
word_size: 4,
page_size: 64,
num_pages: 5,
max_word_writes: 2,
max_page_erases: 9,
};
const NORDIC: Config = Config {
word_size: 4,
page_size: 0x1000,
num_pages: 20,
max_word_writes: 2,
max_page_erases: 10000,
};
const TITAN: Config = Config {
word_size: 4,
page_size: 0x800,
num_pages: 10,
max_word_writes: 2,
max_page_erases: 10000,
};
#[test]
fn nordic_capacity() {
let driver = NORDIC.new_driver().power_on().unwrap();
assert_eq!(driver.model().capacity().total, 19123);
}
#[test]
fn titan_capacity() {
let driver = TITAN.new_driver().power_on().unwrap();
assert_eq!(driver.model().capacity().total, 4315);
}
#[test]
fn minimal_virt_page_size() {
// Make sure a virtual page has 14 words. We use this property in the other tests below to
// know whether entries are spanning, starting, and ending pages.
assert_eq!(MINIMAL.new_driver().model().format().virt_page_size(), 14);
}

46
metadata/metadata.json Normal file
View File

@@ -0,0 +1,46 @@
{
"assertionScheme": "FIDOV2",
"keyProtection": 1,
"attestationRootCertificates": [],
"aaguid": "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05",
"publicKeyAlgAndEncoding": 260,
"protocolFamily": "fido2",
"upv": [
{
"major": 1,
"minor": 0
}
],
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAIQ0lEQVR4Ae1aCVSUVRT+kGVYBBQFBYzYFJFNLdPQVksz85QnszRNbaPNzDI0OaIH27VUUnOpzAqXMJNIszKTUEQWRXBnExRiUYEUBATs3lfzJw3LDP/MMOfMPI/M++97///uve+++9797jO7TgVGXLoYsexCdJMCTBZg5BowLQEjNwCYLMBkAUauAdMSMHIDgEVnKqC8/AKOZh2Do6MDAgMGwMbaWu/s6FUBTU1NyMnNQ8bRTPqfheI/SySBzc3N4devLwaGBGFgcBBcXJylNl1WzHQdDVbX1CDr2HEcJYEz6be6ukYteVxdewtFsEL6+vqgSxfduCudKaCgsBCbt27Dmexc8MzLKba2tggOCkDYszNgZmYm51Mq7+pGrTRMcXEJTp3Oli08c1xDVpR8KBW6gC50pgAVVRsoQWcKcHd3w4jht6N7924GKvo/bGl1F+C1fu78eWH+TdebcOeIUEyfOhkHk1OwJXY7OcBqg1OG1hRwICkZ38fF48LFS82EdHLqjkmPT8DihRF4b8nH4L3fkIrsJcCO6cuvYrD+i40qwrOgly5VYNWn65GUfAjhb7wGKysrQ5Jffji8a/ev2PfH/naF2rY9jma/HA+PG9tuX312kLUErly5grj4H9XmN3b7Dix4Kxz33n2H2u+czs5B9Mo1sLS01MlhSJYC0g5noL7+WjNh+NAydsxoMnVL/ETWcamiQmrPzy9AZWUV2C+oW/hY7KTDnUSWDygoKFSRY/pTk0kBo3D/yHvwyovPq7SXlpWr0Noi/PZ7gvAtDg4ObXXrcJssBdTV16sM7O7mJtFaDmhUE1HFxX/SqfGM9J6ykpySim82bRWPHjf1UZK1+itLAT1aMOWkg4ckBhMSVZ2ju5ur1M47yO5f9iAy6l18sHQ59tJsK0vigYNYu36DdPz18vJUNmn1V5YP4Bg+fufuZgz5+nhLzzY2NlKdKwED+qOJhN7xw04h2PETJ0V4rOz0VcwWnDh1WgQ8qWmHlWTxHBIcKD1rsyJLARy/e3t5Ii//rODJx9sLgwYGS/zdessgxGz+Fo2NjWL/f2LiBPxICtuzd5/U5/+VtPQj/yfB368fujk6qtC1QZC1BJiBZ5+eBtt/Z/qxRx9pxpODvT2G3z4UFhYWCHtuBi5fvgx2apqWUaNGavqK2v21ggcUFJ4Th6FpUyapDHzh4kXU1taK7W/l6nWoratT6dMWwfNmDyxa8FZbXWS1aUUB7XGQkZmF5dGr2+um0s7gx8KIufD0vFmlTVsE2UtAHUaCAwMI1vrPOarzDvcZN3aMToXnMfSiAMbzXnj+GXTrpr4jGzwoBOMffoh51GnRiwJYgh5OTpj35utqefOgwAGE/z2tdfyvJU3qxQfcOHAZHYU/Wb2WgJOiG8lSfXjoMMx4agrtHOYSTZcVvSuAham/dg2bt8Ti94RESTYbG2tMfXISQofdJtH0UekUBSgFY+g89rs4uLn1xrgHx8DevquySW+/naoAvUnZxkB6c4Jt8NCpTSYFdKr6DWDwDltAQ0Mjjh0/ifQjGWBsUFflfFERODTOyzsrDVFRUYnsnFzpuZ6AmRMnT3UIcu9QOMwBzocfrSDBq2FHGGBlVRVeCnuGQuEQiSltVDZs/AaHUtLg4XGTSLj08/XFrJkvIjX9MIGxu7BqxVKBKzAkn5uXT3HDPI2H7ZACNm2OFZcZoiLnw5ouNTDau/7zjVi29H1crb2KSpohOzs7nKVtjpnmCxDKwtgBzyBjCV272lGIfAWlZWXo5eKCMzk56EOQWq9eLigimCwh8QDmz52Dfn19UFpahrkRC8nqTig/JX7j4nciM+s4IubNaTZOs05tPGisAAY3+FbH1MmPC+H526PvH4mdu36mVHi2SITE0CHHxbkneJn8RRjA4kUR4ij8+YavxZLp2cNJoMVRkRHIzc8X0FcfyiU2NV0nwYso/J0vhOFLEympaXB3dxVKWfdpNCyIVkLK4JKSli4s4dWXw9BRzFBjH8D5PVbCjYENAx8c8FRV/SUY4z8L5ofjnagFQpB9dOLjmU88kIRIokdRmsy1d2/8smev6N/Q0IDXX3uF6Cy4o1jP/E1GlY9kZOLV2eGIXrUGZWQpyosSdYQrfEam70hocf/+ftK4mlY0VoBC8c89ntra/4ANFoATowprhRifESCFQgGeQR8vTzLxchQSaMLx/ScEikRELhYmXkaZIjP6x4UF5sLoEjs1LgyvLXl/MebMnolGsqa3310ilg+38Zh33TEC1+lfzL/IMdM1LRovAYXCSpgbz8ywoUPEeMp16evtTevxWDMeKigRwibPCuHZmzXzBVhZWgnGrSjbc/KUKhzOH2BInBMrbEn+NMPeXl4Ie3mWBKJyAubJSRPFzZGPlq9ECF2lGXLL4GZjq/OgsQL4oxMnjMey6FVY95k5nJ17CJCT/YDyLgDf6NhEfoADHN6ewt+YJYANPuszzs+MJlHK/B5KkXUxa9kI/f38sGXrd1i6LBpBgQG07eUJ6/D29kT64QwpVOa2kffeJRK0PAFKHtQRnvuYL6KibmdlP0548OUl9sx8BuAs0AOj7xPNnC3KpT2bEWEOeR98YJTYHi1pWQy5dTBKSkpxlvoM8PcjwHSYgMl5yfAdIC41NVfhRRAYO7XQ0KGEJ9aJJcROddqUyXDuyc61ATa2Ngjw7y/eYdSYcUcubjfkHQShnT9aD4YS/tiP7TviseLjD9oZ2jCaW7Y/GbzZkzPz8NBNGksGW62+qnULaHUkA23QugUYqJytsmVSQKuqMZIGkwUYyUS3KqbJAlpVjZE0mCzASCa6VTH/Bnoy/0KF7w+OAAAAAElFTkSuQmCC",
"matcherProtection": 1,
"supportedExtensions": [
{
"id": "hmac-secret",
"fail_if_unknown": false
},
{
"id": "credProtect",
"fail_if_unknown": false
}
],
"cryptoStrength": 128,
"description": "OpenSK authenticator",
"authenticatorVersion": 1,
"isSecondFactorOnly": false,
"userVerificationDetails": [
[
{
"userVerification": 1
},
{
"userVerification": 4
}
]
],
"attachmentHint": 6,
"attestationTypes": [
15880
],
"authenticationAlgorithm": 1,
"tcDisplay": 0
}

View File

@@ -117,7 +117,7 @@ index d72d20482..118ea6d68 100644
+ // Product
+ "OpenSK",
+ // Serial number
+ "v0.1",
+ "v1.0",
+];
+
// State for loading and holding applications.
@@ -189,7 +189,7 @@ index 2ebb384d8..4a7bfffdd 100644
+ // Product
+ "OpenSK",
+ // Serial number
+ "v0.1",
+ "v1.0",
+];
+
// State for loading and holding applications.

View File

@@ -0,0 +1,100 @@
diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs
index 6bb6c86..3bb8b5a 100644
--- a/chips/nrf52/src/uicr.rs
+++ b/chips/nrf52/src/uicr.rs
@@ -1,38 +1,45 @@
//! User information configuration registers
-//!
-//! Minimal implementation to support activation of the reset button on
-//! nRF52-DK.
+
use enum_primitive::cast::FromPrimitive;
-use kernel::common::registers::{register_bitfields, ReadWrite};
+use kernel::common::registers::{register_bitfields, register_structs, ReadWrite};
use kernel::common::StaticRef;
+use kernel::hil;
+use kernel::ReturnCode;
use crate::gpio::Pin;
const UICR_BASE: StaticRef<UicrRegisters> =
- unsafe { StaticRef::new(0x10001200 as *const UicrRegisters) };
-
-#[repr(C)]
-struct UicrRegisters {
- /// Mapping of the nRESET function (see POWER chapter for details)
- /// - Address: 0x200 - 0x204
- pselreset0: ReadWrite<u32, Pselreset::Register>,
- /// Mapping of the nRESET function (see POWER chapter for details)
- /// - Address: 0x204 - 0x208
- pselreset1: ReadWrite<u32, Pselreset::Register>,
- /// Access Port protection
- /// - Address: 0x208 - 0x20c
- approtect: ReadWrite<u32, ApProtect::Register>,
- /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
- /// - Address: 0x20c - 0x210
- nfcpins: ReadWrite<u32, NfcPins::Register>,
- _reserved1: [u32; 60],
- /// External circuitry to be supplied from VDD pin.
- /// - Address: 0x300 - 0x304
- extsupply: ReadWrite<u32, ExtSupply::Register>,
- /// GPIO reference voltage
- /// - Address: 0x304 - 0x308
- regout0: ReadWrite<u32, RegOut::Register>,
+ unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) };
+
+register_structs! {
+ UicrRegisters {
+ (0x000 => _reserved1),
+ /// Reserved for Nordic firmware design
+ (0x014 => nrffw: [ReadWrite<u32>; 13]),
+ (0x048 => _reserved2),
+ /// Reserved for Nordic hardware design
+ (0x050 => nrfhw: [ReadWrite<u32>; 12]),
+ /// Reserved for customer
+ (0x080 => customer: [ReadWrite<u32>; 32]),
+ (0x100 => _reserved3),
+ /// Mapping of the nRESET function (see POWER chapter for details)
+ (0x200 => pselreset0: ReadWrite<u32, Pselreset::Register>),
+ /// Mapping of the nRESET function (see POWER chapter for details)
+ (0x204 => pselreset1: ReadWrite<u32, Pselreset::Register>),
+ /// Access Port protection
+ (0x208 => approtect: ReadWrite<u32, ApProtect::Register>),
+ /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
+ /// - Address: 0x20c - 0x210
+ (0x20c => nfcpins: ReadWrite<u32, NfcPins::Register>),
+ (0x210 => debugctrl: ReadWrite<u32, DebugControl::Register>),
+ (0x214 => _reserved4),
+ /// External circuitry to be supplied from VDD pin.
+ (0x300 => extsupply: ReadWrite<u32, ExtSupply::Register>),
+ /// GPIO reference voltage
+ (0x304 => regout0: ReadWrite<u32, RegOut::Register>),
+ (0x308 => @END),
+ }
}
register_bitfields! [u32,
@@ -58,6 +65,21 @@ register_bitfields! [u32,
DISABLED = 0xff
]
],
+ /// Processor debug control
+ DebugControl [
+ CPUNIDEN OFFSET(0) NUMBITS(8) [
+ /// Enable
+ ENABLED = 0xff,
+ /// Disable
+ DISABLED = 0x00
+ ],
+ CPUFPBEN OFFSET(8) NUMBITS(8) [
+ /// Enable
+ ENABLED = 0xff,
+ /// Disable
+ DISABLED = 0x00
+ ]
+ ],
/// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
NfcPins [
/// Setting pins dedicated to NFC functionality

View File

@@ -0,0 +1,426 @@
diff --git a/boards/components/src/firmware_protection.rs b/boards/components/src/firmware_protection.rs
new file mode 100644
index 0000000..58695af
--- /dev/null
+++ b/boards/components/src/firmware_protection.rs
@@ -0,0 +1,70 @@
+//! Component for firmware protection syscall interface.
+//!
+//! This provides one Component, `FirmwareProtectionComponent`, which implements a
+//! userspace syscall interface to enable the code readout protection.
+//!
+//! Usage
+//! -----
+//! ```rust
+//! let crp = components::firmware_protection::FirmwareProtectionComponent::new(
+//! board_kernel,
+//! nrf52840::uicr::Uicr::new()
+//! )
+//! .finalize(
+//! components::firmware_protection_component_helper!(uicr));
+//! ```
+
+use core::mem::MaybeUninit;
+
+use capsules::firmware_protection;
+use kernel::capabilities;
+use kernel::component::Component;
+use kernel::create_capability;
+use kernel::hil;
+use kernel::static_init_half;
+
+// Setup static space for the objects.
+#[macro_export]
+macro_rules! firmware_protection_component_helper {
+ ($C:ty) => {{
+ use capsules::firmware_protection;
+ use core::mem::MaybeUninit;
+ static mut BUF: MaybeUninit<firmware_protection::FirmwareProtection<$C>> =
+ MaybeUninit::uninit();
+ &mut BUF
+ };};
+}
+
+pub struct FirmwareProtectionComponent<C: hil::firmware_protection::FirmwareProtection> {
+ board_kernel: &'static kernel::Kernel,
+ crp: C,
+}
+
+impl<C: 'static + hil::firmware_protection::FirmwareProtection> FirmwareProtectionComponent<C> {
+ pub fn new(board_kernel: &'static kernel::Kernel, crp: C) -> FirmwareProtectionComponent<C> {
+ FirmwareProtectionComponent {
+ board_kernel: board_kernel,
+ crp: crp,
+ }
+ }
+}
+
+impl<C: 'static + hil::firmware_protection::FirmwareProtection> Component
+ for FirmwareProtectionComponent<C>
+{
+ type StaticInput = &'static mut MaybeUninit<firmware_protection::FirmwareProtection<C>>;
+ type Output = &'static firmware_protection::FirmwareProtection<C>;
+
+ unsafe fn finalize(self, static_buffer: Self::StaticInput) -> Self::Output {
+ let grant_cap = create_capability!(capabilities::MemoryAllocationCapability);
+
+ static_init_half!(
+ static_buffer,
+ firmware_protection::FirmwareProtection<C>,
+ firmware_protection::FirmwareProtection::new(
+ self.crp,
+ self.board_kernel.create_grant(&grant_cap),
+ )
+ )
+ }
+}
diff --git a/boards/components/src/lib.rs b/boards/components/src/lib.rs
index 917497a..520408f 100644
--- a/boards/components/src/lib.rs
+++ b/boards/components/src/lib.rs
@@ -9,6 +9,7 @@ pub mod console;
pub mod crc;
pub mod debug_queue;
pub mod debug_writer;
+pub mod firmware_protection;
pub mod ft6x06;
pub mod gpio;
pub mod hd44780;
diff --git a/boards/nordic/nrf52840_dongle/src/main.rs b/boards/nordic/nrf52840_dongle/src/main.rs
index 118ea6d..76436f3 100644
--- a/boards/nordic/nrf52840_dongle/src/main.rs
+++ b/boards/nordic/nrf52840_dongle/src/main.rs
@@ -112,6 +112,7 @@ pub struct Platform {
'static,
nrf52840::usbd::Usbd<'static>,
>,
+ crp: &'static capsules::firmware_protection::FirmwareProtection<nrf52840::uicr::Uicr>,
}
impl kernel::Platform for Platform {
@@ -132,6 +133,7 @@ impl kernel::Platform for Platform {
capsules::analog_comparator::DRIVER_NUM => f(Some(self.analog_comparator)),
nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)),
capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)),
+ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)),
kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)),
_ => f(None),
}
@@ -355,6 +357,14 @@ pub unsafe fn reset_handler() {
)
.finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd));
+ let crp = components::firmware_protection::FirmwareProtectionComponent::new(
+ board_kernel,
+ nrf52840::uicr::Uicr::new(),
+ )
+ .finalize(components::firmware_protection_component_helper!(
+ nrf52840::uicr::Uicr
+ ));
+
nrf52_components::NrfClockComponent::new().finalize(());
let platform = Platform {
@@ -371,6 +381,7 @@ pub unsafe fn reset_handler() {
analog_comparator,
nvmc,
usb,
+ crp,
ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability),
};
diff --git a/boards/nordic/nrf52840dk/src/main.rs b/boards/nordic/nrf52840dk/src/main.rs
index b1d0d3c..3cfb38d 100644
--- a/boards/nordic/nrf52840dk/src/main.rs
+++ b/boards/nordic/nrf52840dk/src/main.rs
@@ -180,6 +180,7 @@ pub struct Platform {
'static,
nrf52840::usbd::Usbd<'static>,
>,
+ crp: &'static capsules::firmware_protection::FirmwareProtection<nrf52840::uicr::Uicr>,
}
impl kernel::Platform for Platform {
@@ -201,6 +202,7 @@ impl kernel::Platform for Platform {
capsules::nonvolatile_storage_driver::DRIVER_NUM => f(Some(self.nonvolatile_storage)),
nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)),
capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)),
+ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)),
kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)),
_ => f(None),
}
@@ -480,6 +482,14 @@ pub unsafe fn reset_handler() {
)
.finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd));
+ let crp = components::firmware_protection::FirmwareProtectionComponent::new(
+ board_kernel,
+ nrf52840::uicr::Uicr::new(),
+ )
+ .finalize(components::firmware_protection_component_helper!(
+ nrf52840::uicr::Uicr
+ ));
+
nrf52_components::NrfClockComponent::new().finalize(());
let platform = Platform {
@@ -497,6 +507,7 @@ pub unsafe fn reset_handler() {
nonvolatile_storage,
nvmc,
usb,
+ crp,
ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability),
};
diff --git a/capsules/src/driver.rs b/capsules/src/driver.rs
index ae458b3..f536dad 100644
--- a/capsules/src/driver.rs
+++ b/capsules/src/driver.rs
@@ -16,6 +16,7 @@ pub enum NUM {
Adc = 0x00005,
Dac = 0x00006,
AnalogComparator = 0x00007,
+ FirmwareProtection = 0x00008,
// Kernel
Ipc = 0x10000,
diff --git a/capsules/src/firmware_protection.rs b/capsules/src/firmware_protection.rs
new file mode 100644
index 0000000..8cf63d6
--- /dev/null
+++ b/capsules/src/firmware_protection.rs
@@ -0,0 +1,85 @@
+//! Provides userspace control of firmware protection on a board.
+//!
+//! This allows an application to enable firware readout protection,
+//! disabling JTAG interface and other ways to read/tamper the firmware.
+//! Of course, outside of a hardware bug, once set, the only way to enable
+//! programming/debugging is by fully erasing the flash.
+//!
+//! Usage
+//! -----
+//!
+//! ```rust
+//! # use kernel::static_init;
+//!
+//! let crp = static_init!(
+//! capsules::firmware_protection::FirmwareProtection,
+//! capsules::firmware_protection::FirmwareProtection::new(
+//! nrf52840::uicr::Uicr,
+//! board_kernel.create_grant(&grant_cap),
+//! );
+//! ```
+//!
+//! Syscall Interface
+//! -----------------
+//!
+//! - Stability: 0 - Draft
+//!
+//! ### Command
+//!
+//! Enable code readout protection on the current board.
+//!
+//! #### `command_num`
+//!
+//! - `0`: Driver check.
+//! - `1`: Get current firmware readout protection (aka CRP) state.
+//! - `2`: Set current firmware readout protection (aka CRP) state.
+//!
+
+use kernel::hil;
+use kernel::{AppId, Callback, Driver, Grant, ReturnCode};
+
+/// Syscall driver number.
+use crate::driver;
+pub const DRIVER_NUM: usize = driver::NUM::FirmwareProtection as usize;
+
+pub struct FirmwareProtection<C: hil::firmware_protection::FirmwareProtection> {
+ crp_unit: C,
+ apps: Grant<Option<Callback>>,
+}
+
+impl<C: hil::firmware_protection::FirmwareProtection> FirmwareProtection<C> {
+ pub fn new(crp_unit: C, apps: Grant<Option<Callback>>) -> Self {
+ Self { crp_unit, apps }
+ }
+}
+
+impl<C: hil::firmware_protection::FirmwareProtection> Driver for FirmwareProtection<C> {
+ ///
+ /// ### Command numbers
+ ///
+ /// * `0`: Returns non-zero to indicate the driver is present.
+ /// * `1`: Gets firmware protection state.
+ /// * `2`: Sets firmware protection state.
+ fn command(&self, command_num: usize, data: usize, _: usize, appid: AppId) -> ReturnCode {
+ match command_num {
+ // return if driver is available
+ 0 => ReturnCode::SUCCESS,
+
+ 1 => self
+ .apps
+ .enter(appid, |_, _| ReturnCode::SuccessWithValue {
+ value: self.crp_unit.get_protection() as usize,
+ })
+ .unwrap_or_else(|err| err.into()),
+
+ // sets firmware protection
+ 2 => self
+ .apps
+ .enter(appid, |_, _| self.crp_unit.set_protection(data.into()))
+ .unwrap_or_else(|err| err.into()),
+
+ // default
+ _ => ReturnCode::ENOSUPPORT,
+ }
+ }
+}
diff --git a/capsules/src/lib.rs b/capsules/src/lib.rs
index e4423fe..7538aad 100644
--- a/capsules/src/lib.rs
+++ b/capsules/src/lib.rs
@@ -22,6 +22,7 @@ pub mod crc;
pub mod dac;
pub mod debug_process_restart;
pub mod driver;
+pub mod firmware_protection;
pub mod fm25cl;
pub mod ft6x06;
pub mod fxos8700cq;
diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs
index 3bb8b5a..ea96cb2 100644
--- a/chips/nrf52/src/uicr.rs
+++ b/chips/nrf52/src/uicr.rs
@@ -1,13 +1,14 @@
//! User information configuration registers
-
use enum_primitive::cast::FromPrimitive;
+use hil::firmware_protection::ProtectionLevel;
use kernel::common::registers::{register_bitfields, register_structs, ReadWrite};
use kernel::common::StaticRef;
use kernel::hil;
use kernel::ReturnCode;
use crate::gpio::Pin;
+use crate::nvmc::NVMC;
const UICR_BASE: StaticRef<UicrRegisters> =
unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) };
@@ -210,3 +211,49 @@ impl Uicr {
self.registers.approtect.write(ApProtect::PALL::ENABLED);
}
}
+
+impl hil::firmware_protection::FirmwareProtection for Uicr {
+ fn get_protection(&self) -> ProtectionLevel {
+ let ap_protect_state = self.is_ap_protect_enabled();
+ let cpu_debug_state = self
+ .registers
+ .debugctrl
+ .matches_all(DebugControl::CPUNIDEN::ENABLED + DebugControl::CPUFPBEN::ENABLED);
+ match (ap_protect_state, cpu_debug_state) {
+ (false, _) => ProtectionLevel::NoProtection,
+ (true, true) => ProtectionLevel::JtagDisabled,
+ (true, false) => ProtectionLevel::FullyLocked,
+ }
+ }
+
+ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode {
+ let current_level = self.get_protection();
+ if current_level > level || level == ProtectionLevel::Unknown {
+ return ReturnCode::EINVAL;
+ }
+ if current_level == level {
+ return ReturnCode::EALREADY;
+ }
+
+ unsafe { NVMC.configure_writeable() };
+ if level >= ProtectionLevel::JtagDisabled {
+ self.set_ap_protect();
+ }
+
+ if level >= ProtectionLevel::FullyLocked {
+ // Prevent CPU debug and flash patching. Leaving these enabled could
+ // allow to circumvent protection.
+ self.registers
+ .debugctrl
+ .write(DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED);
+ // TODO(jmichel): prevent returning into bootloader if present
+ }
+ unsafe { NVMC.configure_readonly() };
+
+ if self.get_protection() == level {
+ ReturnCode::SUCCESS
+ } else {
+ ReturnCode::FAIL
+ }
+ }
+}
diff --git a/kernel/src/hil/firmware_protection.rs b/kernel/src/hil/firmware_protection.rs
new file mode 100644
index 0000000..de08246
--- /dev/null
+++ b/kernel/src/hil/firmware_protection.rs
@@ -0,0 +1,48 @@
+//! Interface for Firmware Protection, also called Code Readout Protection.
+
+use crate::returncode::ReturnCode;
+
+#[derive(PartialOrd, PartialEq)]
+pub enum ProtectionLevel {
+ /// Unsupported feature
+ Unknown = 0,
+ /// This should be the factory default for the chip.
+ NoProtection = 1,
+ /// At this level, only JTAG/SWD are disabled but other debugging
+ /// features may still be enabled.
+ JtagDisabled = 2,
+ /// This is the maximum level of protection the chip supports.
+ /// At this level, JTAG and all other features are expected to be
+ /// disabled and only a full chip erase may allow to recover from
+ /// that state.
+ FullyLocked = 0xff,
+}
+
+impl From<usize> for ProtectionLevel {
+ fn from(value: usize) -> Self {
+ match value {
+ 1 => ProtectionLevel::NoProtection,
+ 2 => ProtectionLevel::JtagDisabled,
+ 0xff => ProtectionLevel::FullyLocked,
+ _ => ProtectionLevel::Unknown,
+ }
+ }
+}
+
+pub trait FirmwareProtection {
+ /// Gets the current firmware protection level.
+ /// This doesn't fail and always returns a value.
+ fn get_protection(&self) -> ProtectionLevel;
+
+ /// Sets the firmware protection level.
+ /// There are four valid return values:
+ /// - SUCCESS: protection level has been set to `level`
+ /// - FAIL: something went wrong while setting the protection
+ /// level and the effective protection level is not the one
+ /// that was requested.
+ /// - EALREADY: the requested protection level is already the
+ /// level that is set.
+ /// - EINVAL: unsupported protection level or the requested
+ /// protection level is lower than the currently set one.
+ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode;
+}
diff --git a/kernel/src/hil/mod.rs b/kernel/src/hil/mod.rs
index 4f42afa..83e7702 100644
--- a/kernel/src/hil/mod.rs
+++ b/kernel/src/hil/mod.rs
@@ -8,6 +8,7 @@ pub mod dac;
pub mod digest;
pub mod eic;
pub mod entropy;
+pub mod firmware_protection;
pub mod flash;
pub mod gpio;
pub mod gpio_async;

View File

@@ -44,11 +44,9 @@ cargo test --manifest-path tools/heapviz/Cargo.toml
echo "Checking that CTAP2 builds properly..."
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_ctap2_1
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 debug_allocations
cargo check --release --target=thumbv7em-none-eabi --features ram_storage
cargo check --release --target=thumbv7em-none-eabi --features verbose
cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ctap1
cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ctap1,panic_console,debug_allocations,verbose
@@ -93,7 +91,7 @@ then
cargo test --release --features std
cd ../..
cd libraries/crypto
RUSTFLAGS='-C target-feature=+aes' cargo test --release --features std,derive_debug
RUSTFLAGS='-C target-feature=+aes' cargo test --release --features std
cd ../..
cd libraries/persistent_store
cargo test --release --features std
@@ -105,7 +103,7 @@ then
cargo test --features std
cd ../..
cd libraries/crypto
RUSTFLAGS='-C target-feature=+aes' cargo test --features std,derive_debug
RUSTFLAGS='-C target-feature=+aes' cargo test --features std
cd ../..
cd libraries/persistent_store
cargo test --features std
@@ -117,16 +115,4 @@ then
echo "Running unit tests on the desktop (debug mode + 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

View File

@@ -44,3 +44,6 @@ rustup target add thumbv7em-none-eabi
# Install dependency to create applications.
mkdir -p elf2tab
cargo install elf2tab --version 0.6.0 --root elf2tab/
# Install python dependencies to factory configure OpenSK (crypto, JTAG lockdown)
pip3 install --user --upgrade colorama tqdm cryptography "fido2>=0.9.1"

436
src/ctap/apdu.rs Normal file
View File

@@ -0,0 +1,436 @@
// 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 alloc::vec::Vec;
use byteorder::{BigEndian, ByteOrder};
use core::convert::TryFrom;
const APDU_HEADER_LEN: usize = 4;
#[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types, dead_code)]
pub enum ApduStatusCode {
SW_SUCCESS = 0x90_00,
/// Command successfully executed; 'XX' bytes of data are
/// available and can be requested using GET RESPONSE.
SW_GET_RESPONSE = 0x61_00,
SW_MEMERR = 0x65_01,
SW_WRONG_DATA = 0x6a_80,
SW_WRONG_LENGTH = 0x67_00,
SW_COND_USE_NOT_SATISFIED = 0x69_85,
SW_COMMAND_NOT_ALLOWED = 0x69_86,
SW_FILE_NOT_FOUND = 0x6a_82,
SW_INCORRECT_P1P2 = 0x6a_86,
/// Instruction code not supported or invalid
SW_INS_INVALID = 0x6d_00,
SW_CLA_INVALID = 0x6e_00,
SW_INTERNAL_EXCEPTION = 0x6f_00,
}
impl From<ApduStatusCode> for u16 {
fn from(code: ApduStatusCode) -> Self {
code as u16
}
}
#[allow(dead_code)]
pub enum ApduInstructions {
Select = 0xA4,
ReadBinary = 0xB0,
GetResponse = 0xC0,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[allow(dead_code)]
pub struct ApduHeader {
pub cla: u8,
pub ins: u8,
pub p1: u8,
pub p2: u8,
}
impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader {
fn from(header: &[u8; APDU_HEADER_LEN]) -> Self {
ApduHeader {
cla: header[0],
ins: header[1],
p1: header[2],
p2: header[3],
}
}
}
#[derive(Clone, Debug, PartialEq)]
/// The APDU cases
pub enum Case {
Le1,
Lc1Data,
Lc1DataLe1,
Lc3Data,
Lc3DataLe1,
Lc3DataLe2,
Le3,
}
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)]
pub enum ApduType {
Instruction,
Short(Case),
Extended(Case),
}
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)]
pub struct APDU {
pub header: ApduHeader,
pub lc: u16,
pub data: Vec<u8>,
pub le: u32,
pub case_type: ApduType,
}
impl TryFrom<&[u8]> for APDU {
type Error = ApduStatusCode;
fn try_from(frame: &[u8]) -> Result<Self, ApduStatusCode> {
if frame.len() < APDU_HEADER_LEN as usize {
return Err(ApduStatusCode::SW_WRONG_DATA);
}
// +-----+-----+----+----+
// header | CLA | INS | P1 | P2 |
// +-----+-----+----+----+
let (header, payload) = frame.split_at(APDU_HEADER_LEN);
if payload.is_empty() {
// Lc is zero-bytes in length
return Ok(APDU {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: 0x00,
data: Vec::new(),
le: 0x00,
case_type: ApduType::Instruction,
});
}
// Lc is not zero-bytes in length, let's figure out how long it is
let byte_0 = payload[0];
if payload.len() == 1 {
// There is only one byte in the payload, that byte cannot be Lc because that would
// entail at *least* one another byte in the payload (for the command data)
return Ok(APDU {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: 0x00,
data: Vec::new(),
le: if byte_0 == 0x00 {
// Ne = 256
0x100
} else {
byte_0.into()
},
case_type: ApduType::Short(Case::Le1),
});
}
if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 {
// Lc is one-byte long and since the size specified by Lc covers the rest of the
// payload there's no Le at the end
return Ok(APDU {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: byte_0.into(),
data: payload[1..].to_vec(),
case_type: ApduType::Short(Case::Lc1Data),
le: 0,
});
}
if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 {
// Lc is one-byte long and since the size specified by Lc covers the rest of the
// payload with ONE additional byte that byte must be Le
let last_byte: u32 = (*payload.last().unwrap()).into();
return Ok(APDU {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: byte_0.into(),
data: payload[1..(payload.len() - 1)].to_vec(),
le: if last_byte == 0x00 { 0x100 } else { last_byte },
case_type: ApduType::Short(Case::Lc1DataLe1),
});
}
if payload.len() > 2 {
// Lc is possibly three-bytes long
let extended_apdu_lc = BigEndian::read_u16(&payload[1..3]) as usize;
if payload.len() < extended_apdu_lc + 3 {
return Err(ApduStatusCode::SW_WRONG_LENGTH);
}
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 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
// have an extended-length APDU
let last_byte: u32 = (*payload.last().unwrap()).into();
return Ok(APDU {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: extended_apdu_lc as u16,
data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(),
le: match extended_apdu_le_len {
0 => 0,
1 => {
if last_byte == 0x00 {
0x100
} else {
last_byte
}
}
2 => {
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
if le_parsed == 0x00 {
0x10000
} else {
le_parsed as u32
}
}
3 => {
let le_first_byte: u32 =
(*payload.get(payload.len() - 3).unwrap()).into();
if le_first_byte != 0x00 {
return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION);
}
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
if le_parsed == 0x00 {
0x10000
} else {
le_parsed as u32
}
}
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
},
case_type: ApduType::Extended(match extended_apdu_le_len {
0 => Case::Lc3Data,
1 => Case::Lc3DataLe1,
2 => Case::Lc3DataLe2,
3 => Case::Le3,
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
}),
});
}
}
Err(ApduStatusCode::SW_INTERNAL_EXCEPTION)
}
}
#[cfg(test)]
mod test {
use super::*;
fn pass_frame(frame: &[u8]) -> Result<APDU, ApduStatusCode> {
APDU::try_from(frame)
}
#[test]
fn test_case_type_1() {
let frame: [u8; 4] = [0x00, 0x12, 0x00, 0x80];
let response = pass_frame(&frame);
assert!(response.is_ok());
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0x12,
p1: 0x00,
p2: 0x80,
},
lc: 0x00,
data: Vec::new(),
le: 0x00,
case_type: ApduType::Instruction,
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_2_short() {
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x0f];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0xb0,
p1: 0x00,
p2: 0x00,
},
lc: 0x00,
data: Vec::new(),
le: 0x0f,
case_type: ApduType::Short(Case::Le1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_2_short_le() {
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x00];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0xb0,
p1: 0x00,
p2: 0x00,
},
lc: 0x00,
data: Vec::new(),
le: 0x100,
case_type: ApduType::Short(Case::Le1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_3_short() {
let frame: [u8; 7] = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
let payload = [0xe1, 0x04];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x00,
p2: 0x0c,
},
lc: 0x02,
data: payload.to_vec(),
le: 0x00,
case_type: ApduType::Short(Case::Lc1Data),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_4_short() {
let frame: [u8; 13] = [
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0xff,
];
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x04,
p2: 0x00,
},
lc: 0x07,
data: payload.to_vec(),
le: 0xff,
case_type: ApduType::Short(Case::Lc1DataLe1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_4_short_le() {
let frame: [u8; 13] = [
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00,
];
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x04,
p2: 0x00,
},
lc: 0x07,
data: payload.to_vec(),
le: 0x100,
case_type: ApduType::Short(Case::Lc1DataLe1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_invalid_apdu_header_length() {
let frame: [u8; 3] = [0x00, 0x12, 0x00];
let response = pass_frame(&frame);
assert_eq!(Err(ApduStatusCode::SW_WRONG_DATA), response);
}
#[test]
fn test_extended_length_apdu() {
let frame: [u8; 186] = [
0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0xb1, 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49,
0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8,
0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72,
0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c,
0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29,
0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16,
0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70,
0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d,
0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40,
0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80,
0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45,
0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7,
0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb,
0xb6, 0x2b, 0x00, 0x00,
];
let payload: &[u8] = &frame[7..frame.len() - 2];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0x02,
p1: 0x03,
p2: 0x00,
},
lc: 0xb1,
data: payload.to_vec(),
le: 0x10000,
case_type: ApduType::Extended(Case::Lc3DataLe2),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_previously_unsupported_case_type() {
let frame: [u8; 73] = [
0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x40, 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d,
0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3,
0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b,
0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7,
0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d,
0xcb, 0x00, 0x00,
];
let payload: &[u8] = &frame[7..frame.len() - 2];
let response = pass_frame(&frame);
let expected = APDU {
header: ApduHeader {
cla: 0x00,
ins: 0x01,
p1: 0x03,
p2: 0x00,
},
lc: 0x40,
data: payload.to_vec(),
le: 0x10000,
case_type: ApduType::Extended(Case::Lc3DataLe2),
};
assert_eq!(Ok(expected), response);
}
}

1760
src/ctap/client_pin.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,472 @@
// 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::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorConfigParameters;
use super::customization::ENTERPRISE_ATTESTATION_MODE;
use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams};
use super::response::ResponseData;
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use alloc::vec;
/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig.
fn process_enable_enterprise_attestation(
persistent_store: &mut PersistentStore,
) -> Result<ResponseData, Ctap2StatusCode> {
if ENTERPRISE_ATTESTATION_MODE.is_some() {
persistent_store.enable_enterprise_attestation()?;
Ok(ResponseData::AuthenticatorConfig)
} else {
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
}
/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig.
fn process_toggle_always_uv(
persistent_store: &mut PersistentStore,
) -> Result<ResponseData, Ctap2StatusCode> {
persistent_store.toggle_always_uv()?;
Ok(ResponseData::AuthenticatorConfig)
}
/// 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 {
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,
client_pin: &mut ClientPin,
params: AuthenticatorConfigParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = params;
let enforce_uv = match sub_command {
ConfigSubCommand::ToggleAlwaysUv => false,
_ => true,
} && persistent_store.has_always_uv()?;
if persistent_store.pin_hash()?.is_some() || enforce_uv {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
// 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);
}
}
client_pin.verify_pin_uv_auth_token(
&config_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
client_pin.has_permission(PinPermission::AuthenticatorConfiguration)?;
}
match sub_command {
ConfigSubCommand::EnableEnterpriseAttestation => {
process_enable_enterprise_attestation(persistent_store)
}
ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(persistent_store),
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 crate::ctap::customization::ENFORCE_ALWAYS_UV;
use crate::ctap::data_formats::PinUvAuthProtocol;
use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token;
use crypto::rng256::ThreadRng256;
#[test]
fn test_process_enable_enterprise_attestation() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
if ENTERPRISE_ATTESTATION_MODE.is_some() {
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.enterprise_attestation(), Ok(true));
} else {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}
#[test]
fn test_process_toggle_always_uv() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(persistent_store.has_always_uv().unwrap());
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
if ENFORCE_ALWAYS_UV {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
);
} else {
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(!persistent_store.has_always_uv().unwrap());
}
}
fn test_helper_process_toggle_always_uv_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol);
persistent_store.set_pin(&[0x88; 16], 4).unwrap();
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]);
let pin_uv_auth_param =
authenticate_pin_uv_auth_token(&pin_uv_auth_token, &config_data, pin_uv_auth_protocol);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
if ENFORCE_ALWAYS_UV {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
);
return;
}
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(persistent_store.has_always_uv().unwrap());
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: Some(pin_uv_auth_param),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(!persistent_store.has_always_uv().unwrap());
}
#[test]
fn test_process_toggle_always_uv_with_pin_v1() {
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_toggle_always_uv_with_pin_v2() {
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V2);
}
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(PinUvAuthProtocol::V1),
}
}
#[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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
// 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 client_pin, 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_uv_auth_param = 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_uv_auth_param);
let config_response = process_config(&mut persistent_store, &mut client_pin, 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_uv_auth_param = 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_uv_auth_param);
let config_response = process_config(&mut persistent_store, &mut client_pin, 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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
// 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 client_pin, 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_uv_auth_param = 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_uv_auth_param.clone());
let config_response = process_config(&mut persistent_store, &mut client_pin, 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_uv_auth_param.clone());
let config_response = process_config(&mut persistent_store, &mut client_pin, 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_uv_auth_param);
let config_response = process_config(&mut persistent_store, &mut client_pin, 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_set_min_pin_length_force_pin_change_implicit() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
persistent_store.set_pin(&[0x88; 16], 4).unwrap();
// Increase min PIN, force PIN change.
let min_pin_length = 6;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_uv_auth_param = Some(vec![
0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E,
0xA8, 0xC8,
]);
config_params.pin_uv_auth_param = pin_uv_auth_param;
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(persistent_store.has_force_pin_change(), Ok(true));
}
#[test]
fn test_process_set_min_pin_length_force_pin_change_explicit() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
persistent_store.set_pin(&[0x88; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1,
0x53, 0xD7,
]);
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(persistent_store.min_pin_length().unwrap()),
min_pin_length_rp_ids: None,
force_change_pin: Some(true),
};
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
};
let config_response = process_config(&mut persistent_store, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.has_force_pin_change(), Ok(true));
}
#[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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
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 client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}

View File

@@ -0,0 +1,928 @@
// 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::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorCredentialManagementParameters;
use super::data_formats::{
CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource,
PublicKeyCredentialUserEntity,
};
use super::response::{AuthenticatorCredentialManagementResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use super::{StatefulCommand, StatefulPermission};
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crypto::sha256::Sha256;
use crypto::Hash256;
use libtock_drivers::timer::ClockValue;
/// Generates a set with all existing RP IDs.
fn get_stored_rp_ids(
persistent_store: &PersistentStore,
) -> Result<BTreeSet<String>, Ctap2StatusCode> {
let mut rp_set = BTreeSet::new();
let mut iter_result = Ok(());
for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? {
rp_set.insert(credential.rp_id);
}
iter_result?;
Ok(rp_set)
}
/// Generates the response for subcommands enumerating RPs.
fn enumerate_rps_response(
rp_id: String,
total_rps: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = Some(Sha256::hash(rp_id.as_bytes()).to_vec());
let rp = Some(PublicKeyCredentialRpEntity {
rp_id,
rp_name: None,
rp_icon: None,
});
Ok(AuthenticatorCredentialManagementResponse {
rp,
rp_id_hash,
total_rps,
..Default::default()
})
}
/// Generates the response for subcommands enumerating credentials.
fn enumerate_credentials_response(
credential: PublicKeyCredentialSource,
total_credentials: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let PublicKeyCredentialSource {
key_type,
credential_id,
private_key,
rp_id: _,
user_handle,
user_display_name,
cred_protect_policy,
creation_order: _,
user_name,
user_icon,
cred_blob: _,
large_blob_key,
} = credential;
let user = PublicKeyCredentialUserEntity {
user_id: user_handle,
user_name,
user_display_name,
user_icon,
};
let credential_id = PublicKeyCredentialDescriptor {
key_type,
key_id: credential_id,
transports: None, // You can set USB as a hint here.
};
let public_key = CoseKey::from(private_key.genpk());
Ok(AuthenticatorCredentialManagementResponse {
user: Some(user),
credential_id: Some(credential_id),
public_key: Some(public_key),
total_credentials,
cred_protect: cred_protect_policy,
large_blob_key,
..Default::default()
})
}
/// Check if the token permissions have the correct associated RP ID.
///
/// Either no RP ID is associated, or the RP ID matches the stored credential.
fn check_rp_id_permissions(
persistent_store: &mut PersistentStore,
client_pin: &mut ClientPin,
credential_id: &[u8],
) -> Result<(), Ctap2StatusCode> {
// Pre-check a sufficient condition before calling the store.
if client_pin.has_no_rp_id_permission().is_ok() {
return Ok(());
}
let (_, credential) = persistent_store.find_credential_item(credential_id)?;
client_pin.has_no_or_rp_id_permission(&credential.rp_id)
}
/// Processes the subcommand getCredsMetadata for CredentialManagement.
fn process_get_creds_metadata(
persistent_store: &PersistentStore,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
Ok(AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(persistent_store.count_credentials()? as u64),
max_possible_remaining_resident_credentials_count: Some(
persistent_store.remaining_credentials()? as u64,
),
..Default::default()
})
}
/// Processes the subcommand enumerateRPsBegin for CredentialManagement.
fn process_enumerate_rps_begin(
persistent_store: &PersistentStore,
stateful_command_permission: &mut StatefulPermission,
now: ClockValue,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_set = get_stored_rp_ids(persistent_store)?;
let total_rps = rp_set.len();
if total_rps > 1 {
stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1));
}
// TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first()
let rp_id = rp_set
.into_iter()
.next()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
enumerate_rps_response(rp_id, Some(total_rps as u64))
}
/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement.
fn process_enumerate_rps_get_next_rp(
persistent_store: &PersistentStore,
stateful_command_permission: &mut StatefulPermission,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_index = stateful_command_permission.next_enumerate_rp()?;
let rp_set = get_stored_rp_ids(persistent_store)?;
// A BTreeSet is already sorted.
let rp_id = rp_set
.into_iter()
.nth(rp_id_index)
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
enumerate_rps_response(rp_id, None)
}
/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement.
fn process_enumerate_credentials_begin(
persistent_store: &PersistentStore,
stateful_command_permission: &mut StatefulPermission,
client_pin: &mut ClientPin,
sub_command_params: CredentialManagementSubCommandParameters,
now: ClockValue,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = sub_command_params
.rp_id_hash
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?;
let mut iter_result = Ok(());
let iter = persistent_store.iter_credentials(&mut iter_result)?;
let mut rp_credentials: Vec<usize> = iter
.filter_map(|(key, credential)| {
let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes());
if cred_rp_id_hash == rp_id_hash.as_slice() {
Some(key)
} else {
None
}
})
.collect();
iter_result?;
let total_credentials = rp_credentials.len();
let current_key = rp_credentials
.pop()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
let credential = persistent_store.get_credential(current_key)?;
if total_credentials > 1 {
stateful_command_permission
.set_command(now, StatefulCommand::EnumerateCredentials(rp_credentials));
}
enumerate_credentials_response(credential, Some(total_credentials as u64))
}
/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement.
fn process_enumerate_credentials_get_next_credential(
persistent_store: &PersistentStore,
stateful_command_permission: &mut StatefulPermission,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let credential_key = stateful_command_permission.next_enumerate_credential()?;
let credential = persistent_store.get_credential(credential_key)?;
enumerate_credentials_response(credential, None)
}
/// Processes the subcommand deleteCredential for CredentialManagement.
fn process_delete_credential(
persistent_store: &mut PersistentStore,
client_pin: &mut ClientPin,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
check_rp_id_permissions(persistent_store, client_pin, &credential_id)?;
persistent_store.delete_credential(&credential_id)
}
/// Processes the subcommand updateUserInformation for CredentialManagement.
fn process_update_user_information(
persistent_store: &mut PersistentStore,
client_pin: &mut ClientPin,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
let user = sub_command_params
.user
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
check_rp_id_permissions(persistent_store, client_pin, &credential_id)?;
persistent_store.update_credential(&credential_id, user)
}
/// Processes the CredentialManagement command and all its subcommands.
pub fn process_credential_management(
persistent_store: &mut PersistentStore,
stateful_command_permission: &mut StatefulPermission,
client_pin: &mut ClientPin,
cred_management_params: AuthenticatorCredentialManagementParameters,
now: ClockValue,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorCredentialManagementParameters {
sub_command,
sub_command_params,
pin_uv_auth_protocol,
pin_uv_auth_param,
} = cred_management_params;
match (sub_command, stateful_command_permission.get_command()) {
(
CredentialManagementSubCommand::EnumerateRpsGetNextRp,
Ok(StatefulCommand::EnumerateRps(_)),
)
| (
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
Ok(StatefulCommand::EnumerateCredentials(_)),
) => stateful_command_permission.check_command_permission(now)?,
(_, _) => {
stateful_command_permission.clear();
}
}
match sub_command {
CredentialManagementSubCommand::GetCredsMetadata
| CredentialManagementSubCommand::EnumerateRpsBegin
| CredentialManagementSubCommand::EnumerateCredentialsBegin
| CredentialManagementSubCommand::DeleteCredential
| CredentialManagementSubCommand::UpdateUserInformation => {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut management_data = vec![sub_command as u8];
if let Some(sub_command_params) = sub_command_params.clone() {
if !cbor::write(sub_command_params.into(), &mut management_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
client_pin.verify_pin_uv_auth_token(
&management_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
// The RP ID permission is handled differently per subcommand below.
client_pin.has_permission(PinPermission::CredentialManagement)?;
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp
| CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {}
}
let response = match sub_command {
CredentialManagementSubCommand::GetCredsMetadata => {
client_pin.has_no_rp_id_permission()?;
Some(process_get_creds_metadata(persistent_store)?)
}
CredentialManagementSubCommand::EnumerateRpsBegin => {
client_pin.has_no_rp_id_permission()?;
Some(process_enumerate_rps_begin(
persistent_store,
stateful_command_permission,
now,
)?)
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some(
process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?,
),
CredentialManagementSubCommand::EnumerateCredentialsBegin => {
Some(process_enumerate_credentials_begin(
persistent_store,
stateful_command_permission,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
now,
)?)
}
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {
Some(process_enumerate_credentials_get_next_credential(
persistent_store,
stateful_command_permission,
)?)
}
CredentialManagementSubCommand::DeleteCredential => {
process_delete_credential(
persistent_store,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
CredentialManagementSubCommand::UpdateUserInformation => {
process_update_user_information(
persistent_store,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
};
Ok(ResponseData::AuthenticatorCredentialManagement(response))
}
#[cfg(test)]
mod test {
use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType};
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::super::CtapState;
use super::*;
use crypto::rng256::{Rng256, ThreadRng256};
const CLOCK_FREQUENCY_HZ: usize = 32768;
const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
fn create_credential_source(rng: &mut impl Rng256) -> PublicKeyCredentialSource {
let private_key = crypto::ecdsa::SecKey::gensk(rng);
PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: rng.gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x01],
user_display_name: Some("display_name".to_string()),
cred_protect_policy: None,
creation_order: 0,
user_name: Some("name".to_string()),
user_icon: Some("icon".to_string()),
cred_blob: None,
large_blob_key: None,
}
}
fn test_helper_process_get_creds_metadata(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol);
let credential_source = create_credential_source(&mut rng);
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.client_pin = client_pin;
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8];
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
&pin_uv_auth_token,
&management_data,
pin_uv_auth_protocol,
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let initial_capacity = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(0));
response
.max_possible_remaining_resident_credentials_count
.unwrap()
}
_ => panic!("Invalid response type"),
};
ctap_state
.persistent_store
.store_credential(credential_source)
.unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
pin_uv_auth_param: Some(pin_uv_auth_param),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(1));
assert_eq!(
response.max_possible_remaining_resident_credentials_count,
Some(initial_capacity - 1)
);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_get_creds_metadata_v1() {
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_creds_metadata_v2() {
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V2);
}
#[test]
fn test_process_enumerate_rps_with_uv() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let credential_source1 = create_credential_source(&mut rng);
let mut credential_source2 = create_credential_source(&mut rng);
credential_source2.rp_id = "another.example.com".to_string();
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.client_pin = client_pin;
ctap_state
.persistent_store
.store_credential(credential_source1)
.unwrap();
ctap_state
.persistent_store
.store_credential(credential_source2)
.unwrap();
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let first_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, Some(2));
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let second_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, None);
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
assert!(first_rp_id != second_rp_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_rps_completeness() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let credential_source = create_credential_source(&mut rng);
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.client_pin = client_pin;
const NUM_CREDENTIALS: usize = 20;
for i in 0..NUM_CREDENTIALS {
let mut credential = credential_source.clone();
credential.rp_id = i.to_string();
ctap_state
.persistent_store
.store_credential(credential)
.unwrap();
}
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let mut rp_set = BTreeSet::new();
// This mut is just to make the test code shorter.
// The command is different on the first loop iteration.
let mut cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
for _ in 0..NUM_CREDENTIALS {
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
if rp_set.is_empty() {
assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64));
} else {
assert_eq!(response.total_rps, None);
}
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
assert!(!rp_set.contains(&rp_id));
rp_set.insert(rp_id);
}
_ => panic!("Invalid response type"),
};
cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
}
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_credentials_with_uv() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let credential_source1 = create_credential_source(&mut rng);
let mut credential_source2 = create_credential_source(&mut rng);
credential_source2.user_handle = vec![0x02];
credential_source2.user_name = Some("user2".to_string());
credential_source2.user_display_name = Some("User Two".to_string());
credential_source2.user_icon = Some("icon2".to_string());
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.client_pin = client_pin;
ctap_state
.persistent_store
.store_credential(credential_source1)
.unwrap();
ctap_state
.persistent_store
.store_credential(credential_source2)
.unwrap();
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28,
0x2B, 0x68,
]);
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()),
credential_id: None,
user: None,
};
// RP ID hash:
// A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let first_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, Some(2));
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let second_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, None);
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
assert!(first_credential_id != second_credential_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_delete_credential() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut credential_source = create_credential_source(&mut rng);
credential_source.credential_id = vec![0x1D; 32];
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.client_pin = client_pin;
ctap_state
.persistent_store
.store_credential(credential_source)
.unwrap();
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89,
0x9C, 0x64,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: None,
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params.clone()),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param: pin_uv_auth_param.clone(),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
);
}
#[test]
fn test_process_update_user_information() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut credential_source = create_credential_source(&mut rng);
credential_source.credential_id = vec![0x1D; 32];
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.client_pin = client_pin;
ctap_state
.persistent_store
.store_credential(credential_source)
.unwrap();
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD,
0x9D, 0xB7,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let new_user = PublicKeyCredentialUserEntity {
user_id: vec![0xFF],
user_name: Some("new_name".to_string()),
user_display_name: Some("new_display_name".to_string()),
user_icon: Some("new_icon".to_string()),
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: Some(new_user),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::UpdateUserInformation,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let updated_credential = ctap_state
.persistent_store
.find_credential("example.com", &[0x1D; 32], false)
.unwrap()
.unwrap();
assert_eq!(updated_credential.user_handle, vec![0x01]);
assert_eq!(&updated_credential.user_name.unwrap(), "new_name");
assert_eq!(
&updated_credential.user_display_name.unwrap(),
"new_display_name"
);
assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon");
}
#[test]
fn test_process_credential_management_invalid_pin_uv_auth_param() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param: Some(vec![0u8; 16]),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

147
src/ctap/crypto_wrapper.rs Normal file
View File

@@ -0,0 +1,147 @@
// Copyright 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 crate::ctap::status_code::Ctap2StatusCode;
use alloc::vec;
use alloc::vec::Vec;
use crypto::cbc::{cbc_decrypt, cbc_encrypt};
use crypto::rng256::Rng256;
/// Wraps the AES256-CBC encryption to match what we need in CTAP.
pub fn aes256_cbc_encrypt(
rng: &mut dyn Rng256,
aes_enc_key: &crypto::aes256::EncryptionKey,
plaintext: &[u8],
embeds_iv: bool,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if plaintext.len() % 16 != 0 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let iv = if embeds_iv {
let random_bytes = rng.gen_uniform_u8x32();
*array_ref!(random_bytes, 0, 16)
} else {
[0u8; 16]
};
let mut blocks = Vec::with_capacity(plaintext.len() / 16);
// TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable.
for block in plaintext.chunks_exact(16) {
blocks.push(*array_ref!(block, 0, 16));
}
cbc_encrypt(aes_enc_key, iv, &mut blocks);
let mut ciphertext = if embeds_iv { iv.to_vec() } else { vec![] };
ciphertext.extend(blocks.iter().flatten());
Ok(ciphertext)
}
/// Wraps the AES256-CBC decryption to match what we need in CTAP.
pub fn aes256_cbc_decrypt(
aes_enc_key: &crypto::aes256::EncryptionKey,
ciphertext: &[u8],
embeds_iv: bool,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if ciphertext.len() % 16 != 0 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let mut block_len = ciphertext.len() / 16;
// TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable.
let mut block_iter = ciphertext.chunks_exact(16);
let iv = if embeds_iv {
block_len -= 1;
let iv_block = block_iter
.next()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
*array_ref!(iv_block, 0, 16)
} else {
[0u8; 16]
};
let mut blocks = Vec::with_capacity(block_len);
for block in block_iter {
blocks.push(*array_ref!(block, 0, 16));
}
let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key);
cbc_decrypt(&aes_dec_key, iv, &mut blocks);
Ok(blocks.iter().flatten().cloned().collect::<Vec<u8>>())
}
#[cfg(test)]
mod test {
use super::*;
use crypto::rng256::ThreadRng256;
#[test]
fn test_encrypt_decrypt_with_iv() {
let mut rng = ThreadRng256 {};
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap();
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_decrypt_without_iv() {
let mut rng = ThreadRng256 {};
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, false).unwrap();
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_correct_iv_usage() {
let mut rng = ThreadRng256 {};
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let mut ciphertext_no_iv =
aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, false).unwrap();
let mut ciphertext_with_iv = vec![0u8; 16];
ciphertext_with_iv.append(&mut ciphertext_no_iv);
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_iv_manipulation_property() {
let mut rng = ThreadRng256 {};
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let mut ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap();
let mut expected_plaintext = plaintext;
for i in 0..16 {
ciphertext[i] ^= 0xBB;
expected_plaintext[i] ^= 0xBB;
}
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
assert_eq!(decrypted, expected_plaintext);
}
#[test]
fn test_chaining() {
let mut rng = ThreadRng256 {};
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext1 = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap();
let ciphertext2 = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap();
assert_eq!(ciphertext1.len(), 80);
assert_eq!(ciphertext2.len(), 80);
// The ciphertext should mutate in all blocks with a different IV.
let block_iter1 = ciphertext1.chunks_exact(16);
let block_iter2 = ciphertext2.chunks_exact(16);
for (block1, block2) in block_iter1.zip(block_iter2) {
assert_ne!(block1, block2);
}
}
}

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");
// you may not use this file except in compliance with the License.
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::apdu::{ApduStatusCode, APDU};
use super::hid::ChannelID;
use super::key_material::{ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY};
use super::status_code::Ctap2StatusCode;
use super::CtapState;
use alloc::vec::Vec;
@@ -23,47 +23,13 @@ use core::convert::TryFrom;
use crypto::rng256::Rng256;
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:
// 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_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),
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))]
#[derive(PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub enum Ctap1Flags {
CheckOnly = 0x07,
EnforceUpAndSign = 0x03,
@@ -89,7 +55,7 @@ impl Into<u8> for Ctap1Flags {
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
#[derive(Debug, PartialEq)]
// TODO: remove #allow when https://github.com/rust-lang/rust/issues/64362 is fixed
enum U2fCommand {
#[allow(dead_code)]
@@ -115,11 +81,14 @@ impl TryFrom<&[u8]> for U2fCommand {
type Error = Ctap1StatusCode;
fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> {
if message.len() < Ctap1Command::APDU_HEADER_LEN as usize {
return Err(Ctap1StatusCode::SW_WRONG_DATA);
}
let apdu: APDU = match APDU::try_from(message) {
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
// encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected).
@@ -128,19 +97,17 @@ impl TryFrom<&[u8]> for U2fCommand {
// +-----+-----+----+----+-----+-----+-----+
// | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 |
// +-----+-----+----+----+-----+-----+-----+
if apdu[0] != Ctap1Command::CTAP1_CLA {
return Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED);
if apdu.header.cla != Ctap1Command::CTAP1_CLA {
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
// 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);
}
match apdu[1] {
match apdu.header.ins {
// U2F raw message format specification, Section 4.1
// +-----------------+-------------------+
// + Challenge (32B) | Application (32B) |
@@ -150,8 +117,8 @@ impl TryFrom<&[u8]> for U2fCommand {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
Ok(Self::Register {
challenge: *array_ref!(payload, 0, 32),
application: *array_ref!(payload, 32, 32),
challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(apdu.data, 32, 32),
})
}
@@ -163,15 +130,15 @@ impl TryFrom<&[u8]> for U2fCommand {
if lc < 65 {
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 {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
let flag = Ctap1Flags::try_from(apdu[2])?;
let flag = Ctap1Flags::try_from(apdu.header.p1)?;
Ok(Self::Authenticate {
challenge: *array_ref!(payload, 0, 32),
application: *array_ref!(payload, 32, 32),
key_handle: payload[65..lc].to_vec(),
challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(apdu.data, 32, 32),
key_handle: apdu.data[65..].to_vec(),
flags: flag,
})
}
@@ -187,11 +154,11 @@ impl TryFrom<&[u8]> for U2fCommand {
// For Vendor specific command.
Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => {
Ok(Self::VendorSpecific {
payload: payload.to_vec(),
payload: apdu.data.to_vec(),
})
}
_ => Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED),
_ => Err(Ctap1StatusCode::SW_INS_INVALID),
}
}
}
@@ -199,8 +166,6 @@ impl TryFrom<&[u8]> for U2fCommand {
pub struct Ctap1Command {}
impl Ctap1Command {
const APDU_HEADER_LEN: u32 = 7; // CLA + INS + P1 + P2 + LC1-3
const CTAP1_CLA: u8 = 0;
// This byte is used in Register, but only serves backwards compatibility.
const LEGACY_BYTE: u8 = 0x05;
@@ -224,6 +189,12 @@ impl Ctap1Command {
R: Rng256,
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
{
if !ctap_state
.allows_ctap1()
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
{
return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED);
}
let command = U2fCommand::try_from(message)?;
match command {
U2fCommand::Register {
@@ -231,7 +202,7 @@ impl Ctap1Command {
application,
} => {
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)
}
@@ -246,7 +217,7 @@ impl Ctap1Command {
if flags == Ctap1Flags::EnforceUpAndSign
&& !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(
challenge,
@@ -261,7 +232,7 @@ impl Ctap1Command {
U2fCommand::Version => Ok(Vec::<u8>::from(super::U2F_VERSION_STRING)),
// 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),
}
}
@@ -289,20 +260,30 @@ impl Ctap1Command {
let pk = sk.genpk();
let key_handle = ctap_state
.encrypt_key_handle(sk, &application)
.map_err(|_| Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG)?;
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
if key_handle.len() > 0xFF {
// This is just being defensive with unreachable code.
return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG);
return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION);
}
let mut response =
Vec::with_capacity(105 + key_handle.len() + ATTESTATION_CERTIFICATE.len());
let certificate = ctap_state
.persistent_store
.attestation_certificate()
.map_err(|_| Ctap1StatusCode::SW_MEMERR)?
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let private_key = ctap_state
.persistent_store
.attestation_private_key()
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len());
response.push(Ctap1Command::LEGACY_BYTE);
let user_pk = pk.to_uncompressed();
response.extend_from_slice(&user_pk);
response.push(key_handle.len() as u8);
response.extend(key_handle.clone());
response.extend_from_slice(&ATTESTATION_CERTIFICATE);
response.extend_from_slice(&certificate);
// The first byte is reserved.
let mut signature_data = Vec::with_capacity(66 + key_handle.len());
@@ -312,7 +293,7 @@ impl Ctap1Command {
signature_data.extend(key_handle);
signature_data.extend_from_slice(&user_pk);
let attestation_key = crypto::ecdsa::SecKey::from_bytes(ATTESTATION_PRIVATE_KEY).unwrap();
let attestation_key = crypto::ecdsa::SecKey::from_bytes(&private_key).unwrap();
let signature = attestation_key.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
response.extend(signature.to_asn1_der());
@@ -349,7 +330,7 @@ impl Ctap1Command {
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
if let Some(credential_source) = credential_source {
if flags == Ctap1Flags::CheckOnly {
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED);
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
ctap_state
.increment_global_signature_counter()
@@ -373,7 +354,7 @@ impl Ctap1Command {
#[cfg(test)]
mod test {
use super::super::{ENCRYPTED_CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER};
use super::super::{key_material, CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER};
use super::*;
use crypto::rng256::ThreadRng256;
use crypto::Hash256;
@@ -413,42 +394,75 @@ mod test {
0x00,
0x00,
0x00,
65 + ENCRYPTED_CREDENTIAL_ID_SIZE as u8,
65 + CREDENTIAL_ID_SIZE as u8,
];
let challenge = [0x0C; 32];
message.extend(&challenge);
message.extend(application);
message.push(ENCRYPTED_CREDENTIAL_ID_SIZE as u8);
message.push(CREDENTIAL_ID_SIZE as u8);
message.extend(key_handle);
message
}
#[test]
fn test_process_allowed() {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
ctap_state.persistent_store.toggle_always_uv().unwrap();
let application = [0x0A; 32];
let message = create_register_message(&application);
ctap_state.u2f_up_state.consume_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);
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED));
}
#[test]
fn test_process_register() {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let application = [0x0A; 32];
let message = create_register_message(&application);
ctap_state.u2f_up_state.consume_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);
// Certificate and private key are missing
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH];
assert!(ctap_state
.persistent_store
.set_attestation_private_key(&fake_key)
.is_ok());
ctap_state.u2f_up_state.consume_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);
// Certificate is still missing
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
let fake_cert = [0x99u8; 100]; // Arbitrary length
assert!(ctap_state
.persistent_store
.set_attestation_certificate(&fake_cert[..])
.is_ok());
ctap_state.u2f_up_state.consume_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).unwrap();
assert_eq!(response[0], Ctap1Command::LEGACY_BYTE);
assert_eq!(response[66], ENCRYPTED_CREDENTIAL_ID_SIZE as u8);
assert_eq!(response[66], CREDENTIAL_ID_SIZE as u8);
assert!(ctap_state
.decrypt_credential_source(
response[67..67 + ENCRYPTED_CREDENTIAL_ID_SIZE].to_vec(),
&application
)
.decrypt_credential_source(response[67..67 + CREDENTIAL_ID_SIZE].to_vec(), &application)
.unwrap()
.is_some());
const CERT_START: usize = 67 + ENCRYPTED_CREDENTIAL_ID_SIZE;
const CERT_START: usize = 67 + CREDENTIAL_ID_SIZE;
assert_eq!(
&response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()],
&ATTESTATION_CERTIFICATE[..]
&response[CERT_START..CERT_START + fake_cert.len()],
&fake_cert[..]
);
}
@@ -456,7 +470,7 @@ mod test {
fn test_process_register_bad_message() {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let application = [0x0A; 32];
let message = create_register_message(&application);
@@ -476,13 +490,13 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
let response =
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]
@@ -490,7 +504,7 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -498,7 +512,7 @@ mod test {
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
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]
@@ -506,7 +520,7 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -523,20 +537,29 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
let mut message = create_authenticate_message(
&application,
Ctap1Flags::DontEnforceUpAndSign,
&key_handle,
);
message.push(0x00);
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);
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);
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
@@ -547,7 +570,7 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -557,7 +580,7 @@ mod test {
message[0] = 0xEE;
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]
@@ -565,7 +588,7 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -575,7 +598,7 @@ mod test {
message[1] = 0xEE;
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]
@@ -583,7 +606,7 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -596,12 +619,20 @@ mod test {
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
fn check_signature_counter(response: &[u8; 4], signature_counter: u32) {
if USE_SIGNATURE_COUNTER {
assert_eq!(u32::from_be_bytes(*response), signature_counter);
} else {
assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]);
}
}
#[test]
fn test_process_authenticate_enforce() {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -614,11 +645,13 @@ mod test {
let response =
Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap();
assert_eq!(response[0], 0x01);
if USE_SIGNATURE_COUNTER {
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x01]);
} else {
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x00]);
}
check_signature_counter(
array_ref!(response, 1, 4),
ctap_state
.persistent_store
.global_signature_counter()
.unwrap(),
);
}
#[test]
@@ -626,7 +659,7 @@ mod test {
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
@@ -640,23 +673,25 @@ mod test {
let response =
Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE).unwrap();
assert_eq!(response[0], 0x01);
if USE_SIGNATURE_COUNTER {
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x01]);
} else {
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x00]);
}
check_signature_counter(
array_ref!(response, 1, 4),
ctap_state
.persistent_store
.global_signature_counter()
.unwrap(),
);
}
#[test]
fn test_process_authenticate_bad_key_handle() {
let application = [0x0A; 32];
let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE];
let key_handle = vec![0x00; CREDENTIAL_ID_SIZE];
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
@@ -667,18 +702,18 @@ mod test {
#[test]
fn test_process_authenticate_without_up() {
let application = [0x0A; 32];
let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE];
let key_handle = vec![0x00; CREDENTIAL_ID_SIZE];
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
let mut rng = ThreadRng256 {};
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
let response =
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));
}
}

280
src/ctap/customization.rs Normal file
View File

@@ -0,0 +1,280 @@
// Copyright 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.
//! This file contains all customizable constants.
//!
//! If you adapt them, make sure to run the tests before flashing the firmware.
//! Our deploy script enforces the invariants.
use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode};
// ###########################################################################
// Constants for adjusting privacy and protection levels.
// ###########################################################################
/// Changes the default level for the credProtect extension.
///
/// You can change this value to one of the following for more privacy:
/// - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList
/// - CredentialProtectionPolicy::UserVerificationRequired
///
/// UserVerificationOptionalWithCredentialIdList
/// Resident credentials are discoverable with
/// - an allowList,
/// - an excludeList,
/// - user verification.
///
/// UserVerificationRequired
/// Resident credentials are discoverable with user verification only.
///
/// This can improve privacy, but can make usage less comfortable.
pub const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None;
/// Sets the initial minimum PIN length in code points.
///
/// # Invariant
///
/// - The minimum PIN length must be at least 4.
/// - The minimum PIN length must be at most 63.
/// - DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty if MAX_RP_IDS_LENGTH is 0.
///
/// Requiring longer PINs can help establish trust between users and relying
/// parties. It makes user verification harder to break, but less convenient.
/// NIST recommends at least 6-digit PINs in section 5.1.9.1:
/// https://pages.nist.gov/800-63-3/sp800-63b.html
///
/// Reset reverts the minimum PIN length to this DEFAULT_MIN_PIN_LENGTH.
pub const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
/// Lists relying parties that can read the minimum PIN length.
///
/// # Invariant
///
/// - DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty if MAX_RP_IDS_LENGTH is 0
///
/// Only the RP IDs listed in DEFAULT_MIN_PIN_LENGTH_RP_IDS are allowed to read
/// the minimum PIN length with the minPinLength extension.
pub const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[];
/// Enforces the alwaysUv option.
///
/// When setting to true, commands require a PIN.
/// Also, alwaysUv can not be disabled by commands.
///
/// A certification (additional to FIDO Alliance's) might require enforcing
/// alwaysUv. Otherwise, users should have the choice to configure alwaysUv.
/// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here.
pub const ENFORCE_ALWAYS_UV: bool = false;
/// Allows usage of enterprise attestation.
///
/// # Invariant
///
/// - Enterprise and batch attestation can not both be active.
/// - If the mode is VendorFacilitated, ENTERPRISE_RP_ID_LIST must be non-empty.
///
/// For privacy reasons, it is disabled by default. You can choose between:
/// - EnterpriseAttestationMode::VendorFacilitated
/// - EnterpriseAttestationMode::PlatformManaged
///
/// VendorFacilitated
/// Enterprise attestation is restricted to ENTERPRISE_RP_ID_LIST. Add your
/// enterprises domain, e.g. "example.com", to the list below.
///
/// PlatformManaged
/// All relying parties can request an enterprise attestation. The authenticator
/// trusts the platform to filter requests.
///
/// To enable the feature, send the subcommand enableEnterpriseAttestation in
/// AuthenticatorConfig. An enterprise might want to customize the type of
/// attestation that is used. OpenSK defaults to batch attestation. Configuring
/// individual certificates then makes authenticators identifiable.
///
/// OpenSK prevents activating batch and enterprise attestation together. The
/// current implementation uses the same key material at the moment, and these
/// two modes have conflicting privacy guarantees.
/// If you implement your own enterprise attestation mechanism, and you want
/// batch attestation at the same time, proceed carefully and remove the
/// assertion.
pub const ENTERPRISE_ATTESTATION_MODE: Option<EnterpriseAttestationMode> = None;
/// Lists relying party IDs that can perform enterprise attestation.
///
/// # Invariant
///
/// - If the mode is VendorFacilitated, ENTERPRISE_RP_ID_LIST must be non-empty.
///
/// This list is only considered if the enterprise attestation mode is
/// VendorFacilitated.
pub const ENTERPRISE_RP_ID_LIST: &[&str] = &[];
/// Maximum message size send for CTAP commands.
///
/// The maximum value is 7609, as HID packets can not encode longer messages.
/// 1024 is the default mentioned in the authenticatorLargeBlobs commands.
/// Larger values are preferred, as that allows more parameters in commands.
/// If long commands are too unreliable on your hardware, consider decreasing
/// this value.
pub const MAX_MSG_SIZE: usize = 7609;
/// Sets the number of consecutive failed PINs before blocking interaction.
///
/// # Invariant
///
/// - CTAP2.0: Maximum PIN retries must be 8.
/// - CTAP2.1: Maximum PIN retries must be 8 at most.
///
/// The fail retry counter is reset after entering the correct PIN.
pub const MAX_PIN_RETRIES: u8 = 8;
/// Enables or disables basic attestation for FIDO2.
///
/// # Invariant
///
/// - Enterprise and batch attestation can not both be active (see above).
///
/// The basic attestation uses the signing key configured with a vendor command
/// as a batch key. If you turn batch attestation on, be aware that it is your
/// responsibility to safely generate and store the key material. Also, the
/// batches must have size of at least 100k authenticators before using new key
/// material.
/// U2F is unaffected by this setting.
///
/// https://www.w3.org/TR/webauthn/#attestation
pub const USE_BATCH_ATTESTATION: bool = false;
/// Enables or disables signature counters.
///
/// The signature counter is currently implemented as a global counter.
/// The specification strongly suggests to have per-credential counters.
/// Implementing those means you can't have an infinite amount of server-side
/// credentials anymore. Also, since counters need frequent writes on the
/// persistent storage, we might need a flash friendly implementation. This
/// solution is a compromise to be compatible with U2F and not wasting storage.
///
/// https://www.w3.org/TR/webauthn/#signature-counter
pub const USE_SIGNATURE_COUNTER: bool = true;
// ###########################################################################
// Constants for performance optimization or adapting to different hardware.
//
// Those constants may be modified before compilation to tune the behavior of
// the key.
// ###########################################################################
/// Sets the maximum blob size stored with the credBlob extension.
///
/// # Invariant
///
/// - The length must be at least 32.
pub const MAX_CRED_BLOB_LENGTH: usize = 32;
/// Limits the number of considered entries in credential lists.
///
/// # Invariant
///
/// - This value, if present, must be at least 1 (more is preferred).
///
/// Depending on your memory, you can use Some(n) to limit request sizes in
/// MakeCredential and GetAssertion. This affects allowList and excludeList.
pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option<usize> = None;
/// Limits the size of largeBlobs the authenticator stores.
///
/// # Invariant
///
/// - The allowed size must be at least 1024.
/// - The array must fit into the shards reserved in storage/key.rs.
pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048;
/// Limits the number of RP IDs that can change the minimum PIN length.
///
/// # Invariant
///
/// - If this value is 0, DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty.
///
/// You can use this constant to have an upper limit in storage requirements.
/// This might be useful if you want to more reliably predict the remaining
/// storage. Stored string can still be of arbitrary length though, until RP ID
/// truncation is implemented.
/// Outside of memory considerations, you can set this value to 0 if only RP IDs
/// in DEFAULT_MIN_PIN_LENGTH_RP_IDS should be allowed to change the minimum PIN
/// length.
pub const MAX_RP_IDS_LENGTH: usize = 8;
/// Sets the number of resident keys you can store.
///
/// # Invariant
///
/// - The storage key CREDENTIALS must fit at least this number of credentials.
///
/// This value has implications on the flash lifetime, please see the
/// documentation for NUM_PAGES below.
pub const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150;
/// Sets the number of pages used for persistent storage.
///
/// The number of pages should be at least 3 and at most what the flash can
/// hold. There should be no reason to put a small number here, except that the
/// latency of flash operations is linear in the 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.
///
/// Limiting the number of resident keys permits to ensure a minimum number of
/// counter increments.
/// Let:
/// - P the number of pages (NUM_PAGES)
/// - K the maximum number of resident keys (MAX_SUPPORTED_RESIDENT_KEYS)
/// - S the maximum size of a resident key (about 500)
/// - C the number of erase cycles (10000)
/// - I the minimum number of counter increments
///
/// We have: I = (P * 4084 - 5107 - K * S) / 8 * C
///
/// With P=20 and K=150, we have I=2M which is enough for 500 increments per day
/// for 10 years.
pub const NUM_PAGES: usize = 20;
#[cfg(test)]
mod test {
use super::*;
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_invariants() {
// Two invariants are currently tested in different files:
// - storage.rs: if MAX_LARGE_BLOB_ARRAY_SIZE fits the shards
// - storage/key.rs: if MAX_SUPPORTED_RESIDENT_KEYS fits CREDENTIALS
assert!(DEFAULT_MIN_PIN_LENGTH >= 4);
assert!(DEFAULT_MIN_PIN_LENGTH <= 63);
assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none());
if let Some(EnterpriseAttestationMode::VendorFacilitated) = ENTERPRISE_ATTESTATION_MODE {
assert!(!ENTERPRISE_RP_ID_LIST.is_empty());
} else {
assert!(ENTERPRISE_RP_ID_LIST.is_empty());
}
assert!(MAX_MSG_SIZE >= 1024);
assert!(MAX_MSG_SIZE <= 7609);
assert!(MAX_PIN_RETRIES <= 8);
assert!(MAX_CRED_BLOB_LENGTH >= 32);
if let Some(count) = MAX_CREDENTIAL_COUNT_IN_LIST {
assert!(count >= 1);
}
assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024);
if MAX_RP_IDS_LENGTH == 0 {
assert!(!DEFAULT_MIN_PIN_LENGTH_RP_IDS.is_empty());
}
}
}

File diff suppressed because it is too large Load Diff

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");
// you may not use this file except in compliance with the License.
@@ -68,8 +68,8 @@ pub struct CtapHid {
// vendor specific.
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
// allocated.
// In packets, the ids are then encoded with the native endianness (with the
// u32::to/from_ne_bytes methods).
// In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the
// u32::to/from_be_bytes methods).
allocated_cids: usize,
pub wink_permission: TimedPermission,
}
@@ -117,9 +117,8 @@ impl CtapHid {
// CTAP specification (version 20190130) section 8.1.9.1.3
const PROTOCOL_VERSION: u8 = 2;
// The device version number is vendor-defined. For now we define them to be zero.
// TODO: Update with device version?
const DEVICE_VERSION_MAJOR: u8 = 0;
// The device version number is vendor-defined.
const DEVICE_VERSION_MAJOR: u8 = 1;
const DEVICE_VERSION_MINOR: u8 = 0;
const DEVICE_VERSION_BUILD: u8 = 0;
@@ -178,7 +177,7 @@ impl CtapHid {
match message.cmd {
// CTAP specification (version 20190130) section 8.1.9.1.1
CtapHid::COMMAND_MSG => {
// If we don't have CTAP1 backward compatibilty, this command in invalid.
// If we don't have CTAP1 backward compatibilty, this command is invalid.
#[cfg(not(feature = "with_ctap1"))]
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD);
@@ -200,7 +199,8 @@ impl CtapHid {
// Each transaction is atomic, so we process the command directly here and
// don't handle any other packet in the meantime.
// TODO: Send keep-alive packets in the meantime.
let response = ctap_state.process_command(&message.payload, cid);
let response =
ctap_state.process_command(&message.payload, cid, clock_value);
if let Some(iterator) = CtapHid::split_message(Message {
cid,
cmd: CtapHid::COMMAND_CBOR,
@@ -219,7 +219,7 @@ impl CtapHid {
cid,
cmd: CtapHid::COMMAND_CBOR,
payload: vec![
Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG as u8,
Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8,
],
})
.unwrap()
@@ -234,7 +234,7 @@ impl CtapHid {
let new_cid = if cid == CtapHid::CHANNEL_BROADCAST {
// TODO: Prevent allocating 2^32 channels.
self.allocated_cids += 1;
(self.allocated_cids as u32).to_ne_bytes()
(self.allocated_cids as u32).to_be_bytes()
} else {
// Sync the channel and discard the current transaction.
cid
@@ -322,6 +322,9 @@ impl CtapHid {
receive::Error::UnexpectedSeq => {
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
}
receive::Error::UnexpectedLen => {
CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN)
}
receive::Error::Timeout => {
CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT)
}
@@ -341,7 +344,7 @@ impl CtapHid {
}
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 {
@@ -416,7 +419,7 @@ impl CtapHid {
#[cfg(feature = "with_ctap1")]
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator {
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());
CtapHid::split_message(Message {
cid,
@@ -520,7 +523,7 @@ mod test {
fn test_spurious_continuation_packet() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let mut ctap_hid = CtapHid::new();
let mut packet = [0x00; 64];
@@ -541,7 +544,7 @@ mod test {
fn test_command_init() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let mut ctap_hid = CtapHid::new();
let reply = process_messages(
@@ -568,12 +571,12 @@ mod test {
0xBC,
0xDE,
0xF0,
0x01, // Allocated CID
0x00,
0x00, // Allocated CID
0x00,
0x00,
0x01,
0x02, // Protocol version
0x00, // Device version
0x01, // Device version
0x00,
0x00,
CtapHid::CAPABILITIES
@@ -586,7 +589,7 @@ mod test {
fn test_command_init_for_sync() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let mut ctap_hid = CtapHid::new();
let cid = cid_from_init(&mut ctap_hid, &mut ctap_state);
@@ -633,7 +636,7 @@ mod test {
cid[2],
cid[3],
0x02, // Protocol version
0x00, // Device version
0x01, // Device version
0x00,
0x00,
CtapHid::CAPABILITIES
@@ -646,7 +649,7 @@ mod test {
fn test_command_ping() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let mut ctap_hid = CtapHid::new();
let cid = cid_from_init(&mut ctap_hid, &mut ctap_state);

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");
// 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
// limitations under the License.
use super::super::customization::MAX_MSG_SIZE;
use super::{ChannelID, CtapHid, HidPacket, Message, ProcessedPacket};
use alloc::vec::Vec;
use core::mem::swap;
@@ -45,6 +46,8 @@ pub enum Error {
UnexpectedContinuation,
// Expected a continuation packet with a specific sequence number, got another sequence number.
UnexpectedSeq,
// The length of a message is too big.
UnexpectedLen,
// This packet arrived after a timeout.
Timeout,
}
@@ -107,7 +110,7 @@ impl MessageAssembler {
// Expecting an initialization packet.
match processed_packet {
ProcessedPacket::InitPacket { cmd, len, data } => {
Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp))
self.parse_init_packet(*cid, cmd, len, data, timestamp)
}
ProcessedPacket::ContinuationPacket { .. } => {
// CTAP specification (version 20190130) section 8.1.5.4
@@ -129,7 +132,7 @@ impl MessageAssembler {
ProcessedPacket::InitPacket { cmd, len, data } => {
self.reset();
if cmd == CtapHid::COMMAND_INIT {
Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp))
self.parse_init_packet(*cid, cmd, len, data, timestamp)
} else {
Err((*cid, Error::UnexpectedInit))
}
@@ -151,24 +154,25 @@ impl MessageAssembler {
}
}
fn accept_init_packet(
fn parse_init_packet(
&mut self,
cid: ChannelID,
cmd: u8,
len: usize,
data: &[u8],
timestamp: Timestamp<isize>,
) -> Option<Message> {
// TODO: Should invalid commands/payload lengths be rejected early, i.e. as soon as the
// initialization packet is received, or should we build a message and then catch the
// error?
// The specification (version 20190130) isn't clear on this point.
) -> Result<Option<Message>, (ChannelID, Error)> {
// Reject invalid lengths early to reduce the risk of running out of memory.
// TODO: also reject invalid commands early?
if len > MAX_MSG_SIZE {
return Err((cid, Error::UnexpectedLen));
}
self.cid = cid;
self.last_timestamp = timestamp;
self.cmd = cmd;
self.seq = 0;
self.remaining_payload_len = len;
self.append_payload(data)
Ok(self.append_payload(data))
}
fn append_payload(&mut self, data: &[u8]) -> Option<Message> {

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");
// 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");
// you may not use this file except in compliance with the License.
@@ -12,10 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub const AAGUID: &[u8; 16] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin"));
pub const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32;
pub const AAGUID_LENGTH: usize = 16;
pub const ATTESTATION_CERTIFICATE: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin"));
pub const ATTESTATION_PRIVATE_KEY: &[u8; 32] =
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_pkey.bin"));
pub const AAGUID: &[u8; AAGUID_LENGTH] =
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin"));

411
src/ctap/large_blobs.rs Normal file
View File

@@ -0,0 +1,411 @@
// 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::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorLargeBlobsParameters;
use super::customization::MAX_MSG_SIZE;
use super::response::{AuthenticatorLargeBlobsResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use alloc::vec;
use alloc::vec::Vec;
use byteorder::{ByteOrder, LittleEndian};
use crypto::sha256::Sha256;
use crypto::Hash256;
/// The length of the truncated hash that as appended to the large blob data.
const TRUNCATED_HASH_LEN: usize = 16;
pub struct LargeBlobs {
buffer: Vec<u8>,
expected_length: usize,
expected_next_offset: usize,
}
/// Implements the logic for the AuthenticatorLargeBlobs command and keeps its state.
impl LargeBlobs {
pub fn new() -> LargeBlobs {
LargeBlobs {
buffer: Vec::new(),
expected_length: 0,
expected_next_offset: 0,
}
}
/// Process the large blob command.
pub fn process_command(
&mut self,
persistent_store: &mut PersistentStore,
client_pin: &mut ClientPin,
large_blobs_params: AuthenticatorLargeBlobsParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorLargeBlobsParameters {
get,
set,
offset,
length,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = large_blobs_params;
const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64;
if let Some(get) = get {
if get > MAX_FRAGMENT_LENGTH {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
let config = persistent_store.get_large_blob_array(offset, get)?;
return Ok(ResponseData::AuthenticatorLargeBlobs(Some(
AuthenticatorLargeBlobsResponse { config },
)));
}
if let Some(mut set) = set {
if set.len() > MAX_FRAGMENT_LENGTH {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
if offset == 0 {
// Checks for offset and length are already done in command.
self.expected_length =
length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
self.expected_next_offset = 0;
}
if offset != self.expected_next_offset {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ);
}
if persistent_store.pin_hash()?.is_some() || persistent_store.has_always_uv()? {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut large_blob_data = vec![0xFF; 32];
large_blob_data.extend(&[0x0C, 0x00]);
let mut offset_bytes = [0u8; 4];
LittleEndian::write_u32(&mut offset_bytes, offset as u32);
large_blob_data.extend(&offset_bytes);
large_blob_data.extend(&Sha256::hash(set.as_slice()));
client_pin.verify_pin_uv_auth_token(
&large_blob_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
client_pin.has_permission(PinPermission::LargeBlobWrite)?;
}
if offset + set.len() > self.expected_length {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if offset == 0 {
self.buffer = Vec::with_capacity(self.expected_length);
}
self.buffer.append(&mut set);
self.expected_next_offset = self.buffer.len();
if self.expected_next_offset == self.expected_length {
self.expected_length = 0;
self.expected_next_offset = 0;
// Must be a positive number.
let buffer_hash_index = self.buffer.len() - TRUNCATED_HASH_LEN;
if Sha256::hash(&self.buffer[..buffer_hash_index])[..TRUNCATED_HASH_LEN]
!= self.buffer[buffer_hash_index..]
{
self.buffer = Vec::new();
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
}
persistent_store.commit_large_blob_array(&self.buffer)?;
self.buffer = Vec::new();
}
return Ok(ResponseData::AuthenticatorLargeBlobs(None));
}
// This should be unreachable, since the command has either get or set.
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::PinUvAuthProtocol;
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::*;
use crypto::rng256::ThreadRng256;
#[test]
fn test_process_command_get_empty() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut large_blobs = LargeBlobs::new();
let large_blob = vec![
0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5,
0x7A, 0x6D, 0x3C,
];
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(large_blob.len()),
set: None,
offset: 0,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
match large_blobs_response.unwrap() {
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
assert_eq!(response.config, large_blob);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_command_commit_and_get() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
offset: BLOB_LEN / 2,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(BLOB_LEN),
set: None,
offset: 0,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
match large_blobs_response.unwrap() {
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
assert_eq!(response.config, large_blob);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_command_commit_unexpected_offset() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
// The offset is 1 too big.
offset: BLOB_LEN / 2 + 1,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ),
);
}
#[test]
fn test_process_command_commit_unexpected_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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
// The length is 1 too small.
length: Some(BLOB_LEN - 1),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
offset: BLOB_LEN / 2,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
);
}
#[test]
fn test_process_command_commit_unexpected_hash() {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 20;
// This blob does not have an appropriate hash.
let large_blob = vec![0x1B; BLOB_LEN];
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob.to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE),
);
}
fn test_helper_process_command_commit_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
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 client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 20;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let mut large_blob_data = vec![0xFF; 32];
// Command constant and offset bytes.
large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]);
large_blob_data.extend(&Sha256::hash(&large_blob));
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
&pin_uv_auth_token,
&large_blob_data,
pin_uv_auth_protocol,
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: Some(pin_uv_auth_param),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let large_blobs_response =
large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
}
#[test]
fn test_process_command_commit_with_pin_v1() {
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_command_commit_with_pin_v2() {
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V2);
}
}

File diff suppressed because it is too large Load Diff

408
src/ctap/pin_protocol.rs Normal file
View File

@@ -0,0 +1,408 @@
// Copyright 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 crate::ctap::client_pin::PIN_TOKEN_LENGTH;
use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt};
use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol};
use crate::ctap::status_code::Ctap2StatusCode;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::convert::TryInto;
use crypto::hkdf::hkdf_empty_salt_256;
#[cfg(test)]
use crypto::hmac::hmac_256;
use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits};
use crypto::rng256::Rng256;
use crypto::sha256::Sha256;
use crypto::Hash256;
/// Implements common functions between existing PIN protocols for handshakes.
pub struct PinProtocol {
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
}
impl PinProtocol {
/// This process is run by the authenticator at power-on.
///
/// This function implements "initialize" from the specification.
pub fn new(rng: &mut impl Rng256) -> PinProtocol {
let key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
let pin_uv_auth_token = rng.gen_uniform_u8x32();
PinProtocol {
key_agreement_key,
pin_uv_auth_token,
}
}
/// Generates a fresh public key.
pub fn regenerate(&mut self, rng: &mut impl Rng256) {
self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
}
/// Generates a fresh pinUvAuthToken.
pub fn reset_pin_uv_auth_token(&mut self, rng: &mut impl Rng256) {
self.pin_uv_auth_token = rng.gen_uniform_u8x32();
}
/// Returns the authenticators public key as a CoseKey structure.
pub fn get_public_key(&self) -> CoseKey {
CoseKey::from(self.key_agreement_key.genpk())
}
/// Processes the peer's encapsulated CoseKey and returns the shared secret.
pub fn decapsulate(
&self,
peer_cose_key: CoseKey,
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<Box<dyn SharedSecret>, Ctap2StatusCode> {
let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?;
let handshake = self.key_agreement_key.exchange_x(&pk);
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))),
PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))),
}
}
/// Getter for pinUvAuthToken.
pub fn get_pin_uv_auth_token(&self) -> &[u8; PIN_TOKEN_LENGTH] {
&self.pin_uv_auth_token
}
/// This is used for debugging to inject key material.
#[cfg(test)]
pub fn new_test(
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
) -> PinProtocol {
PinProtocol {
key_agreement_key,
pin_uv_auth_token,
}
}
}
/// Authenticates the pinUvAuthToken for the given PIN protocol.
#[cfg(test)]
pub fn authenticate_pin_uv_auth_token(
token: &[u8; PIN_TOKEN_LENGTH],
message: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Vec<u8> {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => hmac_256::<Sha256>(token, message)[..16].to_vec(),
PinUvAuthProtocol::V2 => hmac_256::<Sha256>(token, message).to_vec(),
}
}
/// Verifies the pinUvAuthToken for the given PIN protocol.
pub fn verify_pin_uv_auth_token(
token: &[u8; PIN_TOKEN_LENGTH],
message: &[u8],
signature: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<(), Ctap2StatusCode> {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => verify_v1(token, message, signature),
PinUvAuthProtocol::V2 => verify_v2(token, message, signature),
}
}
pub trait SharedSecret {
/// Returns the encrypted plaintext.
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
/// Returns the decrypted ciphertext.
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
/// Verifies that the signature is a valid MAC for the given message.
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>;
/// Creates a signature that matches verify.
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8>;
}
fn verify_v1(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
if signature.len() != 16 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if verify_hmac_256_first_128bits::<Sha256>(key, message, array_ref![signature, 0, 16]) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
fn verify_v2(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
if signature.len() != 32 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if verify_hmac_256::<Sha256>(key, message, array_ref![signature, 0, 32]) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
pub struct SharedSecretV1 {
common_secret: [u8; 32],
aes_enc_key: crypto::aes256::EncryptionKey,
}
impl SharedSecretV1 {
/// Creates a new shared secret from the handshake result.
fn new(handshake: [u8; 32]) -> SharedSecretV1 {
let common_secret = Sha256::hash(&handshake);
let aes_enc_key = crypto::aes256::EncryptionKey::new(&common_secret);
SharedSecretV1 {
common_secret,
aes_enc_key,
}
}
}
impl SharedSecret for SharedSecretV1 {
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, false)
}
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
verify_v1(&self.common_secret, message, signature)
}
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
hmac_256::<Sha256>(&self.common_secret, message)[..16].to_vec()
}
}
pub struct SharedSecretV2 {
aes_enc_key: crypto::aes256::EncryptionKey,
hmac_key: [u8; 32],
}
impl SharedSecretV2 {
/// Creates a new shared secret from the handshake result.
fn new(handshake: [u8; 32]) -> SharedSecretV2 {
let aes_key = hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 AES key");
SharedSecretV2 {
aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key),
hmac_key: hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 HMAC key"),
}
}
}
impl SharedSecret for SharedSecretV2 {
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, true)
}
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
verify_v2(&self.hmac_key, message, signature)
}
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
hmac_256::<Sha256>(&self.hmac_key, message).to_vec()
}
}
#[cfg(test)]
mod test {
use super::*;
use crypto::rng256::ThreadRng256;
#[test]
fn test_pin_protocol_public_key() {
let mut rng = ThreadRng256 {};
let mut pin_protocol = PinProtocol::new(&mut rng);
let public_key = pin_protocol.get_public_key();
pin_protocol.regenerate(&mut rng);
let new_public_key = pin_protocol.get_public_key();
assert_ne!(public_key, new_public_key);
}
#[test]
fn test_pin_protocol_pin_uv_auth_token() {
let mut rng = ThreadRng256 {};
let mut pin_protocol = PinProtocol::new(&mut rng);
let token = *pin_protocol.get_pin_uv_auth_token();
pin_protocol.reset_pin_uv_auth_token(&mut rng);
let new_token = pin_protocol.get_pin_uv_auth_token();
assert_ne!(&token, new_token);
}
#[test]
fn test_shared_secret_v1_encrypt_decrypt() {
let mut rng = ThreadRng256 {};
let shared_secret = SharedSecretV1::new([0x55; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret.encrypt(&mut rng, &plaintext).unwrap();
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
}
#[test]
fn test_shared_secret_v1_authenticate_verify() {
let shared_secret = SharedSecretV1::new([0x55; 32]);
let message = [0xAA; 32];
let signature = shared_secret.authenticate(&message);
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
}
#[test]
fn test_shared_secret_v1_verify() {
let shared_secret = SharedSecretV1::new([0x55; 32]);
let message = [0xAA];
let signature = [
0x8B, 0x60, 0x15, 0x7D, 0xF3, 0x44, 0x82, 0x2E, 0x54, 0x34, 0x7A, 0x01, 0xFB, 0x02,
0x48, 0xA6,
];
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
assert_eq!(
shared_secret.verify(&[0xBB], &signature),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
shared_secret.verify(&message, &[0x12; 16]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_shared_secret_v2_encrypt_decrypt() {
let mut rng = ThreadRng256 {};
let shared_secret = SharedSecretV2::new([0x55; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret.encrypt(&mut rng, &plaintext).unwrap();
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
}
#[test]
fn test_shared_secret_v2_authenticate_verify() {
let shared_secret = SharedSecretV2::new([0x55; 32]);
let message = [0xAA; 32];
let signature = shared_secret.authenticate(&message);
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
}
#[test]
fn test_shared_secret_v2_verify() {
let shared_secret = SharedSecretV2::new([0x55; 32]);
let message = [0xAA];
let signature = [
0xC0, 0x3F, 0x2A, 0x22, 0x5C, 0xC3, 0x4E, 0x05, 0xC1, 0x0E, 0x72, 0x9C, 0x8D, 0xD5,
0x7D, 0xE5, 0x98, 0x9C, 0x68, 0x15, 0xEC, 0xE2, 0x3A, 0x95, 0xD5, 0x90, 0xE1, 0xE9,
0x3F, 0xF0, 0x1A, 0xAF,
];
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
assert_eq!(
shared_secret.verify(&[0xBB], &signature),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
shared_secret.verify(&message, &[0x12; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_decapsulate_symmetric() {
let mut rng = ThreadRng256 {};
let pin_protocol1 = PinProtocol::new(&mut rng);
let pin_protocol2 = PinProtocol::new(&mut rng);
for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] {
let shared_secret1 = pin_protocol1
.decapsulate(pin_protocol2.get_public_key(), protocol)
.unwrap();
let shared_secret2 = pin_protocol2
.decapsulate(pin_protocol1.get_public_key(), protocol)
.unwrap();
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret1.encrypt(&mut rng, &plaintext).unwrap();
assert_eq!(plaintext, shared_secret2.decrypt(&ciphertext).unwrap());
}
}
#[test]
fn test_verify_pin_uv_auth_token_v1() {
let token = [0x91; PIN_TOKEN_LENGTH];
let message = [0xAA];
let signature = [
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
0x49, 0x68,
];
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V1),
Ok(())
);
assert_eq!(
verify_pin_uv_auth_token(
&[0x12; PIN_TOKEN_LENGTH],
&message,
&signature,
PinUvAuthProtocol::V1
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V1),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_verify_pin_uv_auth_token_v2() {
let token = [0x91; PIN_TOKEN_LENGTH];
let message = [0xAA];
let signature = [
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
0x49, 0x68, 0x94, 0x90, 0x20, 0x53, 0x0F, 0xA3, 0xD2, 0x7A, 0x9F, 0xFD, 0xFA, 0x62,
0x36, 0x93, 0xF7, 0x84,
];
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V2),
Ok(())
);
assert_eq!(
verify_pin_uv_auth_token(
&[0x12; PIN_TOKEN_LENGTH],
&message,
&signature,
PinUvAuthProtocol::V2
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V2),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

File diff suppressed because it is too large Load Diff

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");
// you may not use this file except in compliance with the License.
@@ -12,19 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(feature = "with_ctap2_1")]
use super::data_formats::{AuthenticatorTransport, PublicKeyCredentialParameter};
use super::data_formats::{
CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor,
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
};
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use cbor::{cbor_array_vec, cbor_bool, cbor_map_btree, cbor_map_options, cbor_text};
use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_collection, cbor_map_options, cbor_text};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
#[derive(Debug, PartialEq)]
pub enum ResponseData {
AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse),
AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse),
@@ -32,8 +29,12 @@ pub enum ResponseData {
AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
AuthenticatorReset,
#[cfg(feature = "with_ctap2_1")]
AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>),
AuthenticatorSelection,
AuthenticatorLargeBlobs(Option<AuthenticatorLargeBlobsResponse>),
// TODO(kaczmarczyck) dummy, extend
AuthenticatorConfig,
AuthenticatorVendor(AuthenticatorVendorResponse),
}
impl From<ResponseData> for Option<cbor::Value> {
@@ -43,21 +44,24 @@ impl From<ResponseData> for Option<cbor::Value> {
ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()),
ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()),
ResponseData::AuthenticatorGetInfo(data) => Some(data.into()),
ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()),
ResponseData::AuthenticatorClientPin(None) => None,
ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorReset => None,
#[cfg(feature = "with_ctap2_1")]
ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorConfig => None,
ResponseData::AuthenticatorVendor(data) => Some(data.into()),
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
#[derive(Debug, PartialEq)]
pub struct AuthenticatorMakeCredentialResponse {
pub fmt: String,
pub auth_data: Vec<u8>,
pub att_stmt: PackedAttestationStatement,
pub ep_att: Option<bool>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorMakeCredentialResponse> for cbor::Value {
@@ -66,24 +70,29 @@ impl From<AuthenticatorMakeCredentialResponse> for cbor::Value {
fmt,
auth_data,
att_stmt,
ep_att,
large_blob_key,
} = make_credential_response;
cbor_map_options! {
1 => fmt,
2 => auth_data,
3 => att_stmt,
0x01 => fmt,
0x02 => auth_data,
0x03 => att_stmt,
0x04 => ep_att,
0x05 => large_blob_key,
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
#[derive(Debug, PartialEq)]
pub struct AuthenticatorGetAssertionResponse {
pub credential: Option<PublicKeyCredentialDescriptor>,
pub auth_data: Vec<u8>,
pub signature: Vec<u8>,
pub user: Option<PublicKeyCredentialUserEntity>,
pub number_of_credentials: Option<u64>,
// 0x06: userSelected missing as we don't support displays.
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
@@ -94,45 +103,49 @@ impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
signature,
user,
number_of_credentials,
large_blob_key,
} = get_assertion_response;
cbor_map_options! {
1 => credential,
2 => auth_data,
3 => signature,
4 => user,
5 => number_of_credentials,
0x01 => credential,
0x02 => auth_data,
0x03 => signature,
0x04 => user,
0x05 => number_of_credentials,
0x07 => large_blob_key,
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
#[derive(Debug, PartialEq)]
pub struct AuthenticatorGetInfoResponse {
// TODO(kaczmarczyck) add maxAuthenticatorConfigLength and defaultCredProtect
pub versions: Vec<String>,
pub extensions: Option<Vec<String>>,
pub aaguid: [u8; 16],
pub options: Option<BTreeMap<String, bool>>,
pub options: Option<Vec<(String, bool)>>,
pub max_msg_size: Option<u64>,
pub pin_protocols: Option<Vec<u64>>,
#[cfg(feature = "with_ctap2_1")]
pub max_credential_count_in_list: Option<u64>,
#[cfg(feature = "with_ctap2_1")]
pub max_credential_id_length: Option<u64>,
#[cfg(feature = "with_ctap2_1")]
pub transports: Option<Vec<AuthenticatorTransport>>,
#[cfg(feature = "with_ctap2_1")]
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
pub default_cred_protect: Option<CredentialProtectionPolicy>,
#[cfg(feature = "with_ctap2_1")]
pub max_serialized_large_blob_array: Option<u64>,
pub force_pin_change: Option<bool>,
pub min_pin_length: u8,
#[cfg(feature = "with_ctap2_1")]
pub firmware_version: Option<u64>,
pub max_cred_blob_length: Option<u64>,
pub max_rp_ids_for_set_min_pin_length: Option<u64>,
// Missing response fields as they are only relevant for internal UV:
// - 0x11: preferredPlatformUvAttempts
// - 0x12: uvModality
// Add them when your hardware supports any kind of user verification within
// the boundary of the device, e.g. fingerprint or built-in keyboard.
pub certifications: Option<Vec<(String, i64)>>,
pub remaining_discoverable_credentials: Option<u64>,
// - 0x15: vendorPrototypeConfigCommands missing as we don't support it.
}
impl From<AuthenticatorGetInfoResponse> for cbor::Value {
#[cfg(feature = "with_ctap2_1")]
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
let AuthenticatorGetInfoResponse {
versions,
@@ -145,17 +158,30 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
max_credential_id_length,
transports,
algorithms,
default_cred_protect,
max_serialized_large_blob_array,
force_pin_change,
min_pin_length,
firmware_version,
max_cred_blob_length,
max_rp_ids_for_set_min_pin_length,
certifications,
remaining_discoverable_credentials,
} = get_info_response;
let options_cbor: Option<cbor::Value> = options.map(|options| {
let option_map: BTreeMap<_, _> = options
let options_map: Vec<(_, _)> = options
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_bool!(value)))
.collect();
cbor_map_btree!(option_map)
cbor_map_collection!(options_map)
});
let certifications_cbor: Option<cbor::Value> = certifications.map(|certifications| {
let certifications_map: Vec<(_, _)> = certifications
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_int!(value)))
.collect();
cbor_map_collection!(certifications_map)
});
cbor_map_options! {
@@ -169,79 +195,138 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
0x08 => max_credential_id_length,
0x09 => transports.map(|vec| cbor_array_vec!(vec)),
0x0A => algorithms.map(|vec| cbor_array_vec!(vec)),
0x0C => default_cred_protect.map(|p| p as u64),
0x0B => max_serialized_large_blob_array,
0x0C => force_pin_change,
0x0D => min_pin_length as u64,
0x0E => firmware_version,
}
}
#[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),
0x0F => max_cred_blob_length,
0x10 => max_rp_ids_for_set_min_pin_length,
0x13 => certifications_cbor,
0x14 => remaining_discoverable_credentials,
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
#[derive(Debug, PartialEq)]
pub struct AuthenticatorClientPinResponse {
pub key_agreement: Option<CoseKey>,
pub pin_token: Option<Vec<u8>>,
pub pin_uv_auth_token: Option<Vec<u8>>,
pub retries: Option<u64>,
pub power_cycle_state: Option<bool>,
// - 0x05: uvRetries missing as we don't support internal UV.
}
impl From<AuthenticatorClientPinResponse> for cbor::Value {
fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self {
let AuthenticatorClientPinResponse {
key_agreement,
pin_token,
pin_uv_auth_token,
retries,
power_cycle_state,
} = client_pin_response;
cbor_map_options! {
1 => key_agreement.map(|cose_key| cbor_map_btree!(cose_key.0)),
2 => pin_token,
3 => retries,
0x01 => key_agreement.map(cbor::Value::from),
0x02 => pin_uv_auth_token,
0x03 => retries,
0x04 => power_cycle_state,
}
}
}
#[derive(Debug, PartialEq)]
pub struct AuthenticatorLargeBlobsResponse {
pub config: Vec<u8>,
}
impl From<AuthenticatorLargeBlobsResponse> for cbor::Value {
fn from(platform_large_blobs_response: AuthenticatorLargeBlobsResponse) -> Self {
let AuthenticatorLargeBlobsResponse { config } = platform_large_blobs_response;
cbor_map_options! {
0x01 => config,
}
}
}
#[derive(Debug, Default, PartialEq)]
pub struct AuthenticatorCredentialManagementResponse {
pub existing_resident_credentials_count: Option<u64>,
pub max_possible_remaining_resident_credentials_count: Option<u64>,
pub rp: Option<PublicKeyCredentialRpEntity>,
pub rp_id_hash: Option<Vec<u8>>,
pub total_rps: Option<u64>,
pub user: Option<PublicKeyCredentialUserEntity>,
pub credential_id: Option<PublicKeyCredentialDescriptor>,
pub public_key: Option<CoseKey>,
pub total_credentials: Option<u64>,
pub cred_protect: Option<CredentialProtectionPolicy>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorCredentialManagementResponse> for cbor::Value {
fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self {
let AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count,
max_possible_remaining_resident_credentials_count,
rp,
rp_id_hash,
total_rps,
user,
credential_id,
public_key,
total_credentials,
cred_protect,
large_blob_key,
} = cred_management_response;
cbor_map_options! {
0x01 => existing_resident_credentials_count,
0x02 => max_possible_remaining_resident_credentials_count,
0x03 => rp,
0x04 => rp_id_hash,
0x05 => total_rps,
0x06 => user,
0x07 => credential_id,
0x08 => public_key.map(cbor::Value::from),
0x09 => total_credentials,
0x0A => cred_protect,
0x0B => large_blob_key,
}
}
}
#[derive(Debug, PartialEq)]
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! {
0x01 => cert_programmed,
0x02 => pkey_programmed,
}
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::PackedAttestationStatement;
#[cfg(feature = "with_ctap2_1")]
use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType};
use super::super::ES256_CRED_PARAM;
use super::*;
use cbor::{cbor_bytes, cbor_map};
use cbor::{cbor_array, cbor_bytes, cbor_map};
use crypto::rng256::ThreadRng256;
#[test]
fn test_make_credential_into_cbor() {
let certificate: cbor::values::KeyType = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]];
let certificate = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]];
let att_stmt = PackedAttestationStatement {
alg: 1,
sig: vec![0x55, 0x55, 0x55, 0x55],
@@ -251,7 +336,7 @@ mod test {
let cbor_packed_attestation_statement = cbor_map! {
"alg" => 1,
"sig" => vec![0x55, 0x55, 0x55, 0x55],
"x5c" => cbor_array_vec![vec![certificate]],
"x5c" => cbor_array![certificate],
"ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D],
};
@@ -259,31 +344,60 @@ mod test {
fmt: "packed".to_string(),
auth_data: vec![0xAD],
att_stmt,
ep_att: Some(true),
large_blob_key: Some(vec![0x1B]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorMakeCredential(make_credential_response).into();
let expected_cbor = cbor_map_options! {
1 => "packed",
2 => vec![0xAD],
3 => cbor_packed_attestation_statement,
0x01 => "packed",
0x02 => vec![0xAD],
0x03 => cbor_packed_attestation_statement,
0x04 => true,
0x05 => vec![0x1B],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_assertion_into_cbor() {
let pub_key_cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
transports: Some(vec![AuthenticatorTransport::Usb]),
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0x1D, 0x1D, 0x1D, 0x1D],
user_name: Some("foo".to_string()),
user_display_name: Some("bar".to_string()),
user_icon: Some("example.com/foo/icon.png".to_string()),
};
let get_assertion_response = AuthenticatorGetAssertionResponse {
credential: None,
credential: Some(pub_key_cred_descriptor),
auth_data: vec![0xAD],
signature: vec![0x51],
user: None,
number_of_credentials: None,
user: Some(user),
number_of_credentials: Some(2),
large_blob_key: Some(vec![0x1B]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetAssertion(get_assertion_response).into();
let expected_cbor = cbor_map_options! {
2 => vec![0xAD],
3 => vec![0x51],
0x01 => cbor_map! {
"id" => vec![0x2D, 0x2D, 0x2D, 0x2D],
"type" => "public-key",
"transports" => cbor_array!["usb"],
},
0x02 => vec![0xAD],
0x03 => vec![0x51],
0x04 => cbor_map! {
"id" => vec![0x1D, 0x1D, 0x1D, 0x1D],
"icon" => "example.com/foo/icon.png".to_string(),
"name" => "foo".to_string(),
"displayName" => "bar".to_string(),
},
0x05 => 2,
0x07 => vec![0x1B],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
@@ -298,28 +412,21 @@ mod test {
options: None,
max_msg_size: None,
pin_protocols: None,
#[cfg(feature = "with_ctap2_1")]
max_credential_count_in_list: None,
#[cfg(feature = "with_ctap2_1")]
max_credential_id_length: None,
#[cfg(feature = "with_ctap2_1")]
transports: None,
#[cfg(feature = "with_ctap2_1")]
algorithms: None,
default_cred_protect: None,
#[cfg(feature = "with_ctap2_1")]
max_serialized_large_blob_array: None,
force_pin_change: None,
min_pin_length: 4,
#[cfg(feature = "with_ctap2_1")]
firmware_version: None,
max_cred_blob_length: None,
max_rp_ids_for_set_min_pin_length: None,
certifications: None,
remaining_discoverable_credentials: None,
};
let response_cbor: Option<cbor::Value> =
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! {
0x01 => cbor_array_vec![versions],
0x03 => vec![0x00; 16],
@@ -329,56 +436,71 @@ mod test {
}
#[test]
#[cfg(feature = "with_ctap2_1")]
fn test_get_info_optionals_into_cbor() {
let mut options_map = BTreeMap::new();
options_map.insert(String::from("rk"), true);
let get_info_response = AuthenticatorGetInfoResponse {
versions: vec!["FIDO_2_0".to_string()],
extensions: Some(vec!["extension".to_string()]),
aaguid: [0x00; 16],
options: Some(options_map),
options: Some(vec![(String::from("rk"), true)]),
max_msg_size: Some(1024),
pin_protocols: Some(vec![1]),
max_credential_count_in_list: Some(20),
max_credential_id_length: Some(256),
transports: Some(vec![AuthenticatorTransport::Usb]),
algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
max_serialized_large_blob_array: Some(1024),
force_pin_change: Some(false),
min_pin_length: 4,
firmware_version: Some(0),
max_cred_blob_length: Some(1024),
max_rp_ids_for_set_min_pin_length: Some(8),
certifications: Some(vec![(String::from("example-cert"), 1)]),
remaining_discoverable_credentials: Some(150),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![vec!["FIDO_2_0"]],
0x02 => cbor_array_vec![vec!["extension"]],
0x01 => cbor_array!["FIDO_2_0"],
0x02 => cbor_array!["extension"],
0x03 => vec![0x00; 16],
0x04 => cbor_map! {"rk" => true},
0x05 => 1024,
0x06 => cbor_array_vec![vec![1]],
0x06 => cbor_array![1],
0x07 => 20,
0x08 => 256,
0x09 => cbor_array_vec![vec!["usb"]],
0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]],
0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64,
0x09 => cbor_array!["usb"],
0x0A => cbor_array![ES256_CRED_PARAM],
0x0B => 1024,
0x0C => false,
0x0D => 4,
0x0E => 0,
0x0F => 1024,
0x10 => 8,
0x13 => cbor_map! {"example-cert" => 1},
0x14 => 150,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_client_pin_into_cbor() {
let mut rng = ThreadRng256 {};
let sk = crypto::ecdh::SecKey::gensk(&mut rng);
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let client_pin_response = AuthenticatorClientPinResponse {
key_agreement: None,
pin_token: Some(vec![70]),
retries: None,
key_agreement: Some(cose_key.clone()),
pin_uv_auth_token: Some(vec![70]),
retries: Some(8),
power_cycle_state: Some(false),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into();
let expected_cbor = cbor_map_options! {
2 => vec![70],
0x01 => cbor::Value::from(cose_key),
0x02 => vec![70],
0x03 => 8,
0x04 => false,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
@@ -395,10 +517,132 @@ mod test {
assert_eq!(response_cbor, None);
}
#[cfg(feature = "with_ctap2_1")]
#[test]
fn test_used_credential_management_into_cbor() {
let cred_management_response = AuthenticatorCredentialManagementResponse::default();
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_credential_management_optionals_into_cbor() {
let mut rng = ThreadRng256 {};
let sk = crypto::ecdh::SecKey::gensk(&mut rng);
let rp = PublicKeyCredentialRpEntity {
rp_id: String::from("example.com"),
rp_name: None,
rp_icon: None,
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0xFA, 0xB1, 0xA2],
user_name: None,
user_display_name: None,
user_icon: None,
};
let cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None,
};
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let cred_management_response = AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(100),
max_possible_remaining_resident_credentials_count: Some(96),
rp: Some(rp.clone()),
rp_id_hash: Some(vec![0x1D; 32]),
total_rps: Some(3),
user: Some(user.clone()),
credential_id: Some(cred_descriptor.clone()),
public_key: Some(cose_key.clone()),
total_credentials: Some(2),
cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
large_blob_key: Some(vec![0xBB; 64]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => 100,
0x02 => 96,
0x03 => rp,
0x04 => vec![0x1D; 32],
0x05 => 3,
0x06 => user,
0x07 => cred_descriptor,
0x08 => cbor::Value::from(cose_key),
0x09 => 2,
0x0A => 0x01,
0x0B => vec![0xBB; 64],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_credential_management_into_cbor() {
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(None).into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_selection_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_large_blobs_into_cbor() {
let large_blobs_response = AuthenticatorLargeBlobsResponse { config: vec![0xC0] };
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorLargeBlobs(Some(large_blobs_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => vec![0xC0],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_large_blobs_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorLargeBlobs(None).into();
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! {
0x01 => true,
0x02 => 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! {
0x01 => false,
0x02 => 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");
// 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_MISSING_PARAMETER = 0x14,
CTAP2_ERR_LIMIT_EXCEEDED = 0x15,
CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_FP_DATABASE_FULL = 0x17,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_PC_STORAGE_FULL = 0x18,
CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18,
CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,
CTAP2_ERR_PROCESSING = 0x21,
CTAP2_ERR_INVALID_CREDENTIAL = 0x22,
@@ -57,29 +54,30 @@ pub enum Ctap2StatusCode {
CTAP2_ERR_PIN_AUTH_INVALID = 0x33,
CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34,
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_TOKEN_EXPIRED = 0x38,
CTAP2_ERR_REQUEST_TOO_LARGE = 0x39,
CTAP2_ERR_ACTION_TIMEOUT = 0x3A,
CTAP2_ERR_UP_REQUIRED = 0x3B,
CTAP2_ERR_UV_BLOCKED = 0x3C,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_INTEGRITY_FAILURE = 0x3D,
#[cfg(feature = "with_ctap2_1")]
CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E,
CTAP2_ERR_UV_INVALID = 0x3F,
CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40,
CTAP1_ERR_OTHER = 0x7F,
CTAP2_ERR_SPEC_LAST = 0xDF,
CTAP2_ERR_EXTENSION_FIRST = 0xE0,
CTAP2_ERR_EXTENSION_LAST = 0xEF,
// CTAP2_ERR_VENDOR_FIRST = 0xF0,
CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0,
CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1,
_CTAP2_ERR_SPEC_LAST = 0xDF,
_CTAP2_ERR_EXTENSION_FIRST = 0xE0,
_CTAP2_ERR_EXTENSION_LAST = 0xEF,
_CTAP2_ERR_VENDOR_FIRST = 0xF0,
/// An internal invariant is broken.
///
/// This type of error is unexpected and the current state is undefined.
CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2,
CTAP2_ERR_VENDOR_LAST = 0xFF,
/// The hardware is malfunctioning.
///
/// It may be possible that some of those errors are actually internal errors.
CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3,
_CTAP2_ERR_VENDOR_LAST = 0xFF,
}

File diff suppressed because it is too large Load Diff

159
src/ctap/storage/key.rs Normal file
View File

@@ -0,0 +1,159 @@
// Copyright 2019-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.
/// Number of keys that persist the CTAP reset command.
pub const NUM_PERSISTENT_KEYS: usize = 20;
/// Defines a key given its name and value or range of values.
macro_rules! make_key {
($(#[$doc: meta])* $name: ident = $key: literal..$end: literal) => {
$(#[$doc])* pub const $name: core::ops::Range<usize> = $key..$end;
};
($(#[$doc: meta])* $name: ident = $key: literal) => {
$(#[$doc])* pub const $name: usize = $key;
};
}
/// Returns the range of values of a key given its value description.
#[cfg(test)]
macro_rules! make_range {
($key: literal..$end: literal) => {
$key..$end
};
($key: literal) => {
$key..$key + 1
};
}
/// Helper to define keys as a partial partition of a range.
macro_rules! make_partition {
($range: expr,
$(
$(#[$doc: meta])*
$name: ident = $key: literal $(.. $end: literal)?;
)*) => {
$(
make_key!($(#[$doc])* $name = $key $(.. $end)?);
)*
#[cfg(test)]
const KEY_RANGE: core::ops::Range<usize> = $range;
#[cfg(test)]
const ALL_KEYS: &[core::ops::Range<usize>] = &[$(make_range!($key $(.. $end)?)),*];
};
}
make_partition! {
// We reserve 0 and 2048+ for possible migration purposes. We add persistent entries starting
// from 1 and going up. We add non-persistent entries starting from 2047 and going down. This
// way, we don't commit to a fixed number of persistent keys.
1..2048,
// WARNING: Keys should not be deleted but prefixed with `_` to avoid accidentally reusing them.
/// The attestation private key.
ATTESTATION_PRIVATE_KEY = 1;
/// The attestation certificate.
ATTESTATION_CERTIFICATE = 2;
/// The aaguid.
AAGUID = 3;
// This is the persistent key limit:
// - When adding a (persistent) key above this message, make sure its value is smaller than
// NUM_PERSISTENT_KEYS.
// - When adding a (non-persistent) key below this message, make sure its value is bigger or
// equal than NUM_PERSISTENT_KEYS.
/// Reserved for future credential-related objects.
///
/// In particular, additional credentials could be added there by reducing the lower bound of
/// the credential range below as well as the upper bound of this range in a similar manner.
_RESERVED_CREDENTIALS = 1000..1700;
/// The credentials.
///
/// Depending on `MAX_SUPPORTED_RESIDENT_KEYS`, only a prefix of those keys is used. Each
/// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size.
CREDENTIALS = 1700..2000;
/// Storage for the serialized large blob array.
///
/// The stored large blob can be too big for one key, so it has to be sharded.
LARGE_BLOB_SHARDS = 2000..2004;
/// If this entry exists and is empty, alwaysUv is enabled.
ALWAYS_UV = 2038;
/// If this entry exists and is empty, enterprise attestation is enabled.
ENTERPRISE_ATTESTATION = 2039;
/// If this entry exists and is empty, the PIN needs to be changed.
FORCE_PIN_CHANGE = 2040;
/// The secret of the CredRandom feature.
CRED_RANDOM_SECRET = 2041;
/// List of RP IDs allowed to read the minimum PIN length.
MIN_PIN_LENGTH_RP_IDS = 2042;
/// The minimum PIN length.
///
/// If the entry is absent, the minimum PIN length is `DEFAULT_MIN_PIN_LENGTH`.
MIN_PIN_LENGTH = 2043;
/// The number of PIN retries.
///
/// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`.
PIN_RETRIES = 2044;
/// The PIN hash and length.
///
/// If the entry is absent, there is no PIN set. The first byte represents
/// the length, the following are an array with the hash.
PIN_PROPERTIES = 2045;
/// The encryption and hmac keys.
///
/// This entry is always present. It is generated at startup if absent.
MASTER_KEYS = 2046;
/// The global signature counter.
///
/// If the entry is absent, the counter is 0.
GLOBAL_SIGNATURE_COUNTER = 2047;
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn enough_credentials() {
use crate::ctap::customization::MAX_SUPPORTED_RESIDENT_KEYS;
assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start);
}
#[test]
fn keys_are_disjoint() {
// Check that keys are in the range.
for keys in ALL_KEYS {
assert!(KEY_RANGE.start <= keys.start && keys.end <= KEY_RANGE.end);
}
// Check that keys are assigned at most once, essentially partitioning the range.
for key in KEY_RANGE {
assert!(ALL_KEYS.iter().filter(|keys| keys.contains(&key)).count() <= 1);
}
}
}

View File

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

277
src/ctap/token_state.rs Normal file
View File

@@ -0,0 +1,277 @@
// Copyright 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 crate::ctap::client_pin::PinPermission;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::timed_permission::TimedPermission;
use alloc::string::String;
use crypto::sha256::Sha256;
use crypto::Hash256;
use libtock_drivers::timer::{ClockValue, Duration};
/// Timeout for auth tokens.
///
/// This usage time limit is correct for USB, BLE, and internal.
/// NFC only allows 19.8 seconds.
/// TODO(#15) multiplex over transports, add NFC
const INITIAL_USAGE_TIME_LIMIT: Duration<isize> = Duration::from_ms(30000);
/// Implements pinUvAuthToken state from section 6.5.2.1.
///
/// The userPresent flag is omitted as the only way to set it to true is
/// built-in user verification. Therefore, we never cache user presence.
///
/// This implementation does not use a rolling timer.
pub struct PinUvAuthTokenState {
// Relies on the fact that all permissions are represented by powers of two.
permissions_set: u8,
permissions_rp_id: Option<String>,
usage_timer: TimedPermission,
user_verified: bool,
in_use: bool,
}
impl PinUvAuthTokenState {
/// Creates a pinUvAuthToken state without permissions.
pub fn new() -> PinUvAuthTokenState {
PinUvAuthTokenState {
permissions_set: 0,
permissions_rp_id: None,
usage_timer: TimedPermission::waiting(),
user_verified: false,
in_use: false,
}
}
/// Returns whether the pinUvAuthToken is active.
pub fn is_in_use(&self) -> bool {
self.in_use
}
/// Checks if the permission is granted.
pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> {
if permission as u8 & self.permissions_set != 0 {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
/// Checks if there is no associated permissions RPID.
pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> {
if self.permissions_rp_id.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
Ok(())
}
/// Checks if the permissions RPID is associated.
pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
match &self.permissions_rp_id {
Some(p) if rp_id == p => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Checks if the permissions RPID's association matches the hash.
pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> {
match &self.permissions_rp_id {
Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Sets the permissions, represented as bits in a byte.
pub fn set_permissions(&mut self, permissions: u8) {
self.permissions_set = permissions;
}
/// Sets the permissions RPID.
pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option<String>) {
self.permissions_rp_id = permissions_rp_id;
}
/// Sets the default permissions.
///
/// Allows MakeCredential and GetAssertion, without specifying a RP ID.
pub fn set_default_permissions(&mut self) {
self.set_permissions(0x03);
self.set_permissions_rp_id(None);
}
/// Starts the timer for pinUvAuthToken usage.
pub fn begin_using_pin_uv_auth_token(&mut self, now: ClockValue) {
self.user_verified = true;
self.usage_timer = TimedPermission::granted(now, INITIAL_USAGE_TIME_LIMIT);
self.in_use = true;
}
/// Updates the usage timer, and disables the pinUvAuthToken on timeout.
pub fn pin_uv_auth_token_usage_timer_observer(&mut self, now: ClockValue) {
if !self.in_use {
return;
}
self.usage_timer = self.usage_timer.check_expiration(now);
if !self.usage_timer.is_granted(now) {
self.stop_using_pin_uv_auth_token();
}
}
/// Returns whether the user is verified.
pub fn get_user_verified_flag_value(&self) -> bool {
self.in_use && self.user_verified
}
/// Consumes the user verification.
pub fn clear_user_verified_flag(&mut self) {
self.user_verified = false;
}
/// Clears all permissions except Large Blob Write.
pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) {
self.permissions_set &= PinPermission::LargeBlobWrite as u8;
}
/// Resets to the initial state.
pub fn stop_using_pin_uv_auth_token(&mut self) {
self.permissions_rp_id = None;
self.permissions_set = 0;
self.usage_timer = TimedPermission::waiting();
self.user_verified = false;
self.in_use = false;
}
}
#[cfg(test)]
mod test {
use super::*;
use enum_iterator::IntoEnumIterator;
const CLOCK_FREQUENCY_HZ: usize = 32768;
const START_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
const SMALL_DURATION: Duration<isize> = Duration::from_ms(100);
#[test]
fn test_observer() {
let mut token_state = PinUvAuthTokenState::new();
let mut now = START_CLOCK_VALUE;
token_state.begin_using_pin_uv_auth_token(now);
assert!(token_state.is_in_use());
now = now.wrapping_add(SMALL_DURATION);
token_state.pin_uv_auth_token_usage_timer_observer(now);
assert!(token_state.is_in_use());
now = now.wrapping_add(INITIAL_USAGE_TIME_LIMIT);
token_state.pin_uv_auth_token_usage_timer_observer(now);
assert!(!token_state.is_in_use());
}
#[test]
fn test_stop() {
let mut token_state = PinUvAuthTokenState::new();
token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE);
assert!(token_state.is_in_use());
token_state.stop_using_pin_uv_auth_token();
assert!(!token_state.is_in_use());
}
#[test]
fn test_permissions() {
let mut token_state = PinUvAuthTokenState::new();
token_state.set_permissions(0xFF);
for permission in PinPermission::into_enum_iter() {
assert_eq!(token_state.has_permission(permission), Ok(()));
}
token_state.clear_pin_uv_auth_token_permissions_except_lbw();
assert_eq!(
token_state.has_permission(PinPermission::CredentialManagement),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permission(PinPermission::LargeBlobWrite),
Ok(())
);
token_state.stop_using_pin_uv_auth_token();
for permission in PinPermission::into_enum_iter() {
assert_eq!(
token_state.has_permission(permission),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}
#[test]
fn test_permissions_rp_id_none() {
let mut token_state = PinUvAuthTokenState::new();
let example_hash = Sha256::hash(b"example.com");
token_state.set_permissions_rp_id(None);
assert_eq!(token_state.has_no_permissions_rp_id(), Ok(()));
assert_eq!(
token_state.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_permissions_rp_id_some() {
let mut token_state = PinUvAuthTokenState::new();
let example_hash = Sha256::hash(b"example.com");
token_state.set_permissions_rp_id(Some(String::from("example.com")));
assert_eq!(
token_state.has_no_permissions_rp_id(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(()));
assert_eq!(
token_state.has_permissions_rp_id("another.example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Ok(())
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&[0x1D; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
token_state.stop_using_pin_uv_auth_token();
assert_eq!(
token_state.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_user_verified_flag() {
let mut token_state = PinUvAuthTokenState::new();
assert!(!token_state.get_user_verified_flag_value());
token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE);
assert!(token_state.get_user_verified_flag_value());
token_state.clear_user_verified_flag();
assert!(!token_state.get_user_verified_flag_value());
token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE);
assert!(token_state.get_user_verified_flag_value());
token_state.stop_using_pin_uv_auth_token();
assert!(!token_state.get_user_verified_flag_value());
}
}

View File

@@ -1,457 +0,0 @@
// Copyright 2019 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::{Index, Storage, StorageError, StorageResult};
use alloc::boxed::Box;
use alloc::vec;
pub struct BufferStorage {
storage: Box<[u8]>,
options: BufferOptions,
word_writes: Box<[usize]>,
page_erases: Box<[usize]>,
snapshot: Snapshot,
}
#[derive(Copy, Clone, Debug)]
pub struct BufferOptions {
/// Size of a word in bytes.
pub word_size: usize,
/// Size of a page in bytes.
pub page_size: usize,
/// How many times a word can be written between page erasures
pub max_word_writes: usize,
/// How many times a page can be erased.
pub max_page_erases: usize,
/// Bits cannot be written from 0 to 1.
pub strict_write: bool,
}
impl BufferStorage {
/// Creates a fake embedded flash using a buffer.
///
/// This implementation checks that no words are written more than `max_word_writes` between
/// page erasures and than no pages are erased more than `max_page_erases`. If `strict_write` is
/// true, it also checks that no bits are written from 0 to 1. It also permits to take snapshots
/// of the storage during write and erase operations (although words would still be written or
/// erased completely).
///
/// # Panics
///
/// The following preconditions must hold:
/// - `options.word_size` must be a power of two.
/// - `options.page_size` must be a power of two.
/// - `options.page_size` must be word-aligned.
/// - `storage.len()` must be page-aligned.
pub fn new(storage: Box<[u8]>, options: BufferOptions) -> BufferStorage {
assert!(options.word_size.is_power_of_two());
assert!(options.page_size.is_power_of_two());
let num_words = storage.len() / options.word_size;
let num_pages = storage.len() / options.page_size;
let buffer = BufferStorage {
storage,
options,
word_writes: vec![0; num_words].into_boxed_slice(),
page_erases: vec![0; num_pages].into_boxed_slice(),
snapshot: Snapshot::Ready,
};
assert!(buffer.is_word_aligned(buffer.options.page_size));
assert!(buffer.is_page_aligned(buffer.storage.len()));
buffer
}
/// Takes a snapshot of the storage after a given amount of word operations.
///
/// Each time a word is written or erased, the delay is decremented if positive. Otherwise, a
/// snapshot is taken before the operation is executed.
///
/// # Panics
///
/// Panics if a snapshot has been armed and not examined.
pub fn arm_snapshot(&mut self, delay: usize) {
self.snapshot.arm(delay);
}
/// Unarms and returns the snapshot or the delay remaining.
///
/// # Panics
///
/// Panics if a snapshot was not armed.
pub fn get_snapshot(&mut self) -> Result<Box<[u8]>, usize> {
self.snapshot.get()
}
/// Takes a snapshot of the storage.
pub fn take_snapshot(&self) -> Box<[u8]> {
self.storage.clone()
}
/// Returns the storage.
pub fn get_storage(self) -> Box<[u8]> {
self.storage
}
fn is_word_aligned(&self, x: usize) -> bool {
x & (self.options.word_size - 1) == 0
}
fn is_page_aligned(&self, x: usize) -> bool {
x & (self.options.page_size - 1) == 0
}
/// Writes a slice to the storage.
///
/// The slice `value` is written to `index`. The `erase` boolean specifies whether this is an
/// erase operation or a write operation which matters for the checks and updating the shadow
/// storage. This also takes a snapshot of the storage if a snapshot was armed and the delay has
/// elapsed.
///
/// The following preconditions should hold:
/// - `index` is word-aligned.
/// - `value.len()` is word-aligned.
///
/// The following checks are performed:
/// - The region of length `value.len()` starting at `index` fits in a storage page.
/// - A word is not written more than `max_word_writes`.
/// - A page is not erased more than `max_page_erases`.
/// - The new word only switches 1s to 0s (only if `strict_write` is set).
fn update_storage(&mut self, index: Index, value: &[u8], erase: bool) -> StorageResult<()> {
debug_assert!(self.is_word_aligned(index.byte) && self.is_word_aligned(value.len()));
let dst = index.range(value.len(), self)?.step_by(self.word_size());
let src = value.chunks(self.word_size());
// Check and update page shadow.
if erase {
let page = index.page;
assert!(self.page_erases[page] < self.max_page_erases());
self.page_erases[page] += 1;
}
for (byte, val) in dst.zip(src) {
let range = byte..byte + self.word_size();
// The driver doesn't write identical words.
if &self.storage[range.clone()] == val {
continue;
}
// Check and update word shadow.
let word = byte / self.word_size();
if erase {
self.word_writes[word] = 0;
} else {
assert!(self.word_writes[word] < self.max_word_writes());
self.word_writes[word] += 1;
}
// Check strict write.
if !erase && self.options.strict_write {
for (byte, &val) in range.clone().zip(val) {
assert_eq!(self.storage[byte] & val, val);
}
}
// Take snapshot if armed and delay expired.
self.snapshot.take(&self.storage);
// Write storage
self.storage[range].copy_from_slice(val);
}
Ok(())
}
}
impl Storage for BufferStorage {
fn word_size(&self) -> usize {
self.options.word_size
}
fn page_size(&self) -> usize {
self.options.page_size
}
fn num_pages(&self) -> usize {
self.storage.len() / self.options.page_size
}
fn max_word_writes(&self) -> usize {
self.options.max_word_writes
}
fn max_page_erases(&self) -> usize {
self.options.max_page_erases
}
fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]> {
Ok(&self.storage[index.range(length, self)?])
}
fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()> {
if !self.is_word_aligned(index.byte) || !self.is_word_aligned(value.len()) {
return Err(StorageError::NotAligned);
}
self.update_storage(index, value, false)
}
fn erase_page(&mut self, page: usize) -> StorageResult<()> {
let index = Index { page, byte: 0 };
let value = vec![0xff; self.page_size()];
self.update_storage(index, &value, true)
}
}
// Controls when a snapshot of the storage is taken.
//
// This can be used to simulate power-offs while the device is writing to the storage or erasing a
// page in the storage.
enum Snapshot {
// Mutable word operations have normal behavior.
Ready,
// If the delay is positive, mutable word operations decrement it. If the count is zero, mutable
// word operations take a snapshot of the storage.
Armed { delay: usize },
// Mutable word operations have normal behavior.
Taken { storage: Box<[u8]> },
}
impl Snapshot {
fn arm(&mut self, delay: usize) {
match self {
Snapshot::Ready => *self = Snapshot::Armed { delay },
_ => panic!(),
}
}
fn get(&mut self) -> Result<Box<[u8]>, usize> {
let mut snapshot = Snapshot::Ready;
core::mem::swap(self, &mut snapshot);
match snapshot {
Snapshot::Armed { delay } => Err(delay),
Snapshot::Taken { storage } => Ok(storage),
_ => panic!(),
}
}
fn take(&mut self, storage: &[u8]) {
if let Snapshot::Armed { delay } = self {
if *delay == 0 {
let storage = storage.to_vec().into_boxed_slice();
*self = Snapshot::Taken { storage };
} else {
*delay -= 1;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const NUM_PAGES: usize = 2;
const OPTIONS: BufferOptions = BufferOptions {
word_size: 4,
page_size: 16,
max_word_writes: 2,
max_page_erases: 3,
strict_write: true,
};
// Those words are decreasing bit patterns. Bits are only changed from 1 to 0 and at last one
// bit is changed.
const BLANK_WORD: &[u8] = &[0xff, 0xff, 0xff, 0xff];
const FIRST_WORD: &[u8] = &[0xee, 0xdd, 0xbb, 0x77];
const SECOND_WORD: &[u8] = &[0xca, 0xc9, 0xa9, 0x65];
const THIRD_WORD: &[u8] = &[0x88, 0x88, 0x88, 0x44];
fn new_storage() -> Box<[u8]> {
vec![0xff; NUM_PAGES * OPTIONS.page_size].into_boxed_slice()
}
#[test]
fn words_are_decreasing() {
fn assert_is_decreasing(prev: &[u8], next: &[u8]) {
for (&prev, &next) in prev.iter().zip(next.iter()) {
assert_eq!(prev & next, next);
assert!(prev != next);
}
}
assert_is_decreasing(BLANK_WORD, FIRST_WORD);
assert_is_decreasing(FIRST_WORD, SECOND_WORD);
assert_is_decreasing(SECOND_WORD, THIRD_WORD);
}
#[test]
fn options_ok() {
let buffer = BufferStorage::new(new_storage(), OPTIONS);
assert_eq!(buffer.word_size(), OPTIONS.word_size);
assert_eq!(buffer.page_size(), OPTIONS.page_size);
assert_eq!(buffer.num_pages(), NUM_PAGES);
assert_eq!(buffer.max_word_writes(), OPTIONS.max_word_writes);
assert_eq!(buffer.max_page_erases(), OPTIONS.max_page_erases);
}
#[test]
fn read_write_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 0 };
let next_index = Index { page: 0, byte: 4 };
assert_eq!(buffer.read_slice(index, 4).unwrap(), BLANK_WORD);
buffer.write_slice(index, FIRST_WORD).unwrap();
assert_eq!(buffer.read_slice(index, 4).unwrap(), FIRST_WORD);
assert_eq!(buffer.read_slice(next_index, 4).unwrap(), BLANK_WORD);
}
#[test]
fn erase_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 0 };
let other_index = Index { page: 1, byte: 0 };
buffer.write_slice(index, FIRST_WORD).unwrap();
buffer.write_slice(other_index, FIRST_WORD).unwrap();
assert_eq!(buffer.read_slice(index, 4).unwrap(), FIRST_WORD);
assert_eq!(buffer.read_slice(other_index, 4).unwrap(), FIRST_WORD);
buffer.erase_page(0).unwrap();
assert_eq!(buffer.read_slice(index, 4).unwrap(), BLANK_WORD);
assert_eq!(buffer.read_slice(other_index, 4).unwrap(), FIRST_WORD);
}
#[test]
fn invalid_range() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 12 };
let half_index = Index { page: 0, byte: 14 };
let over_index = Index { page: 0, byte: 16 };
let bad_page = Index { page: 2, byte: 0 };
// Reading a word in the storage is ok.
assert!(buffer.read_slice(index, 4).is_ok());
// Reading a half-word in the storage is ok.
assert!(buffer.read_slice(half_index, 2).is_ok());
// Reading even a single byte outside a page is not ok.
assert!(buffer.read_slice(over_index, 1).is_err());
// But reading an empty slice just after a page is ok.
assert!(buffer.read_slice(over_index, 0).is_ok());
// Reading even an empty slice outside the storage is not ok.
assert!(buffer.read_slice(bad_page, 0).is_err());
// Writing a word in the storage is ok.
assert!(buffer.write_slice(index, FIRST_WORD).is_ok());
// Writing an unaligned word is not ok.
assert!(buffer.write_slice(half_index, FIRST_WORD).is_err());
// Writing a word outside a page is not ok.
assert!(buffer.write_slice(over_index, FIRST_WORD).is_err());
// But writing an empty slice just after a page is ok.
assert!(buffer.write_slice(over_index, &[]).is_ok());
// Writing even an empty slice outside the storage is not ok.
assert!(buffer.write_slice(bad_page, &[]).is_err());
// Only pages in the storage can be erased.
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(2).is_err());
}
#[test]
fn write_twice_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 4 };
assert!(buffer.write_slice(index, FIRST_WORD).is_ok());
assert!(buffer.write_slice(index, SECOND_WORD).is_ok());
}
#[test]
fn write_twice_and_once_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 0 };
let next_index = Index { page: 0, byte: 4 };
assert!(buffer.write_slice(index, FIRST_WORD).is_ok());
assert!(buffer.write_slice(index, SECOND_WORD).is_ok());
assert!(buffer.write_slice(next_index, THIRD_WORD).is_ok());
}
#[test]
#[should_panic]
fn write_three_times_panics() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 4 };
assert!(buffer.write_slice(index, FIRST_WORD).is_ok());
assert!(buffer.write_slice(index, SECOND_WORD).is_ok());
let _ = buffer.write_slice(index, THIRD_WORD);
}
#[test]
fn write_twice_then_once_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 0 };
assert!(buffer.write_slice(index, FIRST_WORD).is_ok());
assert!(buffer.write_slice(index, SECOND_WORD).is_ok());
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.write_slice(index, FIRST_WORD).is_ok());
}
#[test]
fn erase_three_times_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(0).is_ok());
}
#[test]
fn erase_three_times_and_once_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(1).is_ok());
}
#[test]
#[should_panic]
fn erase_four_times_panics() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(0).is_ok());
assert!(buffer.erase_page(0).is_ok());
let _ = buffer.erase_page(0).is_ok();
}
#[test]
#[should_panic]
fn switch_zero_to_one_panics() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 0 };
assert!(buffer.write_slice(index, SECOND_WORD).is_ok());
let _ = buffer.write_slice(index, FIRST_WORD);
}
#[test]
fn get_storage_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 4 };
buffer.write_slice(index, FIRST_WORD).unwrap();
let storage = buffer.get_storage();
assert_eq!(&storage[..4], BLANK_WORD);
assert_eq!(&storage[4..8], FIRST_WORD);
}
#[test]
fn snapshot_ok() {
let mut buffer = BufferStorage::new(new_storage(), OPTIONS);
let index = Index { page: 0, byte: 0 };
let value = [FIRST_WORD, SECOND_WORD].concat();
buffer.arm_snapshot(1);
buffer.write_slice(index, &value).unwrap();
let storage = buffer.get_snapshot().unwrap();
assert_eq!(&storage[..8], &[FIRST_WORD, BLANK_WORD].concat()[..]);
let storage = buffer.take_snapshot();
assert_eq!(&storage[..8], &value[..]);
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC
// Copyright 2019-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.
@@ -12,16 +12,41 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(any(test, feature = "ram_storage"))]
mod buffer;
mod storage;
mod store;
#[cfg(not(any(test, feature = "ram_storage")))]
#[cfg(not(feature = "std"))]
mod syscall;
#[cfg(any(test, feature = "ram_storage"))]
pub use self::buffer::{BufferOptions, BufferStorage};
pub use self::storage::{Index, Storage, StorageError, StorageResult};
pub use self::store::{Store, StoreConfig, StoreEntry, StoreError, StoreIndex};
#[cfg(not(any(test, feature = "ram_storage")))]
#[cfg(not(feature = "std"))]
pub use self::syscall::SyscallStorage;
/// Storage definition for production.
#[cfg(not(feature = "std"))]
mod prod {
pub type Storage = super::SyscallStorage;
pub fn new_storage(num_pages: usize) -> Storage {
Storage::new(num_pages).unwrap()
}
}
#[cfg(not(feature = "std"))]
pub use self::prod::{new_storage, Storage};
/// Storage definition for testing.
#[cfg(feature = "std")]
mod test {
pub type Storage = persistent_store::BufferStorage;
pub fn new_storage(num_pages: usize) -> Storage {
const PAGE_SIZE: usize = 0x1000;
let store = vec![0xff; num_pages * PAGE_SIZE].into_boxed_slice();
let options = persistent_store::BufferOptions {
word_size: 4,
page_size: PAGE_SIZE,
max_word_writes: 2,
max_page_erases: 10000,
strict_mode: true,
};
Storage::new(store, options)
}
}
#[cfg(feature = "std")]
pub use self::test::{new_storage, Storage};

View File

@@ -1,107 +0,0 @@
// Copyright 2019 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.
#[derive(Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Index {
pub page: usize,
pub byte: usize,
}
#[derive(Debug)]
pub enum StorageError {
BadFlash,
NotAligned,
OutOfBounds,
KernelError { code: isize },
}
pub type StorageResult<T> = Result<T, StorageError>;
/// Abstraction for embedded flash storage.
pub trait Storage {
/// Returns the size of a word in bytes.
fn word_size(&self) -> usize;
/// Returns the size of a page in bytes.
fn page_size(&self) -> usize;
/// Returns the number of pages in the storage.
fn num_pages(&self) -> usize;
/// Returns how many times a word can be written between page erasures.
fn max_word_writes(&self) -> usize;
/// Returns how many times a page can be erased in the lifetime of the flash.
fn max_page_erases(&self) -> usize;
/// Reads a slice from the storage.
///
/// The slice does not need to be word-aligned.
///
/// # Errors
///
/// The `index` must designate `length` bytes in the storage.
fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]>;
/// Writes a word-aligned slice to the storage.
///
/// The written words should not have been written too many times since last page erasure.
///
/// # Errors
///
/// The following preconditions must hold:
/// - `index` must be word-aligned.
/// - `value.len()` must be a multiple of the word size.
/// - `index` must designate `value.len()` bytes in the storage.
/// - `value` must be in memory until [read-only allow][tock_1274] is resolved.
///
/// [tock_1274]: https://github.com/tock/tock/issues/1274.
fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()>;
/// Erases a page of the storage.
///
/// # Errors
///
/// The `page` must be in the storage.
fn erase_page(&mut self, page: usize) -> StorageResult<()>;
}
impl Index {
/// Returns whether a slice fits in a storage page.
fn is_valid(self, length: usize, storage: &impl Storage) -> bool {
self.page < storage.num_pages()
&& storage
.page_size()
.checked_sub(length)
.map(|limit| self.byte <= limit)
.unwrap_or(false)
}
/// Returns the range of a valid slice.
///
/// The range starts at `self` with `length` bytes.
pub fn range(
self,
length: usize,
storage: &impl Storage,
) -> StorageResult<core::ops::Range<usize>> {
if self.is_valid(length, storage) {
let start = self.page * storage.page_size() + self.byte;
Ok(start..start + length)
} else {
Err(StorageError::OutOfBounds)
}
}
}

View File

@@ -1,177 +0,0 @@
// Copyright 2019 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.
/// Defines a consecutive sequence of bits.
#[derive(Copy, Clone)]
pub struct BitRange {
/// The first bit of the sequence.
pub start: usize,
/// The length in bits of the sequence.
pub length: usize,
}
impl BitRange {
/// Returns the first bit following a bit range.
pub fn end(self) -> usize {
self.start + self.length
}
}
/// Defines a consecutive sequence of bytes.
///
/// The bits in those bytes are ignored which essentially creates a gap in a sequence of bits. The
/// gap is necessarily at byte boundaries. This is used to ignore the user data in an entry
/// essentially providing a view of the entry information (header and footer).
#[derive(Copy, Clone)]
pub struct ByteGap {
pub start: usize,
pub length: usize,
}
/// Empty gap. All bits count.
pub const NO_GAP: ByteGap = ByteGap {
start: 0,
length: 0,
};
impl ByteGap {
/// Translates a bit to skip the gap.
fn shift(self, bit: usize) -> usize {
if bit < 8 * self.start {
bit
} else {
bit + 8 * self.length
}
}
/// Returns the slice of `data` corresponding to the gap.
pub fn slice(self, data: &[u8]) -> &[u8] {
&data[self.start..self.start + self.length]
}
}
/// Returns whether a bit is set in a sequence of bits.
///
/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that
/// are in `data` but not in `gap`.
pub fn is_zero(bit: usize, data: &[u8], gap: ByteGap) -> bool {
let bit = gap.shift(bit);
debug_assert!(bit < 8 * data.len());
data[bit / 8] & (1 << (bit % 8)) == 0
}
/// Sets a bit to zero in a sequence of bits.
///
/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that
/// are in `data` but not in `gap`.
pub fn set_zero(bit: usize, data: &mut [u8], gap: ByteGap) {
let bit = gap.shift(bit);
debug_assert!(bit < 8 * data.len());
data[bit / 8] &= !(1 << (bit % 8));
}
/// Returns a little-endian value in a sequence of bits.
///
/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that
/// are in `data` but not in `gap`. The range of bits where the value is stored in defined by
/// `range`. The value must fit in a `usize`.
pub fn get_range(range: BitRange, data: &[u8], gap: ByteGap) -> usize {
debug_assert!(range.length <= 8 * core::mem::size_of::<usize>());
let mut result = 0;
for i in 0..range.length {
if !is_zero(range.start + i, data, gap) {
result |= 1 << i;
}
}
result
}
/// Sets a little-endian value in a sequence of bits.
///
/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that
/// are in `data` but not in `gap`. The range of bits where the value is stored in defined by
/// `range`. The bits set to 1 in `value` must also be set to `1` in the sequence of bits.
pub fn set_range(range: BitRange, data: &mut [u8], gap: ByteGap, value: usize) {
debug_assert!(range.length == 8 * core::mem::size_of::<usize>() || value < 1 << range.length);
for i in 0..range.length {
if value & 1 << i == 0 {
set_zero(range.start + i, data, gap);
}
}
}
/// Tests the `is_zero` and `set_zero` pair of functions.
#[test]
fn zero_ok() {
const GAP: ByteGap = ByteGap {
start: 2,
length: 1,
};
for i in 0..24 {
assert!(!is_zero(i, &[0xffu8, 0xff, 0x00, 0xff] as &[u8], GAP));
}
// Tests reading and setting a bit. The result should have all bits set to 1 except for the bit
// to test and the gap.
fn test(bit: usize, result: &[u8]) {
assert!(is_zero(bit, result, GAP));
let mut data = vec![0xff; result.len()];
// Set the gap bits to 0.
for i in 0..GAP.length {
data[GAP.start + i] = 0x00;
}
set_zero(bit, &mut data, GAP);
assert_eq!(data, result);
}
test(0, &[0xfe, 0xff, 0x00, 0xff]);
test(1, &[0xfd, 0xff, 0x00, 0xff]);
test(2, &[0xfb, 0xff, 0x00, 0xff]);
test(7, &[0x7f, 0xff, 0x00, 0xff]);
test(8, &[0xff, 0xfe, 0x00, 0xff]);
test(15, &[0xff, 0x7f, 0x00, 0xff]);
test(16, &[0xff, 0xff, 0x00, 0xfe]);
test(17, &[0xff, 0xff, 0x00, 0xfd]);
test(23, &[0xff, 0xff, 0x00, 0x7f]);
}
/// Tests the `get_range` and `set_range` pair of functions.
#[test]
fn range_ok() {
// Tests reading and setting a range. The result should have all bits set to 1 except for the
// range to test and the gap.
fn test(start: usize, length: usize, value: usize, result: &[u8], gap: ByteGap) {
let range = BitRange { start, length };
assert_eq!(get_range(range, result, gap), value);
let mut data = vec![0xff; result.len()];
for i in 0..gap.length {
data[gap.start + i] = 0x00;
}
set_range(range, &mut data, gap, value);
assert_eq!(data, result);
}
test(0, 8, 42, &[42], NO_GAP);
test(3, 12, 0b11_0101, &[0b1010_1111, 0b1000_0001], NO_GAP);
test(0, 16, 0x1234, &[0x34, 0x12], NO_GAP);
test(4, 16, 0x1234, &[0x4f, 0x23, 0xf1], NO_GAP);
let mut gap = ByteGap {
start: 1,
length: 1,
};
test(3, 12, 0b11_0101, &[0b1010_1111, 0x00, 0b1000_0001], gap);
gap.length = 2;
test(0, 16, 0x1234, &[0x34, 0x00, 0x00, 0x12], gap);
gap.start = 2;
gap.length = 1;
test(4, 16, 0x1234, &[0x4f, 0x23, 0x00, 0xf1], gap);
}

View File

@@ -1,565 +0,0 @@
// Copyright 2019 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::super::{Index, Storage};
use super::{bitfield, StoreConfig, StoreEntry, StoreError};
use alloc::vec;
use alloc::vec::Vec;
/// Whether a user entry is a replace entry.
pub enum IsReplace {
/// This is a replace entry.
Replace,
/// This is an insert entry.
Insert,
}
/// Helpers to parse the store format.
///
/// See the store module-level documentation for information about the format.
pub struct Format {
pub word_size: usize,
pub page_size: usize,
pub num_pages: usize,
pub max_page_erases: usize,
pub num_tags: usize,
/// Whether an entry is present.
///
/// - 0 for entries (user entries or internal entries).
/// - 1 for free space until the end of the page.
present_bit: usize,
/// Whether an entry is deleted.
///
/// - 0 for deleted entries.
/// - 1 for alive entries.
deleted_bit: usize,
/// Whether an entry is internal.
///
/// - 0 for internal entries.
/// - 1 for user entries.
internal_bit: usize,
/// Whether a user entry is a replace entry.
///
/// - 0 for replace entries.
/// - 1 for insert entries.
replace_bit: usize,
/// Whether a user entry has sensitive data.
///
/// - 0 for sensitive data.
/// - 1 for non-sensitive data.
///
/// When a user entry with sensitive data is deleted, the data is overwritten with zeroes. This
/// feature is subject to the same guarantees as all other features of the store, in particular
/// deleting a sensitive entry is atomic. See the store module-level documentation for more
/// information.
sensitive_bit: usize,
/// The data length of a user entry.
length_range: bitfield::BitRange,
/// The tag of a user entry.
tag_range: bitfield::BitRange,
/// The page index of a replace entry.
replace_page_range: bitfield::BitRange,
/// The byte index of a replace entry.
replace_byte_range: bitfield::BitRange,
/// The index of the page to erase.
///
/// This is only present for internal entries.
old_page_range: bitfield::BitRange,
/// The current erase count of the page to erase.
///
/// This is only present for internal entries.
saved_erase_count_range: bitfield::BitRange,
/// Whether a page is initialized.
///
/// - 0 for initialized pages.
/// - 1 for uninitialized pages.
initialized_bit: usize,
/// The erase count of a page.
erase_count_range: bitfield::BitRange,
/// Whether a page is being compacted.
///
/// - 0 for pages being compacted.
/// - 1 otherwise.
compacting_bit: usize,
/// The page index to which a page is being compacted.
new_page_range: bitfield::BitRange,
}
impl Format {
/// Returns a helper to parse the store format for a given storage and config.
///
/// # Errors
///
/// Returns `None` if any of the following conditions does not hold:
/// - The word size must be a power of two.
/// - The page size must be a power of two.
/// - There should be at least 2 pages in the storage.
/// - It should be possible to write a word at least twice.
/// - It should be possible to erase a page at least once.
/// - There should be at least 1 tag.
pub fn new<S: Storage, C: StoreConfig>(storage: &S, config: &C) -> Option<Format> {
let word_size = storage.word_size();
let page_size = storage.page_size();
let num_pages = storage.num_pages();
let max_word_writes = storage.max_word_writes();
let max_page_erases = storage.max_page_erases();
let num_tags = config.num_tags();
if !(word_size.is_power_of_two()
&& page_size.is_power_of_two()
&& num_pages > 1
&& max_word_writes >= 2
&& max_page_erases > 0
&& num_tags > 0)
{
return None;
}
// Compute how many bits we need to store the fields.
let page_bits = num_bits(num_pages);
let byte_bits = num_bits(page_size);
let tag_bits = num_bits(num_tags);
let erase_bits = num_bits(max_page_erases + 1);
// Compute the bit position of the fields.
let present_bit = 0;
let deleted_bit = present_bit + 1;
let internal_bit = deleted_bit + 1;
let replace_bit = internal_bit + 1;
let sensitive_bit = replace_bit + 1;
let length_range = bitfield::BitRange {
start: sensitive_bit + 1,
length: byte_bits,
};
let tag_range = bitfield::BitRange {
start: length_range.end(),
length: tag_bits,
};
let replace_page_range = bitfield::BitRange {
start: tag_range.end(),
length: page_bits,
};
let replace_byte_range = bitfield::BitRange {
start: replace_page_range.end(),
length: byte_bits,
};
let old_page_range = bitfield::BitRange {
start: internal_bit + 1,
length: page_bits,
};
let saved_erase_count_range = bitfield::BitRange {
start: old_page_range.end(),
length: erase_bits,
};
let initialized_bit = 0;
let erase_count_range = bitfield::BitRange {
start: initialized_bit + 1,
length: erase_bits,
};
let compacting_bit = erase_count_range.end();
let new_page_range = bitfield::BitRange {
start: compacting_bit + 1,
length: page_bits,
};
let format = Format {
word_size,
page_size,
num_pages,
max_page_erases,
num_tags,
present_bit,
deleted_bit,
internal_bit,
replace_bit,
sensitive_bit,
length_range,
tag_range,
replace_page_range,
replace_byte_range,
old_page_range,
saved_erase_count_range,
initialized_bit,
erase_count_range,
compacting_bit,
new_page_range,
};
// Make sure all the following conditions hold:
// - The page header is one word.
// - The internal entry is one word.
// - The entry header fits in one word (which is equivalent to the entry header size being
// exactly one word for sensitive entries).
if format.page_header_size() != word_size
|| format.internal_entry_size() != word_size
|| format.header_size(true) != word_size
{
return None;
}
Some(format)
}
/// Ensures a user entry is valid.
pub fn validate_entry(&self, entry: StoreEntry) -> Result<(), StoreError> {
if entry.tag >= self.num_tags {
return Err(StoreError::InvalidTag);
}
if entry.data.len() >= self.page_size {
return Err(StoreError::StoreFull);
}
Ok(())
}
/// Returns the entry header length in bytes.
///
/// This is the smallest number of bytes necessary to store all fields of the entry info up to
/// and including `length`. For sensitive entries, the result is word-aligned.
pub fn header_size(&self, sensitive: bool) -> usize {
let mut size = self.bits_to_bytes(self.length_range.end());
if sensitive {
// We need to align to the next word boundary so that wiping the user data will not
// count as a write to the header.
size = self.align_word(size);
}
size
}
/// Returns the entry header length in bytes.
///
/// This is a convenience function for `header_size` above.
fn header_offset(&self, entry: &[u8]) -> usize {
self.header_size(self.is_sensitive(entry))
}
/// Returns the entry info length in bytes.
///
/// This is the number of bytes necessary to store all fields of the entry info. This also
/// includes the internal padding to protect the `committed` bit from the `deleted` bit and to
/// protect the entry info from the user data for sensitive entries.
fn info_size(&self, is_replace: IsReplace, sensitive: bool) -> usize {
let suffix_bits = 2; // committed + complete
let info_bits = match is_replace {
IsReplace::Replace => self.replace_byte_range.end() + suffix_bits,
IsReplace::Insert => self.tag_range.end() + suffix_bits,
};
let mut info_size = self.bits_to_bytes(info_bits);
// If the suffix bits would end up in the header, we need to add one byte for them.
let header_size = self.header_size(sensitive);
if info_size <= header_size {
info_size = header_size + 1;
}
// If the entry is sensitive, we need to align to the next word boundary.
if sensitive {
info_size = self.align_word(info_size);
}
info_size
}
/// Returns the length in bytes of an entry.
///
/// This depends on the length of the user data and whether the entry replaces an old entry or
/// is an insertion. This also includes the internal padding to protect the `committed` bit from
/// the `deleted` bit.
pub fn entry_size(&self, is_replace: IsReplace, sensitive: bool, length: usize) -> usize {
let mut entry_size = length + self.info_size(is_replace, sensitive);
let word_size = self.word_size;
entry_size = self.align_word(entry_size);
// The entry must be at least 2 words such that the `committed` and `deleted` bits are on
// different words.
if entry_size == word_size {
entry_size += word_size;
}
entry_size
}
/// Returns the length in bytes of an internal entry.
pub fn internal_entry_size(&self) -> usize {
let length = self.bits_to_bytes(self.saved_erase_count_range.end());
self.align_word(length)
}
pub fn is_present(&self, header: &[u8]) -> bool {
bitfield::is_zero(self.present_bit, header, bitfield::NO_GAP)
}
pub fn set_present(&self, header: &mut [u8]) {
bitfield::set_zero(self.present_bit, header, bitfield::NO_GAP)
}
pub fn is_deleted(&self, header: &[u8]) -> bool {
bitfield::is_zero(self.deleted_bit, header, bitfield::NO_GAP)
}
/// Returns whether an entry is present and not deleted.
pub fn is_alive(&self, header: &[u8]) -> bool {
self.is_present(header) && !self.is_deleted(header)
}
pub fn set_deleted(&self, header: &mut [u8]) {
bitfield::set_zero(self.deleted_bit, header, bitfield::NO_GAP)
}
pub fn is_internal(&self, header: &[u8]) -> bool {
bitfield::is_zero(self.internal_bit, header, bitfield::NO_GAP)
}
pub fn set_internal(&self, header: &mut [u8]) {
bitfield::set_zero(self.internal_bit, header, bitfield::NO_GAP)
}
pub fn is_replace(&self, header: &[u8]) -> IsReplace {
if bitfield::is_zero(self.replace_bit, header, bitfield::NO_GAP) {
IsReplace::Replace
} else {
IsReplace::Insert
}
}
fn set_replace(&self, header: &mut [u8]) {
bitfield::set_zero(self.replace_bit, header, bitfield::NO_GAP)
}
pub fn is_sensitive(&self, header: &[u8]) -> bool {
bitfield::is_zero(self.sensitive_bit, header, bitfield::NO_GAP)
}
pub fn set_sensitive(&self, header: &mut [u8]) {
bitfield::set_zero(self.sensitive_bit, header, bitfield::NO_GAP)
}
pub fn get_length(&self, header: &[u8]) -> usize {
bitfield::get_range(self.length_range, header, bitfield::NO_GAP)
}
fn set_length(&self, header: &mut [u8], length: usize) {
bitfield::set_range(self.length_range, header, bitfield::NO_GAP, length)
}
pub fn get_data<'a>(&self, entry: &'a [u8]) -> &'a [u8] {
&entry[self.header_offset(entry)..][..self.get_length(entry)]
}
/// Returns the span of user data in an entry.
///
/// The complement of this gap in the entry is exactly the entry info. The header is before the
/// gap and the footer is after the gap.
pub fn entry_gap(&self, entry: &[u8]) -> bitfield::ByteGap {
let start = self.header_offset(entry);
let mut length = self.get_length(entry);
if self.is_sensitive(entry) {
length = self.align_word(length);
}
bitfield::ByteGap { start, length }
}
pub fn get_tag(&self, entry: &[u8]) -> usize {
bitfield::get_range(self.tag_range, entry, self.entry_gap(entry))
}
fn set_tag(&self, entry: &mut [u8], tag: usize) {
bitfield::set_range(self.tag_range, entry, self.entry_gap(entry), tag)
}
pub fn get_replace_index(&self, entry: &[u8]) -> Index {
let gap = self.entry_gap(entry);
let page = bitfield::get_range(self.replace_page_range, entry, gap);
let byte = bitfield::get_range(self.replace_byte_range, entry, gap);
Index { page, byte }
}
fn set_replace_page(&self, entry: &mut [u8], page: usize) {
bitfield::set_range(self.replace_page_range, entry, self.entry_gap(entry), page)
}
fn set_replace_byte(&self, entry: &mut [u8], byte: usize) {
bitfield::set_range(self.replace_byte_range, entry, self.entry_gap(entry), byte)
}
/// Returns the bit position of the `committed` bit.
///
/// This cannot be precomputed like other fields since it depends on the length of the entry.
fn committed_bit(&self, entry: &[u8]) -> usize {
8 * entry.len() - 2
}
/// Returns the bit position of the `complete` bit.
///
/// This cannot be precomputed like other fields since it depends on the length of the entry.
fn complete_bit(&self, entry: &[u8]) -> usize {
8 * entry.len() - 1
}
pub fn is_committed(&self, entry: &[u8]) -> bool {
bitfield::is_zero(self.committed_bit(entry), entry, bitfield::NO_GAP)
}
pub fn set_committed(&self, entry: &mut [u8]) {
bitfield::set_zero(self.committed_bit(entry), entry, bitfield::NO_GAP)
}
pub fn is_complete(&self, entry: &[u8]) -> bool {
bitfield::is_zero(self.complete_bit(entry), entry, bitfield::NO_GAP)
}
fn set_complete(&self, entry: &mut [u8]) {
bitfield::set_zero(self.complete_bit(entry), entry, bitfield::NO_GAP)
}
pub fn get_old_page(&self, header: &[u8]) -> usize {
bitfield::get_range(self.old_page_range, header, bitfield::NO_GAP)
}
pub fn set_old_page(&self, header: &mut [u8], old_page: usize) {
bitfield::set_range(self.old_page_range, header, bitfield::NO_GAP, old_page)
}
pub fn get_saved_erase_count(&self, header: &[u8]) -> usize {
bitfield::get_range(self.saved_erase_count_range, header, bitfield::NO_GAP)
}
pub fn set_saved_erase_count(&self, header: &mut [u8], erase_count: usize) {
bitfield::set_range(
self.saved_erase_count_range,
header,
bitfield::NO_GAP,
erase_count,
)
}
/// Builds an entry for replace or insert operations.
pub fn build_entry(&self, replace: Option<Index>, user_entry: StoreEntry) -> Vec<u8> {
let StoreEntry {
tag,
data,
sensitive,
} = user_entry;
let is_replace = match replace {
None => IsReplace::Insert,
Some(_) => IsReplace::Replace,
};
let entry_len = self.entry_size(is_replace, sensitive, data.len());
let mut entry = Vec::with_capacity(entry_len);
// Build the header.
entry.resize(self.header_size(sensitive), 0xff);
self.set_present(&mut entry[..]);
if sensitive {
self.set_sensitive(&mut entry[..]);
}
self.set_length(&mut entry[..], data.len());
// Add the data.
entry.extend_from_slice(data);
// Build the footer.
entry.resize(entry_len, 0xff);
self.set_tag(&mut entry[..], tag);
self.set_complete(&mut entry[..]);
match replace {
None => self.set_committed(&mut entry[..]),
Some(Index { page, byte }) => {
self.set_replace(&mut entry[..]);
self.set_replace_page(&mut entry[..], page);
self.set_replace_byte(&mut entry[..], byte);
}
}
entry
}
/// Builds an entry for replace or insert operations.
pub fn build_erase_entry(&self, old_page: usize, saved_erase_count: usize) -> Vec<u8> {
let mut entry = vec![0xff; self.internal_entry_size()];
self.set_present(&mut entry[..]);
self.set_internal(&mut entry[..]);
self.set_old_page(&mut entry[..], old_page);
self.set_saved_erase_count(&mut entry[..], saved_erase_count);
entry
}
/// Returns the length in bytes of a page header entry.
///
/// This includes the word padding.
pub fn page_header_size(&self) -> usize {
self.align_word(self.bits_to_bytes(self.erase_count_range.end()))
}
pub fn is_initialized(&self, header: &[u8]) -> bool {
bitfield::is_zero(self.initialized_bit, header, bitfield::NO_GAP)
}
pub fn set_initialized(&self, header: &mut [u8]) {
bitfield::set_zero(self.initialized_bit, header, bitfield::NO_GAP)
}
pub fn get_erase_count(&self, header: &[u8]) -> usize {
bitfield::get_range(self.erase_count_range, header, bitfield::NO_GAP)
}
pub fn set_erase_count(&self, header: &mut [u8], count: usize) {
bitfield::set_range(self.erase_count_range, header, bitfield::NO_GAP, count)
}
pub fn is_compacting(&self, header: &[u8]) -> bool {
bitfield::is_zero(self.compacting_bit, header, bitfield::NO_GAP)
}
pub fn set_compacting(&self, header: &mut [u8]) {
bitfield::set_zero(self.compacting_bit, header, bitfield::NO_GAP)
}
pub fn get_new_page(&self, header: &[u8]) -> usize {
bitfield::get_range(self.new_page_range, header, bitfield::NO_GAP)
}
pub fn set_new_page(&self, header: &mut [u8], new_page: usize) {
bitfield::set_range(self.new_page_range, header, bitfield::NO_GAP, new_page)
}
/// Returns the smallest word boundary greater or equal to a value.
fn align_word(&self, value: usize) -> usize {
let word_size = self.word_size;
(value + word_size - 1) / word_size * word_size
}
/// Returns the minimum number of bytes to represent a given number of bits.
fn bits_to_bytes(&self, bits: usize) -> usize {
(bits + 7) / 8
}
}
/// Returns the number of bits necessary to write numbers smaller than `x`.
fn num_bits(x: usize) -> usize {
x.next_power_of_two().trailing_zeros() as usize
}
#[test]
fn num_bits_ok() {
assert_eq!(num_bits(0), 0);
assert_eq!(num_bits(1), 0);
assert_eq!(num_bits(2), 1);
assert_eq!(num_bits(3), 2);
assert_eq!(num_bits(4), 2);
assert_eq!(num_bits(5), 3);
assert_eq!(num_bits(8), 3);
assert_eq!(num_bits(9), 4);
assert_eq!(num_bits(16), 4);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2019 Google LLC
// Copyright 2019-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.
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::{Index, Storage, StorageError, StorageResult};
use alloc::vec::Vec;
use libtock_core::syscalls;
use persistent_store::{Storage, StorageError, StorageIndex, StorageResult};
const DRIVER_NUMBER: usize = 0x50003;
@@ -42,15 +42,13 @@ mod memop_nr {
fn get_info(nr: usize, arg: usize) -> StorageResult<usize> {
let code = syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, arg);
code.map_err(|e| StorageError::KernelError {
code: e.return_code,
})
code.map_err(|_| StorageError::CustomError)
}
fn memop(nr: u32, arg: usize) -> StorageResult<usize> {
let code = unsafe { syscalls::raw::memop(nr, arg) };
if code < 0 {
Err(StorageError::KernelError { code })
Err(StorageError::CustomError)
} else {
Ok(code as usize)
}
@@ -70,7 +68,7 @@ impl SyscallStorage {
///
/// # Errors
///
/// Returns `BadFlash` if any of the following conditions do not hold:
/// Returns `CustomError` if any of the following conditions do not hold:
/// - The word size is a power of two.
/// - The page size is a power of two.
/// - The page size is a multiple of the word size.
@@ -90,13 +88,13 @@ impl SyscallStorage {
|| !syscall.page_size.is_power_of_two()
|| !syscall.is_word_aligned(syscall.page_size)
{
return Err(StorageError::BadFlash);
return Err(StorageError::CustomError);
}
for i in 0..memop(memop_nr::STORAGE_CNT, 0)? {
let storage_ptr = memop(memop_nr::STORAGE_PTR, i)?;
let max_storage_len = memop(memop_nr::STORAGE_LEN, i)?;
if !syscall.is_page_aligned(storage_ptr) || !syscall.is_page_aligned(max_storage_len) {
return Err(StorageError::BadFlash);
return Err(StorageError::CustomError);
}
let storage_len = core::cmp::min(num_pages * syscall.page_size, max_storage_len);
num_pages -= storage_len / syscall.page_size;
@@ -141,12 +139,12 @@ impl Storage for SyscallStorage {
self.max_page_erases
}
fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]> {
fn read_slice(&self, index: StorageIndex, length: usize) -> StorageResult<&[u8]> {
let start = index.range(length, self)?.start;
find_slice(&self.storage_locations, start, length)
}
fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()> {
fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()> {
if !self.is_word_aligned(index.byte) || !self.is_word_aligned(value.len()) {
return Err(StorageError::NotAligned);
}
@@ -163,28 +161,24 @@ impl Storage for SyscallStorage {
)
};
if code < 0 {
return Err(StorageError::KernelError { code });
return Err(StorageError::CustomError);
}
let code = syscalls::command(DRIVER_NUMBER, command_nr::WRITE_SLICE, ptr, value.len());
if let Err(e) = code {
return Err(StorageError::KernelError {
code: e.return_code,
});
if code.is_err() {
return Err(StorageError::CustomError);
}
Ok(())
}
fn erase_page(&mut self, page: usize) -> StorageResult<()> {
let index = Index { page, byte: 0 };
let index = StorageIndex { page, byte: 0 };
let length = self.page_size();
let ptr = self.read_slice(index, length)?.as_ptr() as usize;
let code = syscalls::command(DRIVER_NUMBER, command_nr::ERASE_PAGE, ptr, length);
if let Err(e) = code {
return Err(StorageError::KernelError {
code: e.return_code,
});
if code.is_err() {
return Err(StorageError::CustomError);
}
Ok(())
}

View File

@@ -18,3 +18,6 @@ extern crate alloc;
pub mod ctap;
pub mod embedded_flash;
#[macro_use]
extern crate arrayref;

View File

@@ -18,6 +18,9 @@ extern crate alloc;
#[cfg(feature = "std")]
extern crate core;
extern crate lang_items;
#[macro_use]
extern crate arrayref;
extern crate byteorder;
mod ctap;
pub mod embedded_flash;
@@ -37,9 +40,11 @@ use libtock_drivers::console::Console;
use libtock_drivers::led;
use libtock_drivers::result::{FlexUnwrap, TockError};
use libtock_drivers::timer;
use libtock_drivers::timer::Duration;
#[cfg(feature = "debug_ctap")]
use libtock_drivers::timer::Timer;
use libtock_drivers::timer::{Duration, Timestamp};
#[cfg(feature = "debug_ctap")]
use libtock_drivers::timer::Timestamp;
use libtock_drivers::usb_ctap_hid;
const KEEPALIVE_DELAY_MS: isize = 100;
@@ -57,12 +62,13 @@ fn main() {
panic!("Cannot setup USB driver");
}
let boot_time = timer.get_current_clock().flex_unwrap();
let mut rng = TockRng256 {};
let mut ctap_state = CtapState::new(&mut rng, check_user_presence);
let mut ctap_state = CtapState::new(&mut rng, check_user_presence, boot_time);
let mut ctap_hid = CtapHid::new();
let mut led_counter = 0;
let mut last_led_increment = timer.get_current_clock().flex_unwrap();
let mut last_led_increment = boot_time;
// Main loop. If CTAP1 is used, we register button presses for U2F while receiving and waiting.
// The way TockOS and apps currently interact, callbacks need a yield syscall to execute,
@@ -114,8 +120,8 @@ fn main() {
}
// These calls are making sure that even for long inactivity, wrapping clock values
// never randomly wink or grant user presence for U2F.
ctap_state.check_disable_reset(Timestamp::<isize>::from_clock_value(now));
// don't cause problems with timers.
ctap_state.update_timeouts(now);
ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now);
if has_packet {

View File

@@ -11,7 +11,7 @@ edition = "2018"
[dependencies]
libtock_core = { path = "../../third_party/libtock-rs/core", default-features = false, features = ["alloc_init", "custom_panic_handler", "custom_alloc_error_handler"] }
libtock_drivers = { path = "../libtock-drivers" }
linked_list_allocator = { version = "0.8.1", default-features = false }
linked_list_allocator = { version = "0.8.7", default-features = false, features = ["const_mut_refs"] }
[features]
debug_allocations = []

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

@@ -0,0 +1,52 @@
use crate::result::TockResult;
use libtock_core::syscalls;
const DRIVER_NUMBER: usize = 0x00008;
mod command_nr {
pub const AVAILABLE: usize = 0;
pub const GET_PROTECTION: usize = 1;
pub const SET_PROTECTION: usize = 2;
}
#[derive(PartialOrd, PartialEq)]
pub enum ProtectionLevel {
/// Unsupported feature
Unknown = 0,
/// This should be the factory default for the chip.
NoProtection = 1,
/// At this level, only JTAG/SWD are disabled but other debugging
/// features may still be enabled.
JtagDisabled = 2,
/// This is the maximum level of protection the chip supports.
/// At this level, JTAG and all other features are expected to be
/// disabled and only a full chip erase may allow to recover from
/// that state.
FullyLocked = 0xff,
}
impl From<usize> for ProtectionLevel {
fn from(value: usize) -> Self {
match value {
1 => ProtectionLevel::NoProtection,
2 => ProtectionLevel::JtagDisabled,
0xff => ProtectionLevel::FullyLocked,
_ => ProtectionLevel::Unknown,
}
}
}
pub fn is_available() -> TockResult<()> {
syscalls::command(DRIVER_NUMBER, command_nr::AVAILABLE, 0, 0)?;
Ok(())
}
pub fn get_protection() -> TockResult<ProtectionLevel> {
let current_level = syscalls::command(DRIVER_NUMBER, command_nr::GET_PROTECTION, 0, 0)?;
Ok(current_level.into())
}
pub fn set_protection(level: ProtectionLevel) -> TockResult<()> {
syscalls::command(DRIVER_NUMBER, command_nr::SET_PROTECTION, level as usize, 0)?;
Ok(())
}

View File

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

204
tools/configure.py Executable file
View File

@@ -0,0 +1,204 @@
#!/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.vid, dev.descriptor.pid) == 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.utcnow()
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 OpenSK device AAGUID {} ({}).".format(
aaguid, authenticator.device))
info("Please touch the device to confirm...")
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())