Merge branch 'develop' into usize_32_or_std
This commit is contained in:
22
.github/workflows/cargo_check.yml
vendored
22
.github/workflows/cargo_check.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/crypto_test.yml
vendored
4
.github/workflows/crypto_test.yml
vendored
@@ -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
|
||||
|
||||
24
.github/workflows/opensk_test.yml
vendored
24
.github/workflows/opensk_test.yml
vendored
@@ -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
|
||||
|
||||
|
||||
5
.github/workflows/persistent_store_test.yml
vendored
5
.github/workflows/persistent_store_test.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -11,3 +11,4 @@ Cargo.lock
|
||||
/reproducible/binaries.sha256sum
|
||||
/reproducible/elf2tab.txt
|
||||
/reproducible/reproduced.tar
|
||||
__pycache__
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -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
|
||||
|
||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"davidanson.vscode-markdownlint",
|
||||
"rust-lang.rust",
|
||||
"ms-python.python"
|
||||
]
|
||||
}
|
||||
27
.vscode/settings.json
vendored
27
.vscode/settings.json
vendored
@@ -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
|
||||
},
|
||||
}
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -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
57
OpenSK.code-workspace
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
98
README.md
98
README.md
@@ -1,6 +1,5 @@
|
||||
# <img alt="OpenSK logo" src="docs/img/OpenSK.svg" width="200px">
|
||||
|
||||
[](https://travis-ci.org/google/OpenSK)
|
||||

|
||||

|
||||

|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,7 @@ static STRINGS: &'static [&'static str] = &[
|
||||
// Product
|
||||
"OpenSK",
|
||||
// Serial number
|
||||
"v0.1",
|
||||
"v1.0",
|
||||
];
|
||||
|
||||
// State for loading and holding applications.
|
||||
|
||||
59
build.rs
59
build.rs
@@ -12,11 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use openssl::asn1;
|
||||
use openssl::ec;
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::x509;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
@@ -25,65 +20,11 @@ use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=crypto_data/opensk.key");
|
||||
println!("cargo:rerun-if-changed=crypto_data/opensk_cert.pem");
|
||||
println!("cargo:rerun-if-changed=crypto_data/aaguid.txt");
|
||||
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
let priv_key_bin_path = Path::new(&out_dir).join("opensk_pkey.bin");
|
||||
let cert_bin_path = Path::new(&out_dir).join("opensk_cert.bin");
|
||||
let aaguid_bin_path = Path::new(&out_dir).join("opensk_aaguid.bin");
|
||||
|
||||
// Load the OpenSSL PEM ECC key
|
||||
let ecc_data = include_bytes!("crypto_data/opensk.key");
|
||||
let pkey =
|
||||
ec::EcKey::private_key_from_pem(ecc_data).expect("Failed to load OpenSK private key file");
|
||||
|
||||
// Check key validity
|
||||
pkey.check_key().unwrap();
|
||||
assert_eq!(pkey.group().curve_name(), Some(Nid::X9_62_PRIME256V1));
|
||||
|
||||
// Private keys generated by OpenSSL have variable size but we only handle
|
||||
// constant size. Serialization is done in big endian so if the size is less
|
||||
// than 32 bytes, we need to prepend with null bytes.
|
||||
// If the size is 33 bytes, this means the serialized BigInt is negative.
|
||||
// Any other size is invalid.
|
||||
let priv_key_hex = pkey.private_key().to_hex_str().unwrap();
|
||||
let priv_key_vec = pkey.private_key().to_vec();
|
||||
let key_len = priv_key_vec.len();
|
||||
|
||||
assert!(
|
||||
key_len <= 33,
|
||||
"Invalid private key (too big): {} ({:#?})",
|
||||
priv_key_hex,
|
||||
priv_key_vec,
|
||||
);
|
||||
|
||||
// Copy OpenSSL generated key to our vec, starting from the end
|
||||
let mut output_vec = [0u8; 32];
|
||||
let min_key_len = std::cmp::min(key_len, 32);
|
||||
output_vec[32 - min_key_len..].copy_from_slice(&priv_key_vec[key_len - min_key_len..]);
|
||||
|
||||
// Create the raw private key out of the OpenSSL data
|
||||
let mut priv_key_bin_file = File::create(&priv_key_bin_path).unwrap();
|
||||
priv_key_bin_file.write_all(&output_vec).unwrap();
|
||||
|
||||
// Convert the PEM certificate to DER and extract the serial for AAGUID
|
||||
let input_pem_cert = include_bytes!("crypto_data/opensk_cert.pem");
|
||||
let cert = x509::X509::from_pem(input_pem_cert).expect("Failed to load OpenSK certificate");
|
||||
|
||||
// Do some sanity check on the certificate
|
||||
assert!(cert
|
||||
.public_key()
|
||||
.unwrap()
|
||||
.public_eq(&PKey::from_ec_key(pkey).unwrap()));
|
||||
let now = asn1::Asn1Time::days_from_now(0).unwrap();
|
||||
assert!(cert.not_after() > now);
|
||||
assert!(cert.not_before() <= now);
|
||||
|
||||
let mut cert_bin_file = File::create(&cert_bin_path).unwrap();
|
||||
cert_bin_file.write_all(&cert.to_der().unwrap()).unwrap();
|
||||
|
||||
let mut aaguid_bin_file = File::create(&aaguid_bin_path).unwrap();
|
||||
let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap();
|
||||
let mut content = String::new();
|
||||
|
||||
80
deploy.py
80
deploy.py
@@ -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",
|
||||
|
||||
BIN
docs/FIDO2 Certificate Google FIDO20020210209001.pdf
Normal file
BIN
docs/FIDO2 Certificate Google FIDO20020210209001.pdf
Normal file
Binary file not shown.
BIN
docs/img/FIDO2_Certified_L1.png
Normal file
BIN
docs/img/FIDO2_Certified_L1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -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
53
examples/erase_storage.rs
Normal 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
138
examples/store_latency.rs
Normal 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 |
|
||||
}
|
||||
@@ -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'] }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
226
libraries/crypto/src/hkdf.rs
Normal file
226
libraries/crypto/src/hkdf.rs
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
116
libraries/persistent_store/fuzz/examples/store.rs
Normal file
116
libraries/persistent_store/fuzz/examples/store.rs
Normal 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]);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
426
libraries/persistent_store/fuzz/src/store.rs
Normal file
426
libraries/persistent_store/fuzz/src/store.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
345
libraries/persistent_store/src/fragment.rs
Normal file
345
libraries/persistent_store/src/fragment.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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::{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<()>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
84
libraries/persistent_store/src/test.rs
Normal file
84
libraries/persistent_store/src/test.rs
Normal 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
46
metadata/metadata.json
Normal 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
|
||||
}
|
||||
@@ -117,7 +117,7 @@ index d72d20482..118ea6d68 100644
|
||||
+ // Product
|
||||
+ "OpenSK",
|
||||
+ // Serial number
|
||||
+ "v0.1",
|
||||
+ "v1.0",
|
||||
+];
|
||||
+
|
||||
// State for loading and holding applications.
|
||||
@@ -189,7 +189,7 @@ index 2ebb384d8..4a7bfffdd 100644
|
||||
+ // Product
|
||||
+ "OpenSK",
|
||||
+ // Serial number
|
||||
+ "v0.1",
|
||||
+ "v1.0",
|
||||
+];
|
||||
+
|
||||
// State for loading and holding applications.
|
||||
|
||||
100
patches/tock/06-update-uicr.patch
Normal file
100
patches/tock/06-update-uicr.patch
Normal file
@@ -0,0 +1,100 @@
|
||||
diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs
|
||||
index 6bb6c86..3bb8b5a 100644
|
||||
--- a/chips/nrf52/src/uicr.rs
|
||||
+++ b/chips/nrf52/src/uicr.rs
|
||||
@@ -1,38 +1,45 @@
|
||||
//! User information configuration registers
|
||||
-//!
|
||||
-//! Minimal implementation to support activation of the reset button on
|
||||
-//! nRF52-DK.
|
||||
+
|
||||
|
||||
use enum_primitive::cast::FromPrimitive;
|
||||
-use kernel::common::registers::{register_bitfields, ReadWrite};
|
||||
+use kernel::common::registers::{register_bitfields, register_structs, ReadWrite};
|
||||
use kernel::common::StaticRef;
|
||||
+use kernel::hil;
|
||||
+use kernel::ReturnCode;
|
||||
|
||||
use crate::gpio::Pin;
|
||||
|
||||
const UICR_BASE: StaticRef<UicrRegisters> =
|
||||
- unsafe { StaticRef::new(0x10001200 as *const UicrRegisters) };
|
||||
-
|
||||
-#[repr(C)]
|
||||
-struct UicrRegisters {
|
||||
- /// Mapping of the nRESET function (see POWER chapter for details)
|
||||
- /// - Address: 0x200 - 0x204
|
||||
- pselreset0: ReadWrite<u32, Pselreset::Register>,
|
||||
- /// Mapping of the nRESET function (see POWER chapter for details)
|
||||
- /// - Address: 0x204 - 0x208
|
||||
- pselreset1: ReadWrite<u32, Pselreset::Register>,
|
||||
- /// Access Port protection
|
||||
- /// - Address: 0x208 - 0x20c
|
||||
- approtect: ReadWrite<u32, ApProtect::Register>,
|
||||
- /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
|
||||
- /// - Address: 0x20c - 0x210
|
||||
- nfcpins: ReadWrite<u32, NfcPins::Register>,
|
||||
- _reserved1: [u32; 60],
|
||||
- /// External circuitry to be supplied from VDD pin.
|
||||
- /// - Address: 0x300 - 0x304
|
||||
- extsupply: ReadWrite<u32, ExtSupply::Register>,
|
||||
- /// GPIO reference voltage
|
||||
- /// - Address: 0x304 - 0x308
|
||||
- regout0: ReadWrite<u32, RegOut::Register>,
|
||||
+ unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) };
|
||||
+
|
||||
+register_structs! {
|
||||
+ UicrRegisters {
|
||||
+ (0x000 => _reserved1),
|
||||
+ /// Reserved for Nordic firmware design
|
||||
+ (0x014 => nrffw: [ReadWrite<u32>; 13]),
|
||||
+ (0x048 => _reserved2),
|
||||
+ /// Reserved for Nordic hardware design
|
||||
+ (0x050 => nrfhw: [ReadWrite<u32>; 12]),
|
||||
+ /// Reserved for customer
|
||||
+ (0x080 => customer: [ReadWrite<u32>; 32]),
|
||||
+ (0x100 => _reserved3),
|
||||
+ /// Mapping of the nRESET function (see POWER chapter for details)
|
||||
+ (0x200 => pselreset0: ReadWrite<u32, Pselreset::Register>),
|
||||
+ /// Mapping of the nRESET function (see POWER chapter for details)
|
||||
+ (0x204 => pselreset1: ReadWrite<u32, Pselreset::Register>),
|
||||
+ /// Access Port protection
|
||||
+ (0x208 => approtect: ReadWrite<u32, ApProtect::Register>),
|
||||
+ /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
|
||||
+ /// - Address: 0x20c - 0x210
|
||||
+ (0x20c => nfcpins: ReadWrite<u32, NfcPins::Register>),
|
||||
+ (0x210 => debugctrl: ReadWrite<u32, DebugControl::Register>),
|
||||
+ (0x214 => _reserved4),
|
||||
+ /// External circuitry to be supplied from VDD pin.
|
||||
+ (0x300 => extsupply: ReadWrite<u32, ExtSupply::Register>),
|
||||
+ /// GPIO reference voltage
|
||||
+ (0x304 => regout0: ReadWrite<u32, RegOut::Register>),
|
||||
+ (0x308 => @END),
|
||||
+ }
|
||||
}
|
||||
|
||||
register_bitfields! [u32,
|
||||
@@ -58,6 +65,21 @@ register_bitfields! [u32,
|
||||
DISABLED = 0xff
|
||||
]
|
||||
],
|
||||
+ /// Processor debug control
|
||||
+ DebugControl [
|
||||
+ CPUNIDEN OFFSET(0) NUMBITS(8) [
|
||||
+ /// Enable
|
||||
+ ENABLED = 0xff,
|
||||
+ /// Disable
|
||||
+ DISABLED = 0x00
|
||||
+ ],
|
||||
+ CPUFPBEN OFFSET(8) NUMBITS(8) [
|
||||
+ /// Enable
|
||||
+ ENABLED = 0xff,
|
||||
+ /// Disable
|
||||
+ DISABLED = 0x00
|
||||
+ ]
|
||||
+ ],
|
||||
/// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO
|
||||
NfcPins [
|
||||
/// Setting pins dedicated to NFC functionality
|
||||
|
||||
426
patches/tock/07-firmware-protect.patch
Normal file
426
patches/tock/07-firmware-protect.patch
Normal file
@@ -0,0 +1,426 @@
|
||||
diff --git a/boards/components/src/firmware_protection.rs b/boards/components/src/firmware_protection.rs
|
||||
new file mode 100644
|
||||
index 0000000..58695af
|
||||
--- /dev/null
|
||||
+++ b/boards/components/src/firmware_protection.rs
|
||||
@@ -0,0 +1,70 @@
|
||||
+//! Component for firmware protection syscall interface.
|
||||
+//!
|
||||
+//! This provides one Component, `FirmwareProtectionComponent`, which implements a
|
||||
+//! userspace syscall interface to enable the code readout protection.
|
||||
+//!
|
||||
+//! Usage
|
||||
+//! -----
|
||||
+//! ```rust
|
||||
+//! let crp = components::firmware_protection::FirmwareProtectionComponent::new(
|
||||
+//! board_kernel,
|
||||
+//! nrf52840::uicr::Uicr::new()
|
||||
+//! )
|
||||
+//! .finalize(
|
||||
+//! components::firmware_protection_component_helper!(uicr));
|
||||
+//! ```
|
||||
+
|
||||
+use core::mem::MaybeUninit;
|
||||
+
|
||||
+use capsules::firmware_protection;
|
||||
+use kernel::capabilities;
|
||||
+use kernel::component::Component;
|
||||
+use kernel::create_capability;
|
||||
+use kernel::hil;
|
||||
+use kernel::static_init_half;
|
||||
+
|
||||
+// Setup static space for the objects.
|
||||
+#[macro_export]
|
||||
+macro_rules! firmware_protection_component_helper {
|
||||
+ ($C:ty) => {{
|
||||
+ use capsules::firmware_protection;
|
||||
+ use core::mem::MaybeUninit;
|
||||
+ static mut BUF: MaybeUninit<firmware_protection::FirmwareProtection<$C>> =
|
||||
+ MaybeUninit::uninit();
|
||||
+ &mut BUF
|
||||
+ };};
|
||||
+}
|
||||
+
|
||||
+pub struct FirmwareProtectionComponent<C: hil::firmware_protection::FirmwareProtection> {
|
||||
+ board_kernel: &'static kernel::Kernel,
|
||||
+ crp: C,
|
||||
+}
|
||||
+
|
||||
+impl<C: 'static + hil::firmware_protection::FirmwareProtection> FirmwareProtectionComponent<C> {
|
||||
+ pub fn new(board_kernel: &'static kernel::Kernel, crp: C) -> FirmwareProtectionComponent<C> {
|
||||
+ FirmwareProtectionComponent {
|
||||
+ board_kernel: board_kernel,
|
||||
+ crp: crp,
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl<C: 'static + hil::firmware_protection::FirmwareProtection> Component
|
||||
+ for FirmwareProtectionComponent<C>
|
||||
+{
|
||||
+ type StaticInput = &'static mut MaybeUninit<firmware_protection::FirmwareProtection<C>>;
|
||||
+ type Output = &'static firmware_protection::FirmwareProtection<C>;
|
||||
+
|
||||
+ unsafe fn finalize(self, static_buffer: Self::StaticInput) -> Self::Output {
|
||||
+ let grant_cap = create_capability!(capabilities::MemoryAllocationCapability);
|
||||
+
|
||||
+ static_init_half!(
|
||||
+ static_buffer,
|
||||
+ firmware_protection::FirmwareProtection<C>,
|
||||
+ firmware_protection::FirmwareProtection::new(
|
||||
+ self.crp,
|
||||
+ self.board_kernel.create_grant(&grant_cap),
|
||||
+ )
|
||||
+ )
|
||||
+ }
|
||||
+}
|
||||
diff --git a/boards/components/src/lib.rs b/boards/components/src/lib.rs
|
||||
index 917497a..520408f 100644
|
||||
--- a/boards/components/src/lib.rs
|
||||
+++ b/boards/components/src/lib.rs
|
||||
@@ -9,6 +9,7 @@ pub mod console;
|
||||
pub mod crc;
|
||||
pub mod debug_queue;
|
||||
pub mod debug_writer;
|
||||
+pub mod firmware_protection;
|
||||
pub mod ft6x06;
|
||||
pub mod gpio;
|
||||
pub mod hd44780;
|
||||
diff --git a/boards/nordic/nrf52840_dongle/src/main.rs b/boards/nordic/nrf52840_dongle/src/main.rs
|
||||
index 118ea6d..76436f3 100644
|
||||
--- a/boards/nordic/nrf52840_dongle/src/main.rs
|
||||
+++ b/boards/nordic/nrf52840_dongle/src/main.rs
|
||||
@@ -112,6 +112,7 @@ pub struct Platform {
|
||||
'static,
|
||||
nrf52840::usbd::Usbd<'static>,
|
||||
>,
|
||||
+ crp: &'static capsules::firmware_protection::FirmwareProtection<nrf52840::uicr::Uicr>,
|
||||
}
|
||||
|
||||
impl kernel::Platform for Platform {
|
||||
@@ -132,6 +133,7 @@ impl kernel::Platform for Platform {
|
||||
capsules::analog_comparator::DRIVER_NUM => f(Some(self.analog_comparator)),
|
||||
nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)),
|
||||
capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)),
|
||||
+ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)),
|
||||
kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)),
|
||||
_ => f(None),
|
||||
}
|
||||
@@ -355,6 +357,14 @@ pub unsafe fn reset_handler() {
|
||||
)
|
||||
.finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd));
|
||||
|
||||
+ let crp = components::firmware_protection::FirmwareProtectionComponent::new(
|
||||
+ board_kernel,
|
||||
+ nrf52840::uicr::Uicr::new(),
|
||||
+ )
|
||||
+ .finalize(components::firmware_protection_component_helper!(
|
||||
+ nrf52840::uicr::Uicr
|
||||
+ ));
|
||||
+
|
||||
nrf52_components::NrfClockComponent::new().finalize(());
|
||||
|
||||
let platform = Platform {
|
||||
@@ -371,6 +381,7 @@ pub unsafe fn reset_handler() {
|
||||
analog_comparator,
|
||||
nvmc,
|
||||
usb,
|
||||
+ crp,
|
||||
ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability),
|
||||
};
|
||||
|
||||
diff --git a/boards/nordic/nrf52840dk/src/main.rs b/boards/nordic/nrf52840dk/src/main.rs
|
||||
index b1d0d3c..3cfb38d 100644
|
||||
--- a/boards/nordic/nrf52840dk/src/main.rs
|
||||
+++ b/boards/nordic/nrf52840dk/src/main.rs
|
||||
@@ -180,6 +180,7 @@ pub struct Platform {
|
||||
'static,
|
||||
nrf52840::usbd::Usbd<'static>,
|
||||
>,
|
||||
+ crp: &'static capsules::firmware_protection::FirmwareProtection<nrf52840::uicr::Uicr>,
|
||||
}
|
||||
|
||||
impl kernel::Platform for Platform {
|
||||
@@ -201,6 +202,7 @@ impl kernel::Platform for Platform {
|
||||
capsules::nonvolatile_storage_driver::DRIVER_NUM => f(Some(self.nonvolatile_storage)),
|
||||
nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)),
|
||||
capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)),
|
||||
+ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)),
|
||||
kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)),
|
||||
_ => f(None),
|
||||
}
|
||||
@@ -480,6 +482,14 @@ pub unsafe fn reset_handler() {
|
||||
)
|
||||
.finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd));
|
||||
|
||||
+ let crp = components::firmware_protection::FirmwareProtectionComponent::new(
|
||||
+ board_kernel,
|
||||
+ nrf52840::uicr::Uicr::new(),
|
||||
+ )
|
||||
+ .finalize(components::firmware_protection_component_helper!(
|
||||
+ nrf52840::uicr::Uicr
|
||||
+ ));
|
||||
+
|
||||
nrf52_components::NrfClockComponent::new().finalize(());
|
||||
|
||||
let platform = Platform {
|
||||
@@ -497,6 +507,7 @@ pub unsafe fn reset_handler() {
|
||||
nonvolatile_storage,
|
||||
nvmc,
|
||||
usb,
|
||||
+ crp,
|
||||
ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability),
|
||||
};
|
||||
|
||||
diff --git a/capsules/src/driver.rs b/capsules/src/driver.rs
|
||||
index ae458b3..f536dad 100644
|
||||
--- a/capsules/src/driver.rs
|
||||
+++ b/capsules/src/driver.rs
|
||||
@@ -16,6 +16,7 @@ pub enum NUM {
|
||||
Adc = 0x00005,
|
||||
Dac = 0x00006,
|
||||
AnalogComparator = 0x00007,
|
||||
+ FirmwareProtection = 0x00008,
|
||||
|
||||
// Kernel
|
||||
Ipc = 0x10000,
|
||||
diff --git a/capsules/src/firmware_protection.rs b/capsules/src/firmware_protection.rs
|
||||
new file mode 100644
|
||||
index 0000000..8cf63d6
|
||||
--- /dev/null
|
||||
+++ b/capsules/src/firmware_protection.rs
|
||||
@@ -0,0 +1,85 @@
|
||||
+//! Provides userspace control of firmware protection on a board.
|
||||
+//!
|
||||
+//! This allows an application to enable firware readout protection,
|
||||
+//! disabling JTAG interface and other ways to read/tamper the firmware.
|
||||
+//! Of course, outside of a hardware bug, once set, the only way to enable
|
||||
+//! programming/debugging is by fully erasing the flash.
|
||||
+//!
|
||||
+//! Usage
|
||||
+//! -----
|
||||
+//!
|
||||
+//! ```rust
|
||||
+//! # use kernel::static_init;
|
||||
+//!
|
||||
+//! let crp = static_init!(
|
||||
+//! capsules::firmware_protection::FirmwareProtection,
|
||||
+//! capsules::firmware_protection::FirmwareProtection::new(
|
||||
+//! nrf52840::uicr::Uicr,
|
||||
+//! board_kernel.create_grant(&grant_cap),
|
||||
+//! );
|
||||
+//! ```
|
||||
+//!
|
||||
+//! Syscall Interface
|
||||
+//! -----------------
|
||||
+//!
|
||||
+//! - Stability: 0 - Draft
|
||||
+//!
|
||||
+//! ### Command
|
||||
+//!
|
||||
+//! Enable code readout protection on the current board.
|
||||
+//!
|
||||
+//! #### `command_num`
|
||||
+//!
|
||||
+//! - `0`: Driver check.
|
||||
+//! - `1`: Get current firmware readout protection (aka CRP) state.
|
||||
+//! - `2`: Set current firmware readout protection (aka CRP) state.
|
||||
+//!
|
||||
+
|
||||
+use kernel::hil;
|
||||
+use kernel::{AppId, Callback, Driver, Grant, ReturnCode};
|
||||
+
|
||||
+/// Syscall driver number.
|
||||
+use crate::driver;
|
||||
+pub const DRIVER_NUM: usize = driver::NUM::FirmwareProtection as usize;
|
||||
+
|
||||
+pub struct FirmwareProtection<C: hil::firmware_protection::FirmwareProtection> {
|
||||
+ crp_unit: C,
|
||||
+ apps: Grant<Option<Callback>>,
|
||||
+}
|
||||
+
|
||||
+impl<C: hil::firmware_protection::FirmwareProtection> FirmwareProtection<C> {
|
||||
+ pub fn new(crp_unit: C, apps: Grant<Option<Callback>>) -> Self {
|
||||
+ Self { crp_unit, apps }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl<C: hil::firmware_protection::FirmwareProtection> Driver for FirmwareProtection<C> {
|
||||
+ ///
|
||||
+ /// ### Command numbers
|
||||
+ ///
|
||||
+ /// * `0`: Returns non-zero to indicate the driver is present.
|
||||
+ /// * `1`: Gets firmware protection state.
|
||||
+ /// * `2`: Sets firmware protection state.
|
||||
+ fn command(&self, command_num: usize, data: usize, _: usize, appid: AppId) -> ReturnCode {
|
||||
+ match command_num {
|
||||
+ // return if driver is available
|
||||
+ 0 => ReturnCode::SUCCESS,
|
||||
+
|
||||
+ 1 => self
|
||||
+ .apps
|
||||
+ .enter(appid, |_, _| ReturnCode::SuccessWithValue {
|
||||
+ value: self.crp_unit.get_protection() as usize,
|
||||
+ })
|
||||
+ .unwrap_or_else(|err| err.into()),
|
||||
+
|
||||
+ // sets firmware protection
|
||||
+ 2 => self
|
||||
+ .apps
|
||||
+ .enter(appid, |_, _| self.crp_unit.set_protection(data.into()))
|
||||
+ .unwrap_or_else(|err| err.into()),
|
||||
+
|
||||
+ // default
|
||||
+ _ => ReturnCode::ENOSUPPORT,
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/capsules/src/lib.rs b/capsules/src/lib.rs
|
||||
index e4423fe..7538aad 100644
|
||||
--- a/capsules/src/lib.rs
|
||||
+++ b/capsules/src/lib.rs
|
||||
@@ -22,6 +22,7 @@ pub mod crc;
|
||||
pub mod dac;
|
||||
pub mod debug_process_restart;
|
||||
pub mod driver;
|
||||
+pub mod firmware_protection;
|
||||
pub mod fm25cl;
|
||||
pub mod ft6x06;
|
||||
pub mod fxos8700cq;
|
||||
diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs
|
||||
index 3bb8b5a..ea96cb2 100644
|
||||
--- a/chips/nrf52/src/uicr.rs
|
||||
+++ b/chips/nrf52/src/uicr.rs
|
||||
@@ -1,13 +1,14 @@
|
||||
//! User information configuration registers
|
||||
|
||||
-
|
||||
use enum_primitive::cast::FromPrimitive;
|
||||
+use hil::firmware_protection::ProtectionLevel;
|
||||
use kernel::common::registers::{register_bitfields, register_structs, ReadWrite};
|
||||
use kernel::common::StaticRef;
|
||||
use kernel::hil;
|
||||
use kernel::ReturnCode;
|
||||
|
||||
use crate::gpio::Pin;
|
||||
+use crate::nvmc::NVMC;
|
||||
|
||||
const UICR_BASE: StaticRef<UicrRegisters> =
|
||||
unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) };
|
||||
@@ -210,3 +211,49 @@ impl Uicr {
|
||||
self.registers.approtect.write(ApProtect::PALL::ENABLED);
|
||||
}
|
||||
}
|
||||
+
|
||||
+impl hil::firmware_protection::FirmwareProtection for Uicr {
|
||||
+ fn get_protection(&self) -> ProtectionLevel {
|
||||
+ let ap_protect_state = self.is_ap_protect_enabled();
|
||||
+ let cpu_debug_state = self
|
||||
+ .registers
|
||||
+ .debugctrl
|
||||
+ .matches_all(DebugControl::CPUNIDEN::ENABLED + DebugControl::CPUFPBEN::ENABLED);
|
||||
+ match (ap_protect_state, cpu_debug_state) {
|
||||
+ (false, _) => ProtectionLevel::NoProtection,
|
||||
+ (true, true) => ProtectionLevel::JtagDisabled,
|
||||
+ (true, false) => ProtectionLevel::FullyLocked,
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode {
|
||||
+ let current_level = self.get_protection();
|
||||
+ if current_level > level || level == ProtectionLevel::Unknown {
|
||||
+ return ReturnCode::EINVAL;
|
||||
+ }
|
||||
+ if current_level == level {
|
||||
+ return ReturnCode::EALREADY;
|
||||
+ }
|
||||
+
|
||||
+ unsafe { NVMC.configure_writeable() };
|
||||
+ if level >= ProtectionLevel::JtagDisabled {
|
||||
+ self.set_ap_protect();
|
||||
+ }
|
||||
+
|
||||
+ if level >= ProtectionLevel::FullyLocked {
|
||||
+ // Prevent CPU debug and flash patching. Leaving these enabled could
|
||||
+ // allow to circumvent protection.
|
||||
+ self.registers
|
||||
+ .debugctrl
|
||||
+ .write(DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED);
|
||||
+ // TODO(jmichel): prevent returning into bootloader if present
|
||||
+ }
|
||||
+ unsafe { NVMC.configure_readonly() };
|
||||
+
|
||||
+ if self.get_protection() == level {
|
||||
+ ReturnCode::SUCCESS
|
||||
+ } else {
|
||||
+ ReturnCode::FAIL
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/kernel/src/hil/firmware_protection.rs b/kernel/src/hil/firmware_protection.rs
|
||||
new file mode 100644
|
||||
index 0000000..de08246
|
||||
--- /dev/null
|
||||
+++ b/kernel/src/hil/firmware_protection.rs
|
||||
@@ -0,0 +1,48 @@
|
||||
+//! Interface for Firmware Protection, also called Code Readout Protection.
|
||||
+
|
||||
+use crate::returncode::ReturnCode;
|
||||
+
|
||||
+#[derive(PartialOrd, PartialEq)]
|
||||
+pub enum ProtectionLevel {
|
||||
+ /// Unsupported feature
|
||||
+ Unknown = 0,
|
||||
+ /// This should be the factory default for the chip.
|
||||
+ NoProtection = 1,
|
||||
+ /// At this level, only JTAG/SWD are disabled but other debugging
|
||||
+ /// features may still be enabled.
|
||||
+ JtagDisabled = 2,
|
||||
+ /// This is the maximum level of protection the chip supports.
|
||||
+ /// At this level, JTAG and all other features are expected to be
|
||||
+ /// disabled and only a full chip erase may allow to recover from
|
||||
+ /// that state.
|
||||
+ FullyLocked = 0xff,
|
||||
+}
|
||||
+
|
||||
+impl From<usize> for ProtectionLevel {
|
||||
+ fn from(value: usize) -> Self {
|
||||
+ match value {
|
||||
+ 1 => ProtectionLevel::NoProtection,
|
||||
+ 2 => ProtectionLevel::JtagDisabled,
|
||||
+ 0xff => ProtectionLevel::FullyLocked,
|
||||
+ _ => ProtectionLevel::Unknown,
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+pub trait FirmwareProtection {
|
||||
+ /// Gets the current firmware protection level.
|
||||
+ /// This doesn't fail and always returns a value.
|
||||
+ fn get_protection(&self) -> ProtectionLevel;
|
||||
+
|
||||
+ /// Sets the firmware protection level.
|
||||
+ /// There are four valid return values:
|
||||
+ /// - SUCCESS: protection level has been set to `level`
|
||||
+ /// - FAIL: something went wrong while setting the protection
|
||||
+ /// level and the effective protection level is not the one
|
||||
+ /// that was requested.
|
||||
+ /// - EALREADY: the requested protection level is already the
|
||||
+ /// level that is set.
|
||||
+ /// - EINVAL: unsupported protection level or the requested
|
||||
+ /// protection level is lower than the currently set one.
|
||||
+ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode;
|
||||
+}
|
||||
diff --git a/kernel/src/hil/mod.rs b/kernel/src/hil/mod.rs
|
||||
index 4f42afa..83e7702 100644
|
||||
--- a/kernel/src/hil/mod.rs
|
||||
+++ b/kernel/src/hil/mod.rs
|
||||
@@ -8,6 +8,7 @@ pub mod dac;
|
||||
pub mod digest;
|
||||
pub mod eic;
|
||||
pub mod entropy;
|
||||
+pub mod firmware_protection;
|
||||
pub mod flash;
|
||||
pub mod gpio;
|
||||
pub mod gpio_async;
|
||||
|
||||
@@ -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
|
||||
|
||||
3
setup.sh
3
setup.sh
@@ -44,3 +44,6 @@ rustup target add thumbv7em-none-eabi
|
||||
# Install dependency to create applications.
|
||||
mkdir -p elf2tab
|
||||
cargo install elf2tab --version 0.6.0 --root elf2tab/
|
||||
|
||||
# Install python dependencies to factory configure OpenSK (crypto, JTAG lockdown)
|
||||
pip3 install --user --upgrade colorama tqdm cryptography "fido2>=0.9.1"
|
||||
|
||||
436
src/ctap/apdu.rs
Normal file
436
src/ctap/apdu.rs
Normal 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
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
472
src/ctap/config_command.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
928
src/ctap/credential_management.rs
Normal file
928
src/ctap/credential_management.rs
Normal 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
147
src/ctap/crypto_wrapper.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
280
src/ctap/customization.rs
Normal 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
@@ -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);
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
411
src/ctap/large_blobs.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
2670
src/ctap/mod.rs
2670
src/ctap/mod.rs
File diff suppressed because it is too large
Load Diff
408
src/ctap/pin_protocol.rs
Normal file
408
src/ctap/pin_protocol.rs
Normal 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 authenticator’s 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
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
1494
src/ctap/storage.rs
1494
src/ctap/storage.rs
File diff suppressed because it is too large
Load Diff
159
src/ctap/storage/key.rs
Normal file
159
src/ctap/storage/key.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
277
src/ctap/token_state.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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[..]);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -18,3 +18,6 @@ extern crate alloc;
|
||||
|
||||
pub mod ctap;
|
||||
pub mod embedded_flash;
|
||||
|
||||
#[macro_use]
|
||||
extern crate arrayref;
|
||||
|
||||
16
src/main.rs
16
src/main.rs
@@ -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 {
|
||||
|
||||
2
third_party/lang-items/Cargo.toml
vendored
2
third_party/lang-items/Cargo.toml
vendored
@@ -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
52
third_party/libtock-drivers/src/crp.rs
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::result::TockResult;
|
||||
use libtock_core::syscalls;
|
||||
|
||||
const DRIVER_NUMBER: usize = 0x00008;
|
||||
|
||||
mod command_nr {
|
||||
pub const AVAILABLE: usize = 0;
|
||||
pub const GET_PROTECTION: usize = 1;
|
||||
pub const SET_PROTECTION: usize = 2;
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq)]
|
||||
pub enum ProtectionLevel {
|
||||
/// Unsupported feature
|
||||
Unknown = 0,
|
||||
/// This should be the factory default for the chip.
|
||||
NoProtection = 1,
|
||||
/// At this level, only JTAG/SWD are disabled but other debugging
|
||||
/// features may still be enabled.
|
||||
JtagDisabled = 2,
|
||||
/// This is the maximum level of protection the chip supports.
|
||||
/// At this level, JTAG and all other features are expected to be
|
||||
/// disabled and only a full chip erase may allow to recover from
|
||||
/// that state.
|
||||
FullyLocked = 0xff,
|
||||
}
|
||||
|
||||
impl From<usize> for ProtectionLevel {
|
||||
fn from(value: usize) -> Self {
|
||||
match value {
|
||||
1 => ProtectionLevel::NoProtection,
|
||||
2 => ProtectionLevel::JtagDisabled,
|
||||
0xff => ProtectionLevel::FullyLocked,
|
||||
_ => ProtectionLevel::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_available() -> TockResult<()> {
|
||||
syscalls::command(DRIVER_NUMBER, command_nr::AVAILABLE, 0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_protection() -> TockResult<ProtectionLevel> {
|
||||
let current_level = syscalls::command(DRIVER_NUMBER, command_nr::GET_PROTECTION, 0, 0)?;
|
||||
Ok(current_level.into())
|
||||
}
|
||||
|
||||
pub fn set_protection(level: ProtectionLevel) -> TockResult<()> {
|
||||
syscalls::command(DRIVER_NUMBER, command_nr::SET_PROTECTION, level as usize, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
1
third_party/libtock-drivers/src/lib.rs
vendored
1
third_party/libtock-drivers/src/lib.rs
vendored
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod buttons;
|
||||
pub mod console;
|
||||
pub mod crp;
|
||||
pub mod led;
|
||||
#[cfg(feature = "with_nfc")]
|
||||
pub mod nfc;
|
||||
|
||||
204
tools/configure.py
Executable file
204
tools/configure.py
Executable 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())
|
||||
Reference in New Issue
Block a user