diff --git a/.github/workflows/cargo_check.yml b/.github/workflows/cargo_check.yml index 6676e16..7151806 100644 --- a/.github/workflows/cargo_check.yml +++ b/.github/workflows/cargo_check.yml @@ -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 diff --git a/.github/workflows/crypto_test.yml b/.github/workflows/crypto_test.yml index 50fdf88..5abfce9 100644 --- a/.github/workflows/crypto_test.yml +++ b/.github/workflows/crypto_test.yml @@ -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 diff --git a/.github/workflows/opensk_test.yml b/.github/workflows/opensk_test.yml index 588dab6..406a7f3 100644 --- a/.github/workflows/opensk_test.yml +++ b/.github/workflows/opensk_test.yml @@ -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 - diff --git a/.github/workflows/persistent_store_test.yml b/.github/workflows/persistent_store_test.yml index 1a1d942..ffe10ff 100644 --- a/.github/workflows/persistent_store_test.yml +++ b/.github/workflows/persistent_store_test.yml @@ -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: diff --git a/.gitignore b/.gitignore index 1b32046..611b278 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Cargo.lock /reproducible/binaries.sha256sum /reproducible/elf2tab.txt /reproducible/reproduced.tar +__pycache__ diff --git a/.gitmodules b/.gitmodules index b70a516..273601b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index cce0ec5..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "davidanson.vscode-markdownlint", - "rust-lang.rust", - "ms-python.python" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 138097a..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -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 - }, -} diff --git a/Cargo.toml b/Cargo.toml index 8faf8dd..63b109a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ctap2" -version = "0.1.0" +version = "1.0.0" authors = [ "Fabian Kaczmarczyck ", "Guillaume Endignoux ", @@ -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" diff --git a/OpenSK.code-workspace b/OpenSK.code-workspace new file mode 100644 index 0000000..c367c89 --- /dev/null +++ b/OpenSK.code-workspace @@ -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" + ] + } +} diff --git a/README.md b/README.md index ccb6fc7..9a49826 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # OpenSK logo -[![Build Status](https://travis-ci.org/google/OpenSK.svg?branch=master)](https://travis-ci.org/google/OpenSK) ![markdownlint](https://github.com/google/OpenSK/workflows/markdownlint/badge.svg?branch=master) ![pylint](https://github.com/google/OpenSK/workflows/pylint/badge.svg?branch=master) ![Cargo check](https://github.com/google/OpenSK/workflows/Cargo%20check/badge.svg?branch=master) @@ -25,14 +24,16 @@ few limitations: ### FIDO2 -Although we tested and implemented our firmware based on the published -[CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html), -our implementation was not reviewed nor officially tested and doesn't claim to -be FIDO Certified. -We started adding features of the upcoming next version of the -[CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). -The development is currently between 2.0 and 2.1, with updates hidden behind a feature flag. -Please add the flag `--ctap2.1` to the deploy command to include them. +The stable branch implements the published +[CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html) +and is FIDO certified. + +FIDO2 certified L1 + +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 diff --git a/boards/nordic/nrf52840_mdk_dfu/src/main.rs b/boards/nordic/nrf52840_mdk_dfu/src/main.rs index a346da5..1eccb0e 100644 --- a/boards/nordic/nrf52840_mdk_dfu/src/main.rs +++ b/boards/nordic/nrf52840_mdk_dfu/src/main.rs @@ -48,7 +48,7 @@ static STRINGS: &'static [&'static str] = &[ // Product "OpenSK", // Serial number - "v0.1", + "v1.0", ]; // State for loading and holding applications. diff --git a/build.rs b/build.rs index e981555..b581d8e 100644 --- a/build.rs +++ b/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(); diff --git a/deploy.py b/deploy.py index e1ec38f..0c8d998 100755 --- a/deploy.py +++ b/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", diff --git a/docs/FIDO2 Certificate Google FIDO20020210209001.pdf b/docs/FIDO2 Certificate Google FIDO20020210209001.pdf new file mode 100644 index 0000000..9749108 Binary files /dev/null and b/docs/FIDO2 Certificate Google FIDO20020210209001.pdf differ diff --git a/docs/img/FIDO2_Certified_L1.png b/docs/img/FIDO2_Certified_L1.png new file mode 100644 index 0000000..20d34c2 Binary files /dev/null and b/docs/img/FIDO2_Certified_L1.png differ diff --git a/docs/install.md b/docs/install.md index cf355fa..166d5b5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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 diff --git a/examples/erase_storage.rs b/examples/erase_storage.rs new file mode 100644 index 0000000..6076348 --- /dev/null +++ b/examples/erase_storage.rs @@ -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 +} diff --git a/examples/store_latency.rs b/examples/store_latency.rs new file mode 100644 index 0000000..fd6f504 --- /dev/null +++ b/examples/store_latency.rs @@ -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 { + Timestamp::::from_clock_value(timer.get_current_clock().ok().unwrap()) +} + +fn measure(timer: &Timer, operation: impl FnOnce() -> T) -> (T, Duration) { + 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 { + 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 | +} diff --git a/fuzz/fuzz_helper/Cargo.toml b/fuzz/fuzz_helper/Cargo.toml index 3b70f43..2f2a5c1 100644 --- a/fuzz/fuzz_helper/Cargo.toml +++ b/fuzz/fuzz_helper/Cargo.toml @@ -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'] } diff --git a/fuzz/fuzz_helper/src/lib.rs b/fuzz/fuzz_helper/src/lib.rs index a6dc1a7..1553f65 100644 --- a/fuzz/fuzz_helper/src/lib.rs +++ b/fuzz/fuzz_helper/src/lib.rs @@ -147,7 +147,7 @@ fn process_message( 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. diff --git a/libraries/cbor/src/lib.rs b/libraries/cbor/src/lib.rs index 0a128fc..0667484 100644 --- a/libraries/cbor/src/lib.rs +++ b/libraries/cbor/src/lib.rs @@ -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; diff --git a/libraries/cbor/src/macros.rs b/libraries/cbor/src/macros.rs index 40669d1..7dac984 100644 --- a/libraries/cbor/src/macros.rs +++ b/libraries/cbor/src/macros.rs @@ -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` 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`, 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::::new(); -/// use cbor::values::IntoCborKey; -/// let x: Option = map.remove(&1.into_cbor_key()); -/// let y: Option = 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 = destructure_cbor_map_peek_value(&mut it, $key.into_cbor_key()); + let $variable: Option = 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>, - needle: KeyType, + it: &mut Peekable>, + needle: Value, ) -> Option { 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 = 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. #[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 { + fn extract_map(cbor_value: Value) -> Vec<(Value, Value)> { match cbor_value { Value::Map(map) => map, _ => panic!("Expected CBOR map."), diff --git a/libraries/cbor/src/reader.rs b/libraries/cbor/src/reader.rs index a1061a0..b11cf84 100644 --- a/libraries/cbor/src/reader.rs +++ b/libraries/cbor/src/reader.rs @@ -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 { - 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![ diff --git a/libraries/cbor/src/values.rs b/libraries/cbor/src/values.rs index b20d109..1e1324d 100644 --- a/libraries/cbor/src/values.rs +++ b/libraries/cbor/src/values.rs @@ -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), - Map(BTreeMap), - // 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), TextString(String), + Array(Vec), + 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 { +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Value) -> Option { 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 { match int { @@ -132,54 +139,51 @@ impl SimpleValue { } } -impl From for KeyType { +impl From for Value { fn from(unsigned: u64) -> Self { - KeyType::Unsigned(unsigned) + Value::Unsigned(unsigned) } } -impl From for KeyType { +impl From for Value { fn from(i: i64) -> Self { - KeyType::integer(i) + Value::integer(i) } } -impl From for KeyType { +impl From for Value { fn from(i: i32) -> Self { - KeyType::integer(i as i64) + Value::integer(i as i64) } } -impl From> for KeyType { +impl From> for Value { fn from(bytes: Vec) -> 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 for KeyType { +impl From 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 From for Value -where - KeyType: From, -{ - fn from(t: T) -> Self { - Value::KeyValue(KeyType::from(t)) +impl From> for Value { + fn from(map: Vec<(Value, Value)>) -> Self { + Value::Map(map) } } @@ -189,19 +193,6 @@ impl From for Value { } } -pub trait IntoCborKey { - fn into_cbor_key(self) -> KeyType; -} - -impl IntoCborKey for T -where - KeyType: From, -{ - 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)); } } diff --git a/libraries/cbor/src/writer.rs b/libraries/cbor/src/writer.rs index 592048d..c8a4808 100644 --- a/libraries/cbor/src/writer.rs +++ b/libraries/cbor/src/writer.rs @@ -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) -> 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! { diff --git a/libraries/crypto/Cargo.toml b/libraries/crypto/Cargo.toml index ead1294..aa1a597 100644 --- a/libraries/crypto/Cargo.toml +++ b/libraries/crypto/Cargo.toml @@ -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 = [] diff --git a/libraries/crypto/src/ec/exponent256.rs b/libraries/crypto/src/ec/exponent256.rs index 8638eaa..4a31aee 100644 --- a/libraries/crypto/src/ec/exponent256.rs +++ b/libraries/crypto/src/ec/exponent256.rs @@ -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, } diff --git a/libraries/crypto/src/ec/gfp256.rs b/libraries/crypto/src/ec/gfp256.rs index bb3232c..0e6179f 100644 --- a/libraries/crypto/src/ec/gfp256.rs +++ b/libraries/crypto/src/ec/gfp256.rs @@ -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) diff --git a/libraries/crypto/src/ec/int256.rs b/libraries/crypto/src/ec/int256.rs index 9954c37..8927b19 100644 --- a/libraries/crypto/src/ec/int256.rs +++ b/libraries/crypto/src/ec/int256.rs @@ -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) diff --git a/libraries/crypto/src/ec/point.rs b/libraries/crypto/src/ec/point.rs index 11c6cde..1038808 100644 --- a/libraries/crypto/src/ec/point.rs +++ b/libraries/crypto/src/ec/point.rs @@ -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 diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index c735d11..4a03679 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -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 { 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)); } } diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index 52949e3..eb61365 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -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(&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(&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 { 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> { - 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")] diff --git a/libraries/crypto/src/hkdf.rs b/libraries/crypto/src/hkdf.rs new file mode 100644 index 0000000..ee276a3 --- /dev/null +++ b/libraries/crypto/src/hkdf.rs @@ -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(ikm: &[u8], info: &[u8]) -> [u8; HASH_SIZE] +where + H: Hash256 + HashBlockSize64Bytes, +{ + // Salt is a zero block here. + let prk = hmac_256::(&[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::(&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(salt: &[u8], ikm: &[u8], l: u8, info: &[u8]) -> Vec +where + H: Hash256 + HashBlockSize64Bytes, +{ + let prk = if salt.is_empty() { + hmac_256::(&[0; HASH_SIZE], ikm) + } else { + hmac_256::(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::(&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::(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::(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::(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::(&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::(&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::(&salt, input.as_slice(), l, input.as_slice()), + hkdf::(&[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::(&salt, input.as_slice(), l, input.as_slice()), + hkdf_empty_salt_256::(input.as_slice(), input.as_slice()) + ); + input.push(b'A'); + } + } +} diff --git a/libraries/crypto/src/lib.rs b/libraries/crypto/src/lib.rs index 7b35e99..da13180 100644 --- a/libraries/crypto/src/lib.rs +++ b/libraries/crypto/src/lib.rs @@ -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; diff --git a/libraries/persistent_store/fuzz/Cargo.toml b/libraries/persistent_store/fuzz/Cargo.toml index fdc4f89..f0b01f1 100644 --- a/libraries/persistent_store/fuzz/Cargo.toml +++ b/libraries/persistent_store/fuzz/Cargo.toml @@ -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 diff --git a/libraries/persistent_store/fuzz/examples/store.rs b/libraries/persistent_store/fuzz/examples/store.rs new file mode 100644 index 0000000..be9240b --- /dev/null +++ b/libraries/persistent_store/fuzz/examples/store.rs @@ -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: {} {{ [] | .. }} + +If is not provided, it is read from standard input. + +When .. are provided, only runs matching all predicates are shown. The format of +each is =."#, + 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 { + 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) { + 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 = 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]); +} diff --git a/libraries/persistent_store/fuzz/fuzz_targets/store.rs b/libraries/persistent_store/fuzz/fuzz_targets/store.rs index 1cff2a4..d96874d 100644 --- a/libraries/persistent_store/fuzz/fuzz_targets/store.rs +++ b/libraries/persistent_store/fuzz/fuzz_targets/store.rs @@ -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); }); diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 11645f3..3d32624 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -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. /// diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs new file mode 100644 index 0000000..006532b --- /dev/null +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -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, +} + +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> { + 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> { + 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 { + // 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) -> 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, + } + } +} diff --git a/libraries/persistent_store/src/buffer.rs b/libraries/persistent_store/src/buffer.rs index 0059826..3acd39a 100644 --- a/libraries/persistent_store/src/buffer.rs +++ b/libraries/persistent_store/src/buffer.rs @@ -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. diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index 15001cb..d2baee0 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -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 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 { 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, (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 { + 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, (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 { + 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 -} diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 1f87ef3..a7dd4f5 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -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(::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::() 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(storage: &S) -> Option { 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(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 { 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 { 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 { 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 { + pub fn build_user(&self, key: Nat, value: &[u8]) -> StoreResult> { 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>( + &self, + updates: &[StoreUpdate], + ) -> 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>(&self, update: &StoreUpdate) -> 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> { + pub fn transaction_valid>( + &self, + updates: &[StoreUpdate], + ) -> Option> { 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); + } } diff --git a/libraries/persistent_store/src/format/bitfield.rs b/libraries/persistent_store/src/format/bitfield.rs index 2cffc4b..32c0ae5 100644 --- a/libraries/persistent_store/src/format/bitfield.rs +++ b/libraries/persistent_store/src/format/bitfield.rs @@ -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)); } diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs new file mode 100644 index 0000000..661d5db --- /dev/null +++ b/libraries/persistent_store/src/fragment.rs @@ -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; + + /// 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 { + fn len(&self) -> usize { + self.end - self.start + } + + fn pos(&self, key: usize) -> Option { + 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, keys: &impl Keys) -> StoreResult>> { + 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, + keys: &impl Keys, + range: Range, +) -> StoreResult>> { + 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, 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, keys: &impl Keys) -> StoreResult<()> { + let updates: Vec>> = 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, keys: &impl Keys) -> StoreResult> { + let keys_len = keys.len(); + let mut handles: Vec> = 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)); + } +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 563db65..0ff3224 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -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::{ diff --git a/libraries/persistent_store/src/model.rs b/libraries/persistent_store/src/model.rs index c509b03..e0bf9e3 100644 --- a/libraries/persistent_store/src/model.rs +++ b/libraries/persistent_store/src/model.rs @@ -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, + updates: Vec>>, }, /// Deletes all keys above a threshold. @@ -89,7 +92,7 @@ impl StoreModel { } /// Applies a transaction. - fn transaction(&mut self, updates: Vec) -> StoreResult<()> { + fn transaction(&mut self, updates: Vec>>) -> StoreResult<()> { // Fail if the transaction is invalid. if self.format.transaction_valid(&updates).is_none() { return Err(StoreError::InvalidArgument); diff --git a/libraries/persistent_store/src/storage.rs b/libraries/persistent_store/src/storage.rs index becd900..fb5b0cb 100644 --- a/libraries/persistent_store/src/storage.rs +++ b/libraries/persistent_store/src/storage.rs @@ -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<()>; } diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index a3e084b..c143c89 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -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 for StoreError { } } +impl From for StoreError { + fn from(error: NoneError) -> StoreError { + match error { + NoneError => StoreError::InvalidStorage, + } + } +} + /// Result of store operations. pub type StoreResult = Result; /// 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(&self, store: &Store) -> StoreResult { + 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(&self, store: &Store) -> StoreResult> { 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> { /// Inserts or replaces an entry in the store. - Insert { key: usize, value: Vec }, + Insert { key: usize, value: ByteSlice }, /// Removes an entry from the store. Remove { key: usize }, } -impl StoreUpdate { +impl> StoreUpdate { /// 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> + 'a>; + /// Implements a store with a map interface over a storage. #[derive(Clone)] pub struct Store { @@ -182,6 +202,14 @@ pub struct Store { /// The storage configuration. format: Format, + + /// The position of the first word in the store. + head: Option, + + /// The list of the position of the user entries. + /// + /// The position is encoded as the word offset from the [head](Store::head). + entries: Option>, } impl Store { @@ -193,13 +221,19 @@ impl Store { /// /// # 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, (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 Store { } /// Iterates over the entries. - pub fn iter<'a>(&'a self) -> StoreResult> { - StoreIter::new(self) + pub fn iter<'a>(&'a self) -> StoreResult> { + 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 { 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 Store { /// /// # 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>( + &mut self, + updates: &[StoreUpdate], + ) -> 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 Store { 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 Store { 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 Store { } 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 Store { 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 Store { 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 Store { 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 Store { /// 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 Store { 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 { + 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> { self.check_handle(handle)?; @@ -437,7 +496,7 @@ impl Store { let init_info = self.format.build_init(InitInfo { cycle: 0, prefix: 0, - }); + })?; self.storage_write_slice(index, &init_info) } @@ -460,7 +519,9 @@ impl Store { /// 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 Store { /// 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 Store { /// /// 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 Store { /// 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 Store { 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 Store { // 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 Store { /// 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 Store { 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 Store { 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 Store { 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 Store { /// 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 Store { } } // 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 { 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 { - 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 Store { 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 { /// 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 { - 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 { store .iter() .unwrap() - .map(|x| x.unwrap()) - .filter(|x| delete_key(x.key as usize)) - .collect::>() + .filter(|x| x.is_err() || delete_key(x.as_ref().unwrap().key as usize)) + .collect::, _>>() }; match *operation { StoreOperation::Transaction { ref updates } => { let keys: HashSet = 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 { 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, - - /// 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) -> StoreResult> { - 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> { - 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; - - fn next(&mut self) -> Option> { - 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 { #[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])); + } } diff --git a/libraries/persistent_store/src/test.rs b/libraries/persistent_store/src/test.rs new file mode 100644 index 0000000..2d20574 --- /dev/null +++ b/libraries/persistent_store/src/test.rs @@ -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 { + 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); +} diff --git a/metadata/metadata.json b/metadata/metadata.json new file mode 100644 index 0000000..eedeed9 --- /dev/null +++ b/metadata/metadata.json @@ -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 +} diff --git a/patches/tock/02-usb.patch b/patches/tock/02-usb.patch index 135369d..6220f6a 100644 --- a/patches/tock/02-usb.patch +++ b/patches/tock/02-usb.patch @@ -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. diff --git a/patches/tock/06-update-uicr.patch b/patches/tock/06-update-uicr.patch new file mode 100644 index 0000000..53ee945 --- /dev/null +++ b/patches/tock/06-update-uicr.patch @@ -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 = +- 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, +- /// Mapping of the nRESET function (see POWER chapter for details) +- /// - Address: 0x204 - 0x208 +- pselreset1: ReadWrite, +- /// Access Port protection +- /// - Address: 0x208 - 0x20c +- approtect: ReadWrite, +- /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO +- /// - Address: 0x20c - 0x210 +- nfcpins: ReadWrite, +- _reserved1: [u32; 60], +- /// External circuitry to be supplied from VDD pin. +- /// - Address: 0x300 - 0x304 +- extsupply: ReadWrite, +- /// GPIO reference voltage +- /// - Address: 0x304 - 0x308 +- regout0: ReadWrite, ++ unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; ++ ++register_structs! { ++ UicrRegisters { ++ (0x000 => _reserved1), ++ /// Reserved for Nordic firmware design ++ (0x014 => nrffw: [ReadWrite; 13]), ++ (0x048 => _reserved2), ++ /// Reserved for Nordic hardware design ++ (0x050 => nrfhw: [ReadWrite; 12]), ++ /// Reserved for customer ++ (0x080 => customer: [ReadWrite; 32]), ++ (0x100 => _reserved3), ++ /// Mapping of the nRESET function (see POWER chapter for details) ++ (0x200 => pselreset0: ReadWrite), ++ /// Mapping of the nRESET function (see POWER chapter for details) ++ (0x204 => pselreset1: ReadWrite), ++ /// Access Port protection ++ (0x208 => approtect: ReadWrite), ++ /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO ++ /// - Address: 0x20c - 0x210 ++ (0x20c => nfcpins: ReadWrite), ++ (0x210 => debugctrl: ReadWrite), ++ (0x214 => _reserved4), ++ /// External circuitry to be supplied from VDD pin. ++ (0x300 => extsupply: ReadWrite), ++ /// GPIO reference voltage ++ (0x304 => regout0: ReadWrite), ++ (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 + diff --git a/patches/tock/07-firmware-protect.patch b/patches/tock/07-firmware-protect.patch new file mode 100644 index 0000000..365b20a --- /dev/null +++ b/patches/tock/07-firmware-protect.patch @@ -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> = ++ MaybeUninit::uninit(); ++ &mut BUF ++ };}; ++} ++ ++pub struct FirmwareProtectionComponent { ++ board_kernel: &'static kernel::Kernel, ++ crp: C, ++} ++ ++impl FirmwareProtectionComponent { ++ pub fn new(board_kernel: &'static kernel::Kernel, crp: C) -> FirmwareProtectionComponent { ++ FirmwareProtectionComponent { ++ board_kernel: board_kernel, ++ crp: crp, ++ } ++ } ++} ++ ++impl Component ++ for FirmwareProtectionComponent ++{ ++ type StaticInput = &'static mut MaybeUninit>; ++ type Output = &'static firmware_protection::FirmwareProtection; ++ ++ 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, ++ 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, + } + + 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, + } + + 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 { ++ crp_unit: C, ++ apps: Grant>, ++} ++ ++impl FirmwareProtection { ++ pub fn new(crp_unit: C, apps: Grant>) -> Self { ++ Self { crp_unit, apps } ++ } ++} ++ ++impl Driver for FirmwareProtection { ++ /// ++ /// ### 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 = + 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 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; + diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 7f3b8f3..b54d3a8 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -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 diff --git a/setup.sh b/setup.sh index 903e0e3..6d58053 100755 --- a/setup.sh +++ b/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" diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs new file mode 100644 index 0000000..455e574 --- /dev/null +++ b/src/ctap/apdu.rs @@ -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 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, + pub le: u32, + pub case_type: ApduType, +} + +impl TryFrom<&[u8]> for APDU { + type Error = ApduStatusCode; + + fn try_from(frame: &[u8]) -> Result { + 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::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); + } +} diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs new file mode 100644 index 0000000..8b6588e --- /dev/null +++ b/src/ctap/client_pin.rs @@ -0,0 +1,1760 @@ +// 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::command::AuthenticatorClientPinParameters; +use super::data_formats::{ + ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PinUvAuthProtocol, +}; +use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; +use super::response::{AuthenticatorClientPinResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use super::token_state::PinUvAuthTokenState; +use alloc::boxed::Box; +use alloc::str; +use alloc::string::String; +use alloc::vec::Vec; +use crypto::hmac::hmac_256; +use crypto::rng256::Rng256; +use crypto::sha256::Sha256; +use crypto::Hash256; +#[cfg(test)] +use enum_iterator::IntoEnumIterator; +use libtock_drivers::timer::ClockValue; +use subtle::ConstantTimeEq; + +/// The prefix length of the PIN hash that is stored and compared. +/// +/// The code assumes that this value is a multiple of the AES block length, fits +/// an u8 and is at most as long as a SHA256. The value is fixed for all PIN +/// protocols. +pub const PIN_AUTH_LENGTH: usize = 16; + +/// The length of the pinUvAuthToken used throughout PIN protocols. +/// +/// The code assumes that this value is a multiple of the AES block length. It +/// is fixed since CTAP2.1. +pub const PIN_TOKEN_LENGTH: usize = 32; + +/// The length of the encrypted PINs when received by SetPin or ChangePin. +/// +/// The code assumes that this value is a multiple of the AES block length. It +/// is fixed since CTAP2.1. +const PIN_PADDED_LENGTH: usize = 64; + +/// Decrypts the new_pin_enc and outputs the found PIN. +fn decrypt_pin( + shared_secret: &dyn SharedSecret, + new_pin_enc: Vec, +) -> Result, Ctap2StatusCode> { + let decrypted_pin = shared_secret.decrypt(&new_pin_enc)?; + if decrypted_pin.len() != PIN_PADDED_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + // In CTAP 2.1, the specification changed. The new wording might lead to + // different behavior when there are non-zero bytes after zero bytes. + // This implementation consistently ignores those degenerate cases. + Ok(decrypted_pin.into_iter().take_while(|&c| c != 0).collect()) +} + +/// Stores a hash prefix of the new PIN in the persistent storage, if correct. +/// +/// The new PIN is passed encrypted, so it is first decrypted and stripped from +/// padding. Next, it is checked against the PIN policy. Last, it is hashed and +/// truncated for persistent storage. +fn check_and_store_new_pin( + persistent_store: &mut PersistentStore, + shared_secret: &dyn SharedSecret, + new_pin_enc: Vec, +) -> Result<(), Ctap2StatusCode> { + let pin = decrypt_pin(shared_secret, new_pin_enc)?; + let min_pin_length = persistent_store.min_pin_length()? as usize; + let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count(); + if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); + } + let mut pin_hash = [0u8; PIN_AUTH_LENGTH]; + pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..PIN_AUTH_LENGTH]); + // The PIN length is always < PIN_PADDED_LENGTH < 256. + persistent_store.set_pin(&pin_hash, pin_length as u8)?; + Ok(()) +} + +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum PinPermission { + // All variants should use integers with a single bit set. + MakeCredential = 0x01, + GetAssertion = 0x02, + CredentialManagement = 0x04, + _BioEnrollment = 0x08, + LargeBlobWrite = 0x10, + AuthenticatorConfiguration = 0x20, +} + +pub struct ClientPin { + pin_protocol_v1: PinProtocol, + pin_protocol_v2: PinProtocol, + consecutive_pin_mismatches: u8, + pin_uv_auth_token_state: PinUvAuthTokenState, +} + +impl ClientPin { + pub fn new(rng: &mut impl Rng256) -> ClientPin { + ClientPin { + pin_protocol_v1: PinProtocol::new(rng), + pin_protocol_v2: PinProtocol::new(rng), + consecutive_pin_mismatches: 0, + pin_uv_auth_token_state: PinUvAuthTokenState::new(), + } + } + + /// Gets a reference to the PIN protocol of the given version. + fn get_pin_protocol(&self, pin_uv_auth_protocol: PinUvAuthProtocol) -> &PinProtocol { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => &self.pin_protocol_v1, + PinUvAuthProtocol::V2 => &self.pin_protocol_v2, + } + } + + /// Gets a mutable reference to the PIN protocol of the given version. + fn get_mut_pin_protocol( + &mut self, + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> &mut PinProtocol { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => &mut self.pin_protocol_v1, + PinUvAuthProtocol::V2 => &mut self.pin_protocol_v2, + } + } + + /// Computes the shared secret for the given version. + fn get_shared_secret( + &self, + pin_uv_auth_protocol: PinUvAuthProtocol, + key_agreement: CoseKey, + ) -> Result, Ctap2StatusCode> { + self.get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol) + } + + /// Checks the given encrypted PIN hash against the stored PIN hash. + /// + /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. + /// Resets or decreases the PIN retries, depending on success or failure. + /// Also, in case of failure, the key agreement key is randomly reset. + fn verify_pin_hash_enc( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + pin_uv_auth_protocol: PinUvAuthProtocol, + shared_secret: &dyn SharedSecret, + pin_hash_enc: Vec, + ) -> Result<(), Ctap2StatusCode> { + match persistent_store.pin_hash()? { + Some(pin_hash) => { + if self.consecutive_pin_mismatches >= 3 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); + } + persistent_store.decr_pin_retries()?; + let pin_hash_dec = shared_secret + .decrypt(&pin_hash_enc) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?; + + if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { + self.get_mut_pin_protocol(pin_uv_auth_protocol) + .regenerate(rng); + if persistent_store.pin_retries()? == 0 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + } + self.consecutive_pin_mismatches += 1; + if self.consecutive_pin_mismatches >= 3 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); + } + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + } + // This status code is not explicitly mentioned in the specification. + None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED), + } + persistent_store.reset_pin_retries()?; + self.consecutive_pin_mismatches = 0; + Ok(()) + } + + fn process_get_pin_retries( + &self, + persistent_store: &PersistentStore, + ) -> Result { + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_uv_auth_token: None, + retries: Some(persistent_store.pin_retries()? as u64), + power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), + }) + } + + fn process_get_key_agreement( + &self, + client_pin_params: AuthenticatorClientPinParameters, + ) -> Result { + let key_agreement = Some( + self.get_pin_protocol(client_pin_params.pin_uv_auth_protocol) + .get_public_key(), + ); + Ok(AuthenticatorClientPinResponse { + key_agreement, + pin_uv_auth_token: None, + retries: None, + power_cycle_state: None, + }) + } + + fn process_set_pin( + &mut self, + persistent_store: &mut PersistentStore, + client_pin_params: AuthenticatorClientPinParameters, + ) -> Result<(), Ctap2StatusCode> { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_uv_auth_param, + new_pin_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; + let new_pin_enc = ok_or_missing(new_pin_enc)?; + + if persistent_store.pin_hash()?.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + shared_secret.verify(&new_pin_enc, &pin_uv_auth_param)?; + + check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; + persistent_store.reset_pin_retries()?; + Ok(()) + } + + fn process_change_pin( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + client_pin_params: AuthenticatorClientPinParameters, + ) -> Result<(), Ctap2StatusCode> { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_uv_auth_param, + new_pin_enc, + pin_hash_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; + let new_pin_enc = ok_or_missing(new_pin_enc)?; + let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + + if persistent_store.pin_retries()? == 0 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + } + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + let mut auth_param_data = new_pin_enc.clone(); + auth_param_data.extend(&pin_hash_enc); + shared_secret.verify(&auth_param_data, &pin_uv_auth_param)?; + self.verify_pin_hash_enc( + rng, + persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc, + )?; + + check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); + Ok(()) + } + + fn process_get_pin_token( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, + ) -> Result { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_hash_enc, + permissions, + permissions_rp_id, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if permissions.is_some() || permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + if persistent_store.pin_retries()? == 0 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + } + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + self.verify_pin_hash_enc( + rng, + persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc, + )?; + if persistent_store.has_force_pin_change()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); + self.pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(now); + self.pin_uv_auth_token_state.set_default_permissions(); + let pin_uv_auth_token = shared_secret.encrypt( + rng, + self.get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token(), + )?; + + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_uv_auth_token: Some(pin_uv_auth_token), + retries: None, + power_cycle_state: None, + }) + } + + fn process_get_pin_uv_auth_token_using_uv_with_permissions( + &self, + // If you want to support local user verification, implement this function. + // Lacking a fingerprint reader, this subcommand is currently unsupported. + _client_pin_params: AuthenticatorClientPinParameters, + ) -> Result { + // User verification is only supported through PIN currently. + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) + } + + fn process_get_uv_retries(&self) -> Result { + // User verification is only supported through PIN currently. + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) + } + + fn process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + mut client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, + ) -> Result { + // Mutating client_pin_params is just an optimization to move it into + // process_get_pin_token, without cloning permissions_rp_id here. + // getPinToken requires permissions* to be None. + let permissions = ok_or_missing(client_pin_params.permissions.take())?; + let permissions_rp_id = client_pin_params.permissions_rp_id.take(); + + if permissions == 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + // This check is not mentioned protocol steps, but mentioned in a side note. + if permissions & 0x03 != 0 && permissions_rp_id.is_none() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + let response = self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?; + self.pin_uv_auth_token_state.set_permissions(permissions); + self.pin_uv_auth_token_state + .set_permissions_rp_id(permissions_rp_id); + + Ok(response) + } + + /// Processes the authenticatorClientPin command. + pub fn process_command( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, + ) -> Result { + let response = match client_pin_params.sub_command { + ClientPinSubCommand::GetPinRetries => { + Some(self.process_get_pin_retries(persistent_store)?) + } + ClientPinSubCommand::GetKeyAgreement => { + Some(self.process_get_key_agreement(client_pin_params)?) + } + ClientPinSubCommand::SetPin => { + self.process_set_pin(persistent_store, client_pin_params)?; + None + } + ClientPinSubCommand::ChangePin => { + self.process_change_pin(rng, persistent_store, client_pin_params)?; + None + } + ClientPinSubCommand::GetPinToken => { + Some(self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?) + } + ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( + self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, + ), + ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( + self.process_get_pin_uv_auth_token_using_pin_with_permissions( + rng, + persistent_store, + client_pin_params, + now, + )?, + ), + }; + Ok(ResponseData::AuthenticatorClientPin(response)) + } + + /// Verifies the HMAC for the pinUvAuthToken of the given version. + pub fn verify_pin_uv_auth_token( + &self, + hmac_contents: &[u8], + pin_uv_auth_param: &[u8], + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> Result<(), Ctap2StatusCode> { + if !self.pin_uv_auth_token_state.is_in_use() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + verify_pin_uv_auth_token( + self.get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token(), + hmac_contents, + pin_uv_auth_param, + pin_uv_auth_protocol, + ) + } + + /// Resets all held state. + pub fn reset(&mut self, rng: &mut impl Rng256) { + self.pin_protocol_v1.regenerate(rng); + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.regenerate(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); + self.consecutive_pin_mismatches = 0; + self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); + } + + /// Verifies, computes and encrypts the HMAC-secret outputs. + /// + /// The salt_enc is + /// - verified with the shared secret and salt_auth, + /// - decrypted with the shared secret, + /// - HMAC'ed with cred_random. + /// The length of the output matches salt_enc and has to be 1 or 2 blocks of + /// 32 byte. + pub fn process_hmac_secret( + &self, + rng: &mut impl Rng256, + hmac_secret_input: GetAssertionHmacSecretInput, + cred_random: &[u8; 32], + ) -> Result, Ctap2StatusCode> { + let GetAssertionHmacSecretInput { + key_agreement, + salt_enc, + salt_auth, + pin_uv_auth_protocol, + } = hmac_secret_input; + let shared_secret = self + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol)?; + shared_secret.verify(&salt_enc, &salt_auth)?; + + let decrypted_salts = shared_secret.decrypt(&salt_enc)?; + if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); + if decrypted_salts.len() == 64 { + let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); + output.append(&mut output2); + } + shared_secret.encrypt(rng, &output) + } + + /// Consumes flags and permissions related to the pinUvAuthToken. + pub fn clear_token_flags(&mut self) { + self.pin_uv_auth_token_state.clear_user_verified_flag(); + self.pin_uv_auth_token_state + .clear_pin_uv_auth_token_permissions_except_lbw(); + } + + /// Updates the running timers, triggers timeout events. + pub fn update_timeouts(&mut self, now: ClockValue) { + self.pin_uv_auth_token_state + .pin_uv_auth_token_usage_timer_observer(now); + } + + /// Checks if user verification is cached for use of the pinUvAuthToken. + pub fn check_user_verified_flag(&mut self) -> Result<(), Ctap2StatusCode> { + if self.pin_uv_auth_token_state.get_user_verified_flag_value() { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + } + + /// Check if the required command's token permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state.has_permission(permission) + } + + /// Check if no RP ID is associated with the token permission. + pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state.has_no_permissions_rp_id() + } + + /// Check if no or the passed RP ID is associated with the token permission. + pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id)) + } + + /// Check if no RP ID is associated with the token permission, or it matches the hash. + pub fn has_no_or_rp_id_hash_permission( + &self, + rp_id_hash: &[u8], + ) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| { + self.pin_uv_auth_token_state + .has_permissions_rp_id_hash(rp_id_hash) + }) + } + + /// Check if the passed RP ID is associated with the token permission. + /// + /// If no RP ID is associated, associate the passed RP ID as a side effect. + pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + if self + .pin_uv_auth_token_state + .has_no_permissions_rp_id() + .is_ok() + { + self.pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from(rp_id))); + return Ok(()); + } + self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id) + } + + #[cfg(test)] + pub fn new_test( + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> ClientPin { + use crypto::rng256::ThreadRng256; + let mut rng = ThreadRng256 {}; + let (key_agreement_key_v1, key_agreement_key_v2) = match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(&mut rng)), + PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(&mut rng), key_agreement_key), + }; + let mut pin_uv_auth_token_state = PinUvAuthTokenState::new(); + pin_uv_auth_token_state.set_permissions(0xFF); + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + pin_uv_auth_token_state.begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); + ClientPin { + pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), + pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), + consecutive_pin_mismatches: 0, + pin_uv_auth_token_state, + } + } +} + +#[cfg(test)] +mod test { + use super::super::pin_protocol::authenticate_pin_uv_auth_token; + use super::*; + use alloc::vec; + use crypto::rng256::ThreadRng256; + use libtock_drivers::timer::Duration; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + + /// Stores a PIN hash corresponding to the dummy PIN "1234". + fn set_standard_pin(persistent_store: &mut PersistentStore) { + let mut pin = [0u8; 64]; + pin[..4].copy_from_slice(b"1234"); + let mut pin_hash = [0u8; 16]; + pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); + persistent_store.set_pin(&pin_hash, 4).unwrap(); + } + + /// Fails on PINs bigger than 64 bytes. + fn encrypt_pin(shared_secret: &dyn SharedSecret, pin: Vec) -> Vec { + assert!(pin.len() <= 64); + let mut rng = ThreadRng256 {}; + let mut padded_pin = [0u8; 64]; + padded_pin[..pin.len()].copy_from_slice(&pin[..]); + shared_secret.encrypt(&mut rng, &padded_pin).unwrap() + } + + /// Generates a ClientPin instance and a shared secret for testing. + /// + /// The shared secret for the desired PIN protocol is generated in a + /// handshake with itself. The other protocol has a random private key, so + /// tests using the wrong combination of PIN protocol and shared secret + /// should fail. + fn create_client_pin_and_shared_secret( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> (ClientPin, Box) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let key_agreement = CoseKey::from(pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol) + .unwrap(); + (client_pin, shared_secret) + } + + /// Generates standard input parameters to the ClientPin command. + /// + /// All fields are populated for simplicity, even though most are unused. + fn create_client_pin_and_parameters( + pin_uv_auth_protocol: PinUvAuthProtocol, + sub_command: ClientPinSubCommand, + ) -> (ClientPin, AuthenticatorClientPinParameters) { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + + let pin = b"1234"; + let mut padded_pin = [0u8; 64]; + padded_pin[..pin.len()].copy_from_slice(&pin[..]); + let pin_hash = Sha256::hash(&padded_pin); + let new_pin_enc = shared_secret + .as_ref() + .encrypt(&mut rng, &padded_pin) + .unwrap(); + let pin_uv_auth_param = shared_secret.as_ref().authenticate(&new_pin_enc); + let pin_hash_enc = shared_secret + .as_ref() + .encrypt(&mut rng, &pin_hash[..16]) + .unwrap(); + let (permissions, permissions_rp_id) = match sub_command { + ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions + | ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { + (Some(0x03), Some("example.com".to_string())) + } + _ => (None, None), + }; + let params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command, + key_agreement: Some( + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + ), + pin_uv_auth_param: Some(pin_uv_auth_param), + new_pin_enc: Some(new_pin_enc), + pin_hash_enc: Some(pin_hash_enc), + permissions, + permissions_rp_id, + }; + (client_pin, params) + } + + #[test] + fn test_mix_pin_protocols() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let pin_protocol_v1 = client_pin.get_pin_protocol(PinUvAuthProtocol::V1); + let pin_protocol_v2 = client_pin.get_pin_protocol(PinUvAuthProtocol::V2); + let message = vec![0xAA; 16]; + + let shared_secret_v1 = pin_protocol_v1 + .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V1) + .unwrap(); + let shared_secret_v2 = pin_protocol_v2 + .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V2) + .unwrap(); + let ciphertext = shared_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = shared_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + + let fake_secret_v1 = pin_protocol_v1 + .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V1) + .unwrap(); + let ciphertext = shared_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = fake_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = fake_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + + let fake_secret_v2 = pin_protocol_v2 + .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V2) + .unwrap(); + let ciphertext = shared_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = fake_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = fake_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + } + + fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + let pin_protocol = client_pin.get_pin_protocol(pin_uv_auth_protocol); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); + // The PIN is "1234". + let pin_hash = [ + 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, + 0xC4, 0x12, + ]; + persistent_store.set_pin(&pin_hash, 4).unwrap(); + + let pin_hash_enc = shared_secret.as_ref().encrypt(&mut rng, &pin_hash).unwrap(); + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Ok(()) + ); + + let pin_hash_enc = vec![0xEE; 16]; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + + let pin_hash_enc = shared_secret.as_ref().encrypt(&mut rng, &pin_hash).unwrap(); + client_pin.consecutive_pin_mismatches = 3; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) + ); + client_pin.consecutive_pin_mismatches = 0; + + let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + + let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + fn test_verify_pin_hash_enc_v1() { + test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V1); + } + + #[test] + fn test_verify_pin_hash_enc_v2() { + test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_retries(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinRetries, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: None, + pin_uv_auth_token: None, + retries: Some(persistent_store.pin_retries().unwrap() as u64), + power_cycle_state: Some(false), + }); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + + client_pin.consecutive_pin_mismatches = 3; + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: None, + pin_uv_auth_token: None, + retries: Some(persistent_store.pin_retries().unwrap() as u64), + power_cycle_state: Some(true), + }); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + } + + #[test] + fn test_process_get_pin_retries_v1() { + test_helper_process_get_pin_retries(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_retries_v2() { + test_helper_process_get_pin_retries(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_key_agreement(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetKeyAgreement, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: params.key_agreement.clone(), + pin_uv_auth_token: None, + retries: None, + power_cycle_state: None, + }); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + } + + #[test] + fn test_process_get_key_agreement_v1() { + test_helper_process_get_key_agreement(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_key_agreement_v2() { + test_helper_process_get_key_agreement(PinUvAuthProtocol::V2); + } + + fn test_helper_process_set_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = + create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::SetPin); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Ok(ResponseData::AuthenticatorClientPin(None)) + ); + } + + #[test] + fn test_process_set_pin_v1() { + test_helper_process_set_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_set_pin_v2() { + test_helper_process_set_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_process_change_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, mut params) = + create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::ChangePin); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + + let mut auth_param_data = params.new_pin_enc.clone().unwrap(); + auth_param_data.extend(params.pin_hash_enc.as_ref().unwrap()); + let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); + params.pin_uv_auth_param = Some(pin_uv_auth_param); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), + Ok(ResponseData::AuthenticatorClientPin(None)) + ); + + let mut bad_params = params.clone(); + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + + while persistent_store.pin_retries().unwrap() > 0 { + persistent_store.decr_pin_retries().unwrap(); + } + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) + ); + } + + #[test] + fn test_process_change_pin_v1() { + test_helper_process_change_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_change_pin_v2() { + test_helper_process_change_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_token(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinToken, + ); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + + let response = client_pin + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE, + ) + .unwrap(); + let encrypted_token = match response { + ResponseData::AuthenticatorClientPin(Some(response)) => { + response.pin_uv_auth_token.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!( + &shared_secret.decrypt(&encrypted_token).unwrap(), + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token() + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_no_permissions_rp_id(), + Ok(()) + ); + + let mut bad_params = params; + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + fn test_process_get_pin_token_v1() { + test_helper_process_get_pin_token(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_token_v2() { + test_helper_process_get_pin_token(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_token_force_pin_change(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinToken, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + + assert_eq!(persistent_store.force_pin_change(), Ok(())); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + ); + } + + #[test] + fn test_process_get_pin_token_force_pin_change_v1() { + test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_token_force_pin_change_v2() { + test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + + let response = client_pin + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE, + ) + .unwrap(); + let encrypted_token = match response { + ResponseData::AuthenticatorClientPin(Some(response)) => { + response.pin_uv_auth_token.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!( + &shared_secret.decrypt(&encrypted_token).unwrap(), + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token() + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) + ); + + let mut bad_params = params.clone(); + bad_params.permissions = Some(0x00); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + let mut bad_params = params.clone(); + bad_params.permissions_rp_id = None; + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + let mut bad_params = params; + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v1() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v2() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + + assert_eq!(persistent_store.force_pin_change(), Ok(())); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v1() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + PinUvAuthProtocol::V1, + ); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v2() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + PinUvAuthProtocol::V2, + ); + } + + fn test_helper_decrypt_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); + + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"1234".to_vec()); + assert_eq!( + decrypt_pin(shared_secret.as_ref(), new_pin_enc), + Ok(b"1234".to_vec()), + ); + + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"123".to_vec()); + assert_eq!( + decrypt_pin(shared_secret.as_ref(), new_pin_enc), + Ok(b"123".to_vec()), + ); + + // Encrypted PIN is too short. + let new_pin_enc = vec![0x44; 63]; + assert_eq!( + decrypt_pin(shared_secret.as_ref(), new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // Encrypted PIN is too long. + let new_pin_enc = vec![0x44; 65]; + assert_eq!( + decrypt_pin(shared_secret.as_ref(), new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + fn test_decrypt_pin_v1() { + test_helper_decrypt_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_decrypt_pin_v2() { + test_helper_decrypt_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_check_and_store_new_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); + + let test_cases = vec![ + // Accept PIN "1234". + (b"1234".to_vec(), Ok(())), + // Reject PIN "123" since it is too short. + ( + b"123".to_vec(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + // Reject PIN "12'\0'4" (a zero byte at index 2). + ( + b"12\04".to_vec(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + // PINs must be at most 63 bytes long, to allow for a trailing 0u8 padding. + ( + vec![0x30; 64], + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + ]; + for (pin, result) in test_cases { + let old_pin_hash = persistent_store.pin_hash().unwrap(); + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin); + + assert_eq!( + check_and_store_new_pin(&mut persistent_store, shared_secret.as_ref(), new_pin_enc), + result + ); + if result.is_ok() { + assert_ne!(old_pin_hash, persistent_store.pin_hash().unwrap()); + } else { + assert_eq!(old_pin_hash, persistent_store.pin_hash().unwrap()); + } + } + } + + #[test] + fn test_check_and_store_new_pin_v1() { + test_helper_check_and_store_new_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_check_and_store_new_pin_v2() { + test_helper_check_and_store_new_pin(PinUvAuthProtocol::V2); + } + + /// Generates valid inputs for process_hmac_secret and returns the output. + fn get_process_hmac_secret_decrypted_output( + pin_uv_auth_protocol: PinUvAuthProtocol, + cred_random: &[u8; 32], + salt: Vec, + ) -> Result, Ctap2StatusCode> { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + + let salt_enc = shared_secret.as_ref().encrypt(&mut rng, &salt).unwrap(); + let salt_auth = shared_secret.authenticate(&salt_enc); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let output = client_pin.process_hmac_secret(&mut rng, hmac_secret_input, cred_random); + output.map(|v| shared_secret.as_ref().decrypt(&v).unwrap()) + } + + fn test_helper_process_hmac_secret_bad_salt_auth(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + let cred_random = [0xC9; 32]; + + let salt_enc = vec![0x01; 32]; + let mut salt_auth = shared_secret.authenticate(&salt_enc); + salt_auth[0] ^= 0x01; + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let output = client_pin.process_hmac_secret(&mut rng, hmac_secret_input, &cred_random); + assert_eq!(output, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)); + } + + #[test] + fn test_process_hmac_secret_bad_salt_auth_v1() { + test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_bad_salt_auth_v2() { + test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_one_salt(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let salt = vec![0x01; 32]; + let expected_output = hmac_256::(&cred_random, &salt); + + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt) + .unwrap(); + assert_eq!(&output, &expected_output); + } + + #[test] + fn test_process_hmac_secret_one_salt_v1() { + test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_one_salt_v2() { + test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_two_salts(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let salt1 = [0x01; 32]; + let salt2 = [0x02; 32]; + let expected_output1 = hmac_256::(&cred_random, &salt1); + let expected_output2 = hmac_256::(&cred_random, &salt2); + + let mut salt12 = vec![0x00; 64]; + salt12[..32].copy_from_slice(&salt1); + salt12[32..].copy_from_slice(&salt2); + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt12) + .unwrap(); + assert_eq!(&output[..32], &expected_output1); + assert_eq!(&output[32..], &expected_output2); + + let mut salt02 = vec![0x00; 64]; + salt02[32..].copy_from_slice(&salt2); + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt02) + .unwrap(); + assert_eq!(&output[32..], &expected_output2); + + let mut salt10 = vec![0x00; 64]; + salt10[..32].copy_from_slice(&salt1); + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt10) + .unwrap(); + assert_eq!(&output[..32], &expected_output1); + } + + #[test] + fn test_process_hmac_secret_two_salts_v1() { + test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_two_salts_v2() { + test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_wrong_length(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let output = get_process_hmac_secret_decrypted_output( + pin_uv_auth_protocol, + &cred_random, + vec![0x5E; 48], + ); + assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); + } + + #[test] + fn test_process_hmac_secret_wrong_length_v1() { + test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_wrong_length_v2() { + test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V2); + } + + #[test] + fn test_has_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + client_pin.pin_uv_auth_token_state.set_permissions(0x7F); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + client_pin.pin_uv_auth_token_state.set_permissions(0x00); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + } + + #[test] + fn test_has_no_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); + assert_eq!( + client_pin.has_no_rp_id_permission(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_has_no_or_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); + assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); + assert_eq!( + client_pin.has_no_or_rp_id_permission("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_has_no_or_rp_id_hash_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + let rp_id_hash = Sha256::hash(b"example.com"); + assert_eq!( + client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); + assert_eq!( + client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!( + client_pin.has_no_or_rp_id_hash_permission(&[0x4A; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_ensure_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) + ); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); + assert_eq!( + client_pin.ensure_rp_id_permission("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + client_pin + .pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + let pin_uv_auth_token_v2 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V2) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v2 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V2); + let pin_uv_auth_param_v1_from_v2_token = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V1); + let pin_uv_auth_param_v2_from_v1_token = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V2); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Ok(()) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2, + PinUvAuthProtocol::V2 + ), + Ok(()) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V2 + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1_from_v2_token, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2_from_v1_token, + PinUvAuthProtocol::V2 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token_not_in_use() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_reset() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + let public_key_v1 = client_pin.pin_protocol_v1.get_public_key(); + let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); + let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); + let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); + client_pin.pin_uv_auth_token_state.set_permissions(0xFF); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from("example.com"))); + client_pin.reset(&mut rng); + assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); + assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); + assert_ne!( + &token_v1, + client_pin.pin_protocol_v1.get_pin_uv_auth_token() + ); + assert_ne!( + &token_v2, + client_pin.pin_protocol_v2.get_pin_uv_auth_token() + ); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + } + + #[test] + fn test_update_timeouts() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) + ); + + let timeout = DUMMY_CLOCK_VALUE.wrapping_add(Duration::from_ms(30001)); + client_pin.update_timeouts(timeout); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_clear_token_flags() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!(client_pin.check_user_verified_flag(), Ok(())); + + client_pin.clear_token_flags(); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + assert_eq!( + client_pin.check_user_verified_flag(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } +} diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 5c58234..387a687 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -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,25 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::customization::{MAX_CREDENTIAL_COUNT_IN_LIST, MAX_LARGE_BLOB_ARRAY_SIZE}; use super::data_formats::{ - extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned, - ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, + extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, + PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; +use super::key_material; use super::status_code::Ctap2StatusCode; use alloc::string::String; use alloc::vec::Vec; +use arrayref::array_ref; use cbor::destructure_cbor_map; use core::convert::TryFrom; -// Depending on your memory, you can use Some(n) to limit request sizes in -// MakeCredential and GetAssertion. This affects allowList and excludeList. -// You might also want to set the max credential size in process_get_info then. -pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; +// This constant is a consequence of the structure of messages. +const MIN_LARGE_BLOB_LEN: usize = 17; // CTAP specification (version 20190130) section 6.1 -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub enum Command { AuthenticatorMakeCredential(AuthenticatorMakeCredentialParameters), AuthenticatorGetAssertion(AuthenticatorGetAssertionParameters), @@ -38,9 +41,12 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, - #[cfg(feature = "with_ctap2_1")] + AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, - // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) + AuthenticatorLargeBlobs(AuthenticatorLargeBlobsParameters), + AuthenticatorConfig(AuthenticatorConfigParameters), + // Vendor specific commands + AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } impl From for Ctap2StatusCode { @@ -49,22 +55,22 @@ impl From for Ctap2StatusCode { } } -// TODO: Remove this `allow(dead_code)` once the constants are used. -#[allow(dead_code)] impl Command { const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01; const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02; const AUTHENTICATOR_GET_INFO: u8 = 0x04; const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_RESET: u8 = 0x07; - // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; - const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; - const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; - const AUTHENTICATOR_SELECTION: u8 = 0xB0; - const AUTHENTICATOR_CONFIG: u8 = 0xC0; - const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; - const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; + // Implement Bio Enrollment when your hardware supports biometrics. + const _AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; + const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A; + const AUTHENTICATOR_SELECTION: u8 = 0x0B; + const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C; + const AUTHENTICATOR_CONFIG: u8 = 0x0D; + const _AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; + const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40; + const _AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; pub fn deserialize(bytes: &[u8]) -> Result { if bytes.is_empty() { @@ -104,28 +110,53 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } - #[cfg(feature = "with_ctap2_1")] + Command::AUTHENTICATOR_CREDENTIAL_MANAGEMENT => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorCredentialManagement( + AuthenticatorCredentialManagementParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_SELECTION => { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_LARGE_BLOBS => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorLargeBlobs( + AuthenticatorLargeBlobsParameters::try_from(decoded_cbor)?, + )) + } + Command::AUTHENTICATOR_CONFIG => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorConfig( + AuthenticatorConfigParameters::try_from(decoded_cbor)?, + )) + } + Command::AUTHENTICATOR_VENDOR_CONFIGURE => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters::try_from(decoded_cbor)?, + )) + } _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND), } } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, pub user: PublicKeyCredentialUserEntity, pub pub_key_cred_params: Vec, pub exclude_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: MakeCredentialExtensions, + // Same for options, use defaults when not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -134,15 +165,16 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => client_data_hash, - 2 => rp, - 3 => user, - 4 => cred_param_vec, - 5 => exclude_list, - 6 => extensions, - 7 => options, - 8 => pin_uv_auth_param, - 9 => pin_uv_auth_protocol, + 0x01 => client_data_hash, + 0x02 => rp, + 0x03 => user, + 0x04 => cred_param_vec, + 0x05 => exclude_list, + 0x06 => extensions, + 0x07 => options, + 0x08 => pin_uv_auth_param, + 0x09 => pin_uv_auth_protocol, + 0x0A => enterprise_attestation, } = extract_map(cbor_value)?; } @@ -172,18 +204,19 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let extensions = extensions .map(MakeCredentialExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => MakeCredentialOptions::try_from(entry)?, - None => MakeCredentialOptions { - rk: false, - uv: false, - }, - }; + let options = options + .map(MakeCredentialOptions::try_from) + .transpose()? + .unwrap_or_default(); let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; - let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; + let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -195,20 +228,22 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, pub allow_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: GetAssertionExtensions, + // Same for options, use defaults when not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorGetAssertionParameters { @@ -217,13 +252,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => rp_id, - 2 => client_data_hash, - 3 => allow_list, - 4 => extensions, - 5 => options, - 6 => pin_uv_auth_param, - 7 => pin_uv_auth_protocol, + 0x01 => rp_id, + 0x02 => client_data_hash, + 0x03 => allow_list, + 0x04 => extensions, + 0x05 => options, + 0x06 => pin_uv_auth_param, + 0x07 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } @@ -246,18 +281,18 @@ impl TryFrom for AuthenticatorGetAssertionParameters { let extensions = extensions .map(GetAssertionExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => GetAssertionOptions::try_from(entry)?, - None => GetAssertionOptions { - up: true, - uv: false, - }, - }; + let options = options + .map(GetAssertionOptions::try_from) + .transpose()? + .unwrap_or_default(); let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; - let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; Ok(AuthenticatorGetAssertionParameters { rp_id, @@ -271,21 +306,15 @@ impl TryFrom for AuthenticatorGetAssertionParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { - pub pin_protocol: u64, + pub pin_uv_auth_protocol: PinUvAuthProtocol, pub sub_command: ClientPinSubCommand, pub key_agreement: Option, - pub pin_auth: Option>, + pub pin_uv_auth_param: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, - #[cfg(feature = "with_ctap2_1")] - pub min_pin_length: Option, - #[cfg(feature = "with_ctap2_1")] - pub min_pin_length_rp_ids: Option>, - #[cfg(feature = "with_ctap2_1")] pub permissions: Option, - #[cfg(feature = "with_ctap2_1")] pub permissions_rp_id: Option, } @@ -293,85 +322,261 @@ impl TryFrom for AuthenticatorClientPinParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - #[cfg(not(feature = "with_ctap2_1"))] destructure_cbor_map! { let { - 1 => pin_protocol, - 2 => sub_command, - 3 => key_agreement, - 4 => pin_auth, - 5 => new_pin_enc, - 6 => pin_hash_enc, - } = extract_map(cbor_value)?; - } - #[cfg(feature = "with_ctap2_1")] - destructure_cbor_map! { - let { - 1 => pin_protocol, - 2 => sub_command, - 3 => key_agreement, - 4 => pin_auth, - 5 => new_pin_enc, - 6 => pin_hash_enc, - 7 => min_pin_length, - 8 => min_pin_length_rp_ids, - 9 => permissions, - 10 => permissions_rp_id, + 0x01 => pin_uv_auth_protocol, + 0x02 => sub_command, + 0x03 => key_agreement, + 0x04 => pin_uv_auth_param, + 0x05 => new_pin_enc, + 0x06 => pin_hash_enc, + 0x09 => permissions, + 0x0A => permissions_rp_id, } = extract_map(cbor_value)?; } - let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; + let pin_uv_auth_protocol = + PinUvAuthProtocol::try_from(ok_or_missing(pin_uv_auth_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; - let key_agreement = key_agreement.map(extract_map).transpose()?.map(CoseKey); - let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?; let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?; - #[cfg(feature = "with_ctap2_1")] - let min_pin_length = min_pin_length - .map(extract_unsigned) - .transpose()? - .map(u8::try_from) - .transpose() - .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - #[cfg(feature = "with_ctap2_1")] - let min_pin_length_rp_ids = match min_pin_length_rp_ids { - Some(entry) => Some( - extract_array(entry)? - .into_iter() - .map(extract_text_string) - .collect::, Ctap2StatusCode>>()?, - ), - None => None, - }; - #[cfg(feature = "with_ctap2_1")] // We expect a bit field of 8 bits, and drop everything else. // This means we ignore extensions in future versions. let permissions = permissions .map(extract_unsigned) .transpose()? .map(|p| p as u8); - #[cfg(feature = "with_ctap2_1")] let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; Ok(AuthenticatorClientPinParameters { - pin_protocol, + pin_uv_auth_protocol, sub_command, key_agreement, - pin_auth, + pin_uv_auth_param, new_pin_enc, pin_hash_enc, - #[cfg(feature = "with_ctap2_1")] - min_pin_length, - #[cfg(feature = "with_ctap2_1")] - min_pin_length_rp_ids, - #[cfg(feature = "with_ctap2_1")] permissions, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id, }) } } +#[derive(Debug, PartialEq)] +pub struct AuthenticatorLargeBlobsParameters { + pub get: Option, + pub set: Option>, + pub offset: usize, + pub length: Option, + pub pin_uv_auth_param: Option>, + pub pin_uv_auth_protocol: Option, +} + +impl TryFrom for AuthenticatorLargeBlobsParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => get, + 0x02 => set, + 0x03 => offset, + 0x04 => length, + 0x05 => pin_uv_auth_param, + 0x06 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } + + // careful: some missing parameters here are CTAP1_ERR_INVALID_PARAMETER + let get = get.map(extract_unsigned).transpose()?.map(|u| u as usize); + let set = set.map(extract_byte_string).transpose()?; + let offset = + extract_unsigned(offset.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?)? as usize; + let length = length + .map(extract_unsigned) + .transpose()? + .map(|u| u as usize); + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; + + if get.is_none() && set.is_none() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if get.is_some() && set.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if get.is_some() + && (length.is_some() || pin_uv_auth_param.is_some() || pin_uv_auth_protocol.is_some()) + { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if set.is_some() && offset == 0 { + match length { + None => return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + Some(len) if len > MAX_LARGE_BLOB_ARRAY_SIZE => { + return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL) + } + Some(len) if len < MIN_LARGE_BLOB_LEN => { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } + Some(_) => (), + } + } + if set.is_some() && offset != 0 && length.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + Ok(AuthenticatorLargeBlobsParameters { + get, + set, + offset, + length, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct AuthenticatorConfigParameters { + pub sub_command: ConfigSubCommand, + pub sub_command_params: Option, + pub pin_uv_auth_param: Option>, + pub pin_uv_auth_protocol: Option, +} + +impl TryFrom for AuthenticatorConfigParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_uv_auth_param, + 0x04 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } + + let sub_command = ConfigSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = match sub_command { + ConfigSubCommand::SetMinPinLength => Some(ConfigSubCommandParams::SetMinPinLength( + SetMinPinLengthParams::try_from(ok_or_missing(sub_command_params)?)?, + )), + _ => None, + }; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; + + Ok(AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct AuthenticatorAttestationMaterial { + pub certificate: Vec, + pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], +} + +impl TryFrom for AuthenticatorAttestationMaterial { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => certificate, + 0x02 => private_key, + } = extract_map(cbor_value)?; + } + let certificate = extract_byte_string(ok_or_missing(certificate)?)?; + let private_key = extract_byte_string(ok_or_missing(private_key)?)?; + if private_key.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let private_key = array_ref!(private_key, 0, key_material::ATTESTATION_PRIVATE_KEY_LENGTH); + Ok(AuthenticatorAttestationMaterial { + certificate, + private_key: *private_key, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct AuthenticatorCredentialManagementParameters { + pub sub_command: CredentialManagementSubCommand, + pub sub_command_params: Option, + pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_param: Option>, +} + +impl TryFrom for AuthenticatorCredentialManagementParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_uv_auth_protocol, + 0x04 => pin_uv_auth_param, + } = extract_map(cbor_value)?; + } + + let sub_command = CredentialManagementSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = sub_command_params + .map(CredentialManagementSubCommandParameters::try_from) + .transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + + Ok(AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct AuthenticatorVendorConfigureParameters { + pub lockdown: bool, + pub attestation_material: Option, +} + +impl TryFrom for AuthenticatorVendorConfigureParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => lockdown, + 0x02 => attestation_material, + } = extract_map(cbor_value)?; + } + let lockdown = lockdown.map_or(Ok(false), extract_bool)?; + let attestation_material = attestation_material + .map(AuthenticatorAttestationMaterial::try_from) + .transpose()?; + Ok(AuthenticatorVendorConfigureParameters { + lockdown, + attestation_material, + }) + } +} + #[cfg(test)] mod test { use super::super::data_formats::{ @@ -380,28 +585,29 @@ mod test { }; use super::super::ES256_CRED_PARAM; use super::*; - use alloc::collections::BTreeMap; use cbor::{cbor_array, cbor_map}; + use crypto::rng256::ThreadRng256; #[test] fn test_from_cbor_make_credential_parameters() { let cbor_value = cbor_map! { - 1 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], - 2 => cbor_map! { + 0x01 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], + 0x02 => cbor_map! { "id" => "example.com", - "name" => "Example", "icon" => "example.com/icon.png", + "name" => "Example", }, - 3 => cbor_map! { + 0x03 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png", "name" => "foo", "displayName" => "bar", - "icon" => "example.com/foo/icon.png", }, - 4 => cbor_array![ES256_CRED_PARAM], - 5 => cbor_array![], - 8 => vec![0x12, 0x34], - 9 => 1, + 0x04 => cbor_array![ES256_CRED_PARAM], + 0x05 => cbor_array![], + 0x08 => vec![0x12, 0x34], + 0x09 => 1, + 0x0A => 2, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -431,10 +637,11 @@ mod test { user, pub_key_cred_params: vec![ES256_CRED_PARAM], exclude_list: Some(vec![]), - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + enterprise_attestation: Some(2), }; assert_eq!( @@ -446,15 +653,15 @@ mod test { #[test] fn test_from_cbor_get_assertion_parameters() { let cbor_value = cbor_map! { - 1 => "example.com", - 2 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], - 3 => cbor_array![ cbor_map! { - "type" => "public-key", + 0x01 => "example.com", + 0x02 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], + 0x03 => cbor_array![ cbor_map! { "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], } ], - 6 => vec![0x12, 0x34], - 7 => 1, + 0x06 => vec![0x12, 0x34], + 0x07 => 1, }; let returned_get_assertion_parameters = AuthenticatorGetAssertionParameters::try_from(cbor_value).unwrap(); @@ -477,10 +684,10 @@ mod test { rp_id, client_data_hash, allow_list: Some(vec![pub_key_cred_descriptor]), - extensions: None, + extensions: GetAssertionExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( @@ -491,53 +698,38 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { - // TODO(kaczmarczyck) inline the #cfg when #128 is resolved: - // https://github.com/google/OpenSK/issues/128 - #[cfg(not(feature = "with_ctap2_1"))] + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cbor_value = cbor_map! { - 1 => 1, - 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor_map!{}, - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], + 0x01 => 1, + 0x02 => ClientPinSubCommand::GetPinRetries, + 0x03 => cbor::Value::from(cose_key.clone()), + 0x04 => vec! [0xBB], + 0x05 => vec! [0xCC], + 0x06 => vec! [0xDD], + 0x09 => 0x03, + 0x0A => "example.com", }; - #[cfg(feature = "with_ctap2_1")] - let cbor_value = cbor_map! { - 1 => 1, - 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor_map!{}, - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], - 7 => 4, - 8 => cbor_array!["example.com"], - 9 => 0x03, - 10 => "example.com", - }; - let returned_pin_protocol_parameters = + let returned_client_pin_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); - let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { - pin_protocol: 1, + let expected_client_pin_parameters = AuthenticatorClientPinParameters { + pin_uv_auth_protocol: PinUvAuthProtocol::V1, sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: Some(CoseKey(BTreeMap::new())), - pin_auth: Some(vec![0xBB]), + key_agreement: Some(cose_key), + pin_uv_auth_param: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), - #[cfg(feature = "with_ctap2_1")] - min_pin_length: Some(4), - #[cfg(feature = "with_ctap2_1")] - min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), - #[cfg(feature = "with_ctap2_1")] permissions: Some(0x03), - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: Some("example.com".to_string()), }; assert_eq!( - returned_pin_protocol_parameters, - expected_pin_protocol_parameters + returned_client_pin_parameters, + expected_client_pin_parameters ); } @@ -563,11 +755,263 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } - #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_from_cbor_cred_management_parameters() { + let cbor_value = cbor_map! { + 0x01 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 0x02 => cbor_map!{ + 0x01 => vec![0x1D; 32], + }, + 0x03 => 1, + 0x04 => vec! [0x9A; 16], + }; + let returned_cred_management_parameters = + AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap(); + + let params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: None, + user: None, + }; + let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(params), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: Some(vec![0x9A; 16]), + }; + + assert_eq!( + returned_cred_management_parameters, + expected_cred_management_parameters + ); + } + #[test] fn test_deserialize_selection() { let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; let command = Command::deserialize(&cbor_bytes); assert_eq!(command, Ok(Command::AuthenticatorSelection)); } + + #[test] + fn test_from_cbor_large_blobs_parameters() { + // successful get + let cbor_value = cbor_map! { + 0x01 => 2, + 0x03 => 4, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: Some(2), + set: None, + offset: 4, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful first set + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(vec![0x5E]), + offset: 0, + length: Some(MIN_LARGE_BLOB_LEN), + pin_uv_auth_param: Some(vec![0xA9]), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful next set + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 1, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(vec![0x5E]), + offset: 1, + length: None, + pin_uv_auth_param: Some(vec![0xA9]), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // failing with neither get nor set + let cbor_value = cbor_map! { + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and set + let cbor_value = cbor_map! { + 0x01 => 2, + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and length + let cbor_value = cbor_map! { + 0x01 => 2, + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with zero offset and no length present + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with length smaller than minimum + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x04 => MIN_LARGE_BLOB_LEN as u64 - 1, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with non-zero offset and length present + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + fn test_vendor_configure() { + // Incomplete command + let mut cbor_bytes = vec![Command::AUTHENTICATOR_VENDOR_CONFIGURE]; + let command = Command::deserialize(&cbor_bytes); + assert_eq!(command, Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)); + + cbor_bytes.extend(&[0xA1, 0x01, 0xF5]); + let command = Command::deserialize(&cbor_bytes); + assert_eq!( + command, + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None + } + )) + ); + + let dummy_cert = [0xddu8; 20]; + let dummy_pkey = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + + // Attestation key is too short. + let cbor_value = cbor_map! { + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // Missing private key + let cbor_value = cbor_map! { + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + ); + + // Missing certificate + let cbor_value = cbor_map! { + 0x01 => false, + 0x02 => cbor_map! { + 0x02 => dummy_pkey + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + ); + + // Valid + let cbor_value = cbor_map! { + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Ok(AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_pkey + }) + }) + ); + } } diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs new file mode 100644 index 0000000..bdedc6b --- /dev/null +++ b/src/ctap/config_command.rs @@ -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 { + 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 { + 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 { + 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 { + 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>, + ) -> 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) + ); + } +} diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs new file mode 100644 index 0000000..b81648a --- /dev/null +++ b/src/ctap/credential_management.rs @@ -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, 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, +) -> Result { + 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, +) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 = 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 { + 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 { + 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) + ); + } +} diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs new file mode 100644 index 0000000..2587e76 --- /dev/null +++ b/src/ctap/crypto_wrapper.rs @@ -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, 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, 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::>()) +} + +#[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); + } + } +} diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 84c6fb0..d5e60ee 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -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 for Ctap1StatusCode { - type Error = (); - - fn try_from(value: u16) -> Result { - 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 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 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 { - 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::::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::(&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)); } } diff --git a/src/ctap/customization.rs b/src/ctap/customization.rs new file mode 100644 index 0000000..7c28a2f --- /dev/null +++ b/src/ctap/customization.rs @@ -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 = 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 = 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 = 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()); + } + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dacafe5..be76b65 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -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. @@ -13,18 +13,20 @@ // limitations under the License. use super::status_code::Ctap2StatusCode; -use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; -use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; +use cbor::{cbor_array_vec, cbor_map, cbor_map_options, destructure_cbor_map}; use core::convert::TryFrom; use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; +// Used as the identifier for ECDSA in assertion signatures and COSE. +const ES256_ALGORITHM: i64 = -7; + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialRpEntity { pub rp_id: String, pub rp_name: Option, @@ -55,8 +57,18 @@ impl TryFrom for PublicKeyCredentialRpEntity { } } +impl From for cbor::Value { + fn from(entity: PublicKeyCredentialRpEntity) -> Self { + cbor_map_options! { + "id" => entity.rp_id, + "icon" => entity.rp_icon, + "name" => entity.rp_name, + } + } +} + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -95,16 +107,15 @@ impl From for cbor::Value { fn from(entity: PublicKeyCredentialUserEntity) -> Self { cbor_map_options! { "id" => entity.user_id, + "icon" => entity.user_icon, "name" => entity.user_name, "displayName" => entity.user_display_name, - "icon" => entity.user_icon, } } } // https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype -#[derive(Clone, PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub enum PublicKeyCredentialType { PublicKey, // This is the default for all strings not covered above. @@ -136,8 +147,7 @@ impl TryFrom for PublicKeyCredentialType { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -163,14 +173,14 @@ impl TryFrom for PublicKeyCredentialParameter { impl From for cbor::Value { fn from(cred_param: PublicKeyCredentialParameter) -> Self { cbor_map_options! { - "type" => cred_param.cred_type, "alg" => cred_param.alg, + "type" => cred_param.cred_type, } } } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -207,7 +217,7 @@ impl TryFrom for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -251,17 +261,20 @@ impl TryFrom for PublicKeyCredentialDescriptor { impl From for cbor::Value { fn from(desc: PublicKeyCredentialDescriptor) -> Self { cbor_map_options! { - "type" => desc.key_type, "id" => desc.key_id, + "type" => desc.key_type, "transports" => desc.transports.map(|vec| cbor_array_vec!(vec)), } } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, + pub min_pin_length: bool, + pub cred_blob: Option>, + pub large_blob_key: Option, } impl TryFrom for MakeCredentialExtensions { @@ -270,8 +283,11 @@ impl TryFrom for MakeCredentialExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "credProtect" => cred_protect, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, + "minPinLength" => min_pin_length, } = extract_map(cbor_value)?; } @@ -279,16 +295,29 @@ impl TryFrom for MakeCredentialExtensions { let cred_protect = cred_protect .map(CredentialProtectionPolicy::try_from) .transpose()?; + let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let large_blob_key = large_blob_key.map(extract_bool).transpose()?; + if let Some(large_blob_key) = large_blob_key { + if !large_blob_key { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + } Ok(Self { hmac_secret, cred_protect, + min_pin_length, + cred_blob, + large_blob_key, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct GetAssertionExtensions { pub hmac_secret: Option, + pub cred_blob: bool, + pub large_blob_key: Option, } impl TryFrom for GetAssertionExtensions { @@ -297,22 +326,36 @@ impl TryFrom for GetAssertionExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, } = extract_map(cbor_value)?; } let hmac_secret = hmac_secret .map(GetAssertionHmacSecretInput::try_from) .transpose()?; - Ok(Self { hmac_secret }) + let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?; + let large_blob_key = large_blob_key.map(extract_bool).transpose()?; + if let Some(large_blob_key) = large_blob_key { + if !large_blob_key { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + } + Ok(Self { + hmac_secret, + cred_blob, + large_blob_key, + }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, pub salt_auth: Vec, + pub pin_uv_auth_protocol: PinUvAuthProtocol, } impl TryFrom for GetAssertionHmacSecretInput { @@ -321,25 +364,29 @@ impl TryFrom for GetAssertionHmacSecretInput { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => cose_key, + 1 => key_agreement, 2 => salt_enc, 3 => salt_auth, + 4 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } - let cose_key = extract_map(ok_or_missing(cose_key)?)?; + let key_agreement = CoseKey::try_from(ok_or_missing(key_agreement)?)?; let salt_enc = extract_byte_string(ok_or_missing(salt_enc)?)?; let salt_auth = extract_byte_string(ok_or_missing(salt_auth)?)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.map_or(Ok(PinUvAuthProtocol::V1), PinUvAuthProtocol::try_from)?; Ok(Self { - key_agreement: CoseKey(cose_key), + key_agreement, salt_enc, salt_auth, + pin_uv_auth_protocol, }) } } // Even though options are optional, we can use the default if not present. -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -361,6 +408,7 @@ impl TryFrom for MakeCredentialOptions { Some(options_entry) => extract_bool(options_entry)?, None => false, }; + // In CTAP2.0, the up option is supposed to always fail when present. if let Some(options_entry) = up { if !extract_bool(options_entry)? { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); @@ -374,12 +422,21 @@ impl TryFrom for MakeCredentialOptions { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct GetAssertionOptions { pub up: bool, pub uv: bool, } +impl Default for GetAssertionOptions { + fn default() -> Self { + GetAssertionOptions { + up: true, + uv: false, + } + } +} + impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; @@ -410,8 +467,7 @@ impl TryFrom for GetAssertionOptions { } // https://www.w3.org/TR/webauthn/#packed-attestation -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct PackedAttestationStatement { pub alg: i64, pub sig: Vec, @@ -430,10 +486,9 @@ impl From for cbor::Value { } } -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub enum SignatureAlgorithm { - ES256 = ecdsa::PubKey::ES256_ALGORITHM as isize, + ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. // Unknown types should be ignored, instead of returning errors. Unknown = 0, @@ -450,18 +505,24 @@ impl TryFrom for SignatureAlgorithm { fn try_from(cbor_value: cbor::Value) -> Result { match extract_integer(cbor_value)? { - ecdsa::PubKey::ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), + ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), _ => Ok(SignatureAlgorithm::Unknown), } } } -#[derive(Clone, Copy, PartialEq, PartialOrd)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +/// The credProtect extension's policies for resident credentials. +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialProtectionPolicy { + /// The credential is always discoverable, as if it had no protection level. UserVerificationOptional = 0x01, + /// The credential is discoverable with + /// - an allowList, + /// - an excludeList, + /// - user verification. UserVerificationOptionalWithCredentialIdList = 0x02, + /// The credentials is discoverable with user verification only. UserVerificationRequired = 0x03, } @@ -488,9 +549,7 @@ impl TryFrom for CredentialProtectionPolicy { // // Note that we only use the WebAuthn definition as an example. This data-structure is not specified // by FIDO. In particular we may choose how we serialize and deserialize it. -#[derive(Clone)] -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialSource { // TODO function to convert to / from Vec pub key_type: PublicKeyCredentialType, @@ -498,9 +557,13 @@ pub struct PublicKeyCredentialSource { pub private_key: ecdsa::SecKey, // TODO(kaczmarczyck) open for other algorithms pub rp_id: String, pub user_handle: Vec, // not optional, but nullable - pub other_ui: Option, - pub cred_random: Option>, + pub user_display_name: Option, pub cred_protect_policy: Option, + pub creation_order: u64, + pub user_name: Option, + pub user_icon: Option, + pub cred_blob: Option>, + pub large_blob_key: Option>, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -510,16 +573,21 @@ enum PublicKeyCredentialSourceField { PrivateKey = 1, RpId = 2, UserHandle = 3, - OtherUi = 4, - CredRandom = 5, + UserDisplayName = 4, CredProtectPolicy = 6, + CreationOrder = 7, + UserName = 8, + UserIcon = 9, + CredBlob = 10, + LargeBlobKey = 11, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. - // Reserved tags: none. + // Reserved tags: + // - CredRandom = 5, } -impl From for cbor::KeyType { - fn from(field: PublicKeyCredentialSourceField) -> cbor::KeyType { +impl From for cbor::Value { + fn from(field: PublicKeyCredentialSourceField) -> cbor::Value { (field as u64).into() } } @@ -533,9 +601,13 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::PrivateKey => Some(private_key.to_vec()), PublicKeyCredentialSourceField::RpId => Some(credential.rp_id), PublicKeyCredentialSourceField::UserHandle => Some(credential.user_handle), - PublicKeyCredentialSourceField::OtherUi => credential.other_ui, - PublicKeyCredentialSourceField::CredRandom => credential.cred_random, + PublicKeyCredentialSourceField::UserDisplayName => credential.user_display_name, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, + PublicKeyCredentialSourceField::UserName => credential.user_name, + PublicKeyCredentialSourceField::UserIcon => credential.user_icon, + PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => credential.large_blob_key, } } } @@ -550,9 +622,13 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::PrivateKey => private_key, PublicKeyCredentialSourceField::RpId => rp_id, PublicKeyCredentialSourceField::UserHandle => user_handle, - PublicKeyCredentialSourceField::OtherUi => other_ui, - PublicKeyCredentialSourceField::CredRandom => cred_random, + PublicKeyCredentialSourceField::UserDisplayName => user_display_name, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => creation_order, + PublicKeyCredentialSourceField::UserName => user_name, + PublicKeyCredentialSourceField::UserIcon => user_icon, + PublicKeyCredentialSourceField::CredBlob => cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => large_blob_key, } = extract_map(cbor_value)?; } @@ -565,11 +641,15 @@ impl TryFrom for PublicKeyCredentialSource { .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)?; let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; let user_handle = extract_byte_string(ok_or_missing(user_handle)?)?; - let other_ui = other_ui.map(extract_text_string).transpose()?; - let cred_random = cred_random.map(extract_byte_string).transpose()?; + let user_display_name = user_display_name.map(extract_text_string).transpose()?; let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; + let creation_order = creation_order.map_or(Ok(0), extract_unsigned)?; + let user_name = user_name.map(extract_text_string).transpose()?; + let user_icon = user_icon.map(extract_text_string).transpose()?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let large_blob_key = large_blob_key.map(extract_byte_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -586,9 +666,13 @@ impl TryFrom for PublicKeyCredentialSource { private_key, rp_id, user_handle, - other_ui, - cred_random, + user_display_name, cred_protect_policy, + creation_order, + user_name, + user_icon, + cred_blob, + large_blob_key, }) } } @@ -602,71 +686,41 @@ impl PublicKeyCredentialSource { } } -// TODO(kaczmarczyck) we could decide to split this data type up -// It depends on the algorithm though, I think. -// So before creating a mess, this is my workaround. -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] -pub struct CoseKey(pub BTreeMap); - -// This is the algorithm specifier that is supposed to be used in a COSE key -// map. The CTAP specification says -25 which represents ECDH-ES + HKDF-256 -// here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms -// In fact, this is just used for compatibility with older specification versions. -const ECDH_ALGORITHM: i64 = -25; -// This is the identifier used by OpenSSH. To be compatible, we accept both. -const ES256_ALGORITHM: i64 = -7; -const EC2_KEY_TYPE: i64 = 2; -const P_256_CURVE: i64 = 1; - -impl From for CoseKey { - fn from(pk: ecdh::PubKey) -> Self { - let mut x_bytes = [0; ecdh::NBYTES]; - let mut y_bytes = [0; ecdh::NBYTES]; - pk.to_coordinates(&mut x_bytes, &mut y_bytes); - let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); - let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); - // TODO(kaczmarczyck) do not write optional parameters, spec is unclear - let cose_cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - if let cbor::Value::Map(cose_map) = cose_cbor_value { - CoseKey(cose_map) - } else { - unreachable!(); - } - } +// The COSE key is used for both ECDH and ECDSA public keys for transmission. +#[derive(Clone, Debug, PartialEq)] +pub struct CoseKey { + x_bytes: [u8; ecdh::NBYTES], + y_bytes: [u8; ecdh::NBYTES], + algorithm: i64, } -impl TryFrom for ecdh::PubKey { +impl CoseKey { + // This is the algorithm specifier for ECDH. + // CTAP requests -25 which represents ECDH-ES + HKDF-256 here: + // https://www.iana.org/assignments/cose/cose.xhtml#algorithms + const ECDH_ALGORITHM: i64 = -25; + // The parameter behind map key 1. + const EC2_KEY_TYPE: i64 = 2; + // The parameter behind map key -1. + const P_256_CURVE: i64 = 1; +} + +// This conversion accepts both ECDH and ECDSA. +impl TryFrom for CoseKey { type Error = Ctap2StatusCode; - fn try_from(cose_key: CoseKey) -> Result { + fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + // This is sorted correctly, negative encoding is bigger. 1 => key_type, 3 => algorithm, -1 => curve, -2 => x_bytes, -3 => y_bytes, - } = cose_key.0; + } = extract_map(cbor_value)?; } - let key_type = extract_integer(ok_or_missing(key_type)?)?; - if key_type != EC2_KEY_TYPE { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } - let algorithm = extract_integer(ok_or_missing(algorithm)?)?; - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } - let curve = extract_integer(ok_or_missing(curve)?)?; - if curve != P_256_CURVE { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } let x_bytes = extract_byte_string(ok_or_missing(x_bytes)?)?; if x_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -675,15 +729,112 @@ impl TryFrom for ecdh::PubKey { if y_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } + let curve = extract_integer(ok_or_missing(curve)?)?; + if curve != CoseKey::P_256_CURVE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + let key_type = extract_integer(ok_or_missing(key_type)?)?; + if key_type != CoseKey::EC2_KEY_TYPE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + let algorithm = extract_integer(ok_or_missing(algorithm)?)?; + if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } - let x_array_ref = array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES]; - let y_array_ref = array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES]; - ecdh::PubKey::from_coordinates(x_array_ref, y_array_ref) + Ok(CoseKey { + x_bytes: *array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES], + y_bytes: *array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES], + algorithm, + }) + } +} + +impl From for cbor::Value { + fn from(cose_key: CoseKey) -> Self { + let CoseKey { + x_bytes, + y_bytes, + algorithm, + } = cose_key; + + cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => algorithm, + -1 => CoseKey::P_256_CURVE, + -2 => x_bytes, + -3 => y_bytes, + } + } +} + +impl From for CoseKey { + fn from(pk: ecdh::PubKey) -> Self { + let mut x_bytes = [0; ecdh::NBYTES]; + let mut y_bytes = [0; ecdh::NBYTES]; + pk.to_coordinates(&mut x_bytes, &mut y_bytes); + CoseKey { + x_bytes, + y_bytes, + algorithm: CoseKey::ECDH_ALGORITHM, + } + } +} + +impl From for CoseKey { + fn from(pk: ecdsa::PubKey) -> Self { + let mut x_bytes = [0; ecdh::NBYTES]; + let mut y_bytes = [0; ecdh::NBYTES]; + pk.to_coordinates(&mut x_bytes, &mut y_bytes); + CoseKey { + x_bytes, + y_bytes, + algorithm: ES256_ALGORITHM, + } + } +} + +impl TryFrom for ecdh::PubKey { + type Error = Ctap2StatusCode; + + fn try_from(cose_key: CoseKey) -> Result { + let CoseKey { + x_bytes, + y_bytes, + algorithm, + } = cose_key; + + // Since algorithm can be used for different COSE key types, we check + // whether the current type is correct for ECDH. For an OpenSSH bugfix, + // the algorithm ES256_ALGORITHM is allowed here too. + // https://github.com/google/OpenSK/issues/90 + if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes) .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PinUvAuthProtocol { + V1 = 1, + V2 = 2, +} + +impl TryFrom for PinUvAuthProtocol { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + match extract_unsigned(cbor_value)? { + 1 => Ok(PinUvAuthProtocol::V1), + 2 => Ok(PinUvAuthProtocol::V2), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } + } +} + +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { GetPinRetries = 0x01, @@ -691,13 +842,8 @@ pub enum ClientPinSubCommand { SetPin = 0x03, ChangePin = 0x04, GetPinToken = 0x05, - #[cfg(feature = "with_ctap2_1")] GetPinUvAuthTokenUsingUvWithPermissions = 0x06, - #[cfg(feature = "with_ctap2_1")] GetUvRetries = 0x07, - #[cfg(feature = "with_ctap2_1")] - SetMinPinLength = 0x08, - #[cfg(feature = "with_ctap2_1")] GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } @@ -718,53 +864,247 @@ impl TryFrom for ClientPinSubCommand { 0x03 => Ok(ClientPinSubCommand::SetPin), 0x04 => Ok(ClientPinSubCommand::ChangePin), 0x05 => Ok(ClientPinSubCommand::GetPinToken), - #[cfg(feature = "with_ctap2_1")] 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), - #[cfg(feature = "with_ctap2_1")] 0x07 => Ok(ClientPinSubCommand::GetUvRetries), - #[cfg(feature = "with_ctap2_1")] - 0x08 => Ok(ClientPinSubCommand::SetMinPinLength), - #[cfg(feature = "with_ctap2_1")] 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), - #[cfg(feature = "with_ctap2_1")] _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), - #[cfg(not(feature = "with_ctap2_1"))] - _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum ConfigSubCommand { + EnableEnterpriseAttestation = 0x01, + ToggleAlwaysUv = 0x02, + SetMinPinLength = 0x03, + VendorPrototype = 0xFF, +} + +impl From for cbor::Value { + fn from(subcommand: ConfigSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for ConfigSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(ConfigSubCommand::EnableEnterpriseAttestation), + 0x02 => Ok(ConfigSubCommand::ToggleAlwaysUv), + 0x03 => Ok(ConfigSubCommand::SetMinPinLength), + 0xFF => Ok(ConfigSubCommand::VendorPrototype), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ConfigSubCommandParams { + SetMinPinLength(SetMinPinLengthParams), +} + +impl From for cbor::Value { + fn from(params: ConfigSubCommandParams) -> Self { + match params { + ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params) => { + set_min_pin_length_params.into() + } + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SetMinPinLengthParams { + pub new_min_pin_length: Option, + pub min_pin_length_rp_ids: Option>, + pub force_change_pin: Option, +} + +impl TryFrom for SetMinPinLengthParams { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => new_min_pin_length, + 0x02 => min_pin_length_rp_ids, + 0x03 => force_change_pin, + } = extract_map(cbor_value)?; + } + + let new_min_pin_length = new_min_pin_length + .map(extract_unsigned) + .transpose()? + .map(u8::try_from) + .transpose() + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; + let min_pin_length_rp_ids = match min_pin_length_rp_ids { + Some(entry) => Some( + extract_array(entry)? + .into_iter() + .map(extract_text_string) + .collect::, Ctap2StatusCode>>()?, + ), + None => None, + }; + let force_change_pin = force_change_pin.map(extract_bool).transpose()?; + + Ok(Self { + new_min_pin_length, + min_pin_length_rp_ids, + force_change_pin, + }) + } +} + +impl From for cbor::Value { + fn from(params: SetMinPinLengthParams) -> Self { + cbor_map_options! { + 0x01 => params.new_min_pin_length.map(|u| u as u64), + 0x02 => params.min_pin_length_rp_ids.map(|vec| cbor_array_vec!(vec)), + 0x03 => params.force_change_pin, + } + } +} + +/// The level of enterprise attestation allowed in MakeCredential. +#[derive(Debug, PartialEq)] +pub enum EnterpriseAttestationMode { + /// Enterprise attestation is restricted to a list of RP IDs. Add your + /// enterprises domain, e.g. "example.com", to the list below. + VendorFacilitated = 0x01, + /// All relying parties can request an enterprise attestation. The authenticator + /// trusts the platform to filter requests. + PlatformManaged = 0x02, +} + +impl TryFrom for EnterpriseAttestationMode { + type Error = Ctap2StatusCode; + + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(EnterpriseAttestationMode::VendorFacilitated), + 2 => Ok(EnterpriseAttestationMode::PlatformManaged), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum CredentialManagementSubCommand { + GetCredsMetadata = 0x01, + EnumerateRpsBegin = 0x02, + EnumerateRpsGetNextRp = 0x03, + EnumerateCredentialsBegin = 0x04, + EnumerateCredentialsGetNextCredential = 0x05, + DeleteCredential = 0x06, + UpdateUserInformation = 0x07, +} + +impl From for cbor::Value { + fn from(subcommand: CredentialManagementSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for CredentialManagementSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(CredentialManagementSubCommand::GetCredsMetadata), + 0x02 => Ok(CredentialManagementSubCommand::EnumerateRpsBegin), + 0x03 => Ok(CredentialManagementSubCommand::EnumerateRpsGetNextRp), + 0x04 => Ok(CredentialManagementSubCommand::EnumerateCredentialsBegin), + 0x05 => Ok(CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential), + 0x06 => Ok(CredentialManagementSubCommand::DeleteCredential), + 0x07 => Ok(CredentialManagementSubCommand::UpdateUserInformation), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct CredentialManagementSubCommandParameters { + pub rp_id_hash: Option>, + pub credential_id: Option, + pub user: Option, +} + +impl TryFrom for CredentialManagementSubCommandParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => rp_id_hash, + 0x02 => credential_id, + 0x03 => user, + } = extract_map(cbor_value)?; + } + + let rp_id_hash = rp_id_hash.map(extract_byte_string).transpose()?; + let credential_id = credential_id + .map(PublicKeyCredentialDescriptor::try_from) + .transpose()?; + let user = user + .map(PublicKeyCredentialUserEntity::try_from) + .transpose()?; + Ok(Self { + rp_id_hash, + credential_id, + user, + }) + } +} + +impl From for cbor::Value { + fn from(sub_command_params: CredentialManagementSubCommandParameters) -> Self { + cbor_map_options! { + 0x01 => sub_command_params.rp_id_hash, + 0x02 => sub_command_params.credential_id, + 0x03 => sub_command_params.user, } } } pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), + cbor::Value::Unsigned(unsigned) => Ok(unsigned), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } pub(super) fn extract_integer(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => { + cbor::Value::Unsigned(unsigned) => { if unsigned <= core::i64::MAX as u64 { Ok(unsigned as i64) } else { Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE) } } - cbor::Value::KeyValue(cbor::KeyType::Negative(signed)) => Ok(signed), + cbor::Value::Negative(signed) => Ok(signed), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } pub fn extract_byte_string(cbor_value: cbor::Value) -> Result, Ctap2StatusCode> { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string), + cbor::Value::ByteString(byte_string) => Ok(byte_string), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } pub(super) fn extract_text_string(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::TextString(text_string)) => Ok(text_string), + cbor::Value::TextString(text_string) => Ok(text_string), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } @@ -778,7 +1118,7 @@ pub(super) fn extract_array(cbor_value: cbor::Value) -> Result, pub(super) fn extract_map( cbor_value: cbor::Value, -) -> Result, Ctap2StatusCode> { +) -> Result, Ctap2StatusCode> { match cbor_value { cbor::Value::Map(map) => Ok(map), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), @@ -801,10 +1141,9 @@ pub(super) fn ok_or_missing(value_option: Option) -> Result cbor_false!(), - "foo" => b"bar", b"bin" => -42, + "foo" => b"bar", }), - Ok([ + Ok(vec![ (cbor_unsigned!(1), cbor_false!()), - (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), (cbor_bytes_lit!(b"bin"), cbor_int!(-42)), - ] - .iter() - .cloned() - .collect::>()) + (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), + ]) ); } @@ -1078,8 +1414,8 @@ mod test { fn test_from_public_key_credential_rp_entity() { let cbor_rp_entity = cbor_map! { "id" => "example.com", - "name" => "Example", "icon" => "example.com/icon.png", + "name" => "Example", }; let rp_entity = PublicKeyCredentialRpEntity::try_from(cbor_rp_entity); let expected_rp_entity = PublicKeyCredentialRpEntity { @@ -1094,9 +1430,9 @@ mod test { fn test_from_into_public_key_credential_user_entity() { let cbor_user_entity = cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png", "name" => "foo", "displayName" => "bar", - "icon" => "example.com/foo/icon.png", }; let user_entity = PublicKeyCredentialUserEntity::try_from(cbor_user_entity.clone()); let expected_user_entity = PublicKeyCredentialUserEntity { @@ -1127,7 +1463,7 @@ mod test { #[test] fn test_from_into_signature_algorithm() { - let cbor_signature_algorithm: cbor::Value = cbor_int!(ecdsa::PubKey::ES256_ALGORITHM); + let cbor_signature_algorithm: cbor::Value = cbor_int!(ES256_ALGORITHM); let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); let expected_signature_algorithm = SignatureAlgorithm::ES256; assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); @@ -1200,8 +1536,8 @@ mod test { #[test] fn test_from_into_public_key_credential_parameter() { let cbor_credential_parameter = cbor_map! { + "alg" => ES256_ALGORITHM, "type" => "public-key", - "alg" => ecdsa::PubKey::ES256_ALGORITHM, }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); @@ -1217,8 +1553,8 @@ mod test { #[test] fn test_from_into_public_key_credential_descriptor() { let cbor_credential_descriptor = cbor_map! { - "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], }; let credential_descriptor = @@ -1236,42 +1572,85 @@ mod test { #[test] fn test_from_make_credential_extensions() { let cbor_extensions = cbor_map! { - "hmac-secret" => true, + "credBlob" => vec![0xCB], "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, + "hmac-secret" => true, + "largeBlobKey" => true, + "minPinLength" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), + min_pin_length: true, + cred_blob: Some(vec![0xCB]), + large_blob_key: Some(true), }; assert_eq!(extensions, Ok(expected_extensions)); } #[test] - fn test_from_get_assertion_extensions() { + fn test_from_get_assertion_extensions_default_protocol() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { + "credBlob" => true, "hmac-secret" => cbor_map! { - 1 => cbor::Value::Map(cose_key.0.clone()), + 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, + "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { key_agreement: cose_key, salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), + cred_blob: true, + large_blob_key: Some(true), }; assert_eq!(extensions, Ok(expected_extensions)); } + #[test] + fn test_from_get_assertion_extensions_with_protocol() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cbor_extensions = cbor_map! { + "credBlob" => true, + "hmac-secret" => cbor_map! { + 1 => cbor::Value::from(cose_key.clone()), + 2 => vec![0x02; 32], + 3 => vec![0x03; 16], + 4 => 2, + }, + "largeBlobKey" => true, + }; + let extensions = GetAssertionExtensions::try_from(cbor_extensions); + let expected_input = GetAssertionHmacSecretInput { + key_agreement: cose_key, + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V2, + }; + let expected_extensions = GetAssertionExtensions { + hmac_secret: Some(expected_input), + cred_blob: true, + large_blob_key: Some(true), + }; + assert_eq!(extensions, Ok(expected_extensions)); + // TODO more tests, check default + } + #[test] fn test_from_make_credential_options() { let cbor_make_options = cbor_map! { @@ -1302,11 +1681,11 @@ mod test { #[test] fn test_into_packed_attestation_statement() { - let certificate: cbor::values::KeyType = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]]; + let certificate = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]]; 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], }; let packed_attestation_statement = PackedAttestationStatement { @@ -1320,7 +1699,103 @@ mod test { } #[test] - fn test_from_into_cose_key() { + fn test_from_into_cose_key_cbor() { + for algorithm in &[CoseKey::ECDH_ALGORITHM, ES256_ALGORITHM] { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => algorithm, + -1 => CoseKey::P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + let cose_key = CoseKey::try_from(cbor_value.clone()).unwrap(); + let created_cbor_value = cbor::Value::from(cose_key); + assert_eq!(created_cbor_value, cbor_value); + } + } + + #[test] + fn test_cose_key_unknown_algorithm() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + // unknown algorithm + 3 => 0, + -1 => CoseKey::P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + } + + #[test] + fn test_cose_key_unknown_type() { + let cbor_value = cbor_map! { + // unknown type + 1 => 0, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + } + + #[test] + fn test_cose_key_unknown_curve() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + // unknown curve + -1 => 0, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + } + + #[test] + fn test_cose_key_wrong_length_x() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, + // wrong length + -2 => [0u8; 31], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + fn test_cose_key_wrong_length_y() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, + -2 => [0u8; 32], + // wrong length + -3 => [0u8; 33], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + fn test_from_into_cose_key_ecdh() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1329,6 +1804,34 @@ mod test { assert_eq!(created_pk, Ok(pk)); } + #[test] + fn test_into_cose_key_ecdsa() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdsa::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + assert_eq!(cose_key.algorithm, ES256_ALGORITHM); + } + + #[test] + fn test_from_pin_uv_auth_protocol() { + let cbor_protocol: cbor::Value = cbor_int!(0x01); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Ok(PinUvAuthProtocol::V1) + ); + let cbor_protocol: cbor::Value = cbor_int!(0x02); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Ok(PinUvAuthProtocol::V2) + ); + let cbor_protocol: cbor::Value = cbor_int!(0x03); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_from_into_client_pin_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); @@ -1345,6 +1848,122 @@ mod test { } } + #[test] + fn test_from_into_config_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = ConfigSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = ConfigSubCommand::EnableEnterpriseAttestation; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in ConfigSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = ConfigSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_set_min_pin_length_params() { + let params = SetMinPinLengthParams { + new_min_pin_length: Some(6), + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + force_change_pin: Some(true), + }; + let cbor_params = cbor_map! { + 0x01 => 6, + 0x02 => cbor_array!("example.com".to_string()), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(params.clone()), cbor_params); + let reconstructed_params = SetMinPinLengthParams::try_from(cbor_params); + assert_eq!(reconstructed_params, Ok(params)); + } + + #[test] + fn test_from_config_sub_command_params() { + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(6), + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + force_change_pin: Some(true), + }; + let config_sub_command_params = + ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params); + let cbor_params = cbor_map! { + 0x01 => 6, + 0x02 => cbor_array!("example.com".to_string()), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); + } + + #[test] + fn test_from_enterprise_attestation_mode() { + assert_eq!( + EnterpriseAttestationMode::try_from(0), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(1), + Ok(EnterpriseAttestationMode::VendorFacilitated), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(2), + Ok(EnterpriseAttestationMode::PlatformManaged), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(3), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); + } + + #[test] + fn test_from_into_cred_management_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = CredentialManagementSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = CredentialManagementSubCommand::GetCredsMetadata; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in CredentialManagementSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = CredentialManagementSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_into_cred_management_sub_command_params() { + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], + transports: Some(vec![AuthenticatorTransport::Usb]), + }; + let user_entity = 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 cbor_sub_command_params = cbor_map! { + 0x01 => vec![0x1D; 32], + 0x02 => credential_id.clone(), + 0x03 => user_entity.clone(), + }; + let sub_command_params = + CredentialManagementSubCommandParameters::try_from(cbor_sub_command_params.clone()); + let expected_sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: Some(credential_id), + user: Some(user_entity), + }; + assert_eq!(sub_command_params, Ok(expected_sub_command_params)); + let created_cbor: cbor::Value = sub_command_params.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command_params); + } + #[test] fn test_credential_source_cbor_round_trip() { let mut rng = ThreadRng256 {}; @@ -1354,9 +1973,13 @@ mod test { private_key: crypto::ecdsa::SecKey::gensk(&mut rng), rp_id: "example.com".to_string(), user_handle: b"foo".to_vec(), - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert_eq!( @@ -1365,17 +1988,7 @@ mod test { ); let credential = PublicKeyCredentialSource { - other_ui: Some("other".to_string()), - ..credential - }; - - assert_eq!( - PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), - Ok(credential.clone()) - ); - - let credential = PublicKeyCredentialSource { - cred_random: Some(vec![0x00; 32]), + user_display_name: Some("Display Name".to_string()), ..credential }; @@ -1389,6 +2002,46 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + user_name: Some("name".to_string()), + ..credential + }; + + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + user_icon: Some("icon".to_string()), + ..credential + }; + + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + cred_blob: Some(vec![0xCB]), + ..credential + }; + + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + large_blob_key: Some(vec![0x1B]), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index a6a18f7..c6fc418 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -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); diff --git a/src/ctap/hid/receive.rs b/src/ctap/hid/receive.rs index b522837..caf9ffc 100644 --- a/src/ctap/hid/receive.rs +++ b/src/ctap/hid/receive.rs @@ -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, - ) -> Option { - // 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, (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 { diff --git a/src/ctap/hid/send.rs b/src/ctap/hid/send.rs index 434d633..22f9c61 100644 --- a/src/ctap/hid/send.rs +++ b/src/ctap/hid/send.rs @@ -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. diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index 1958040..a8ae6da 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -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")); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs new file mode 100644 index 0000000..846bc33 --- /dev/null +++ b/src/ctap/large_blobs.rs @@ -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, + 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 { + 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); + } +} diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 66ef234..eaca021 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -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,79 +12,82 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod apdu; +mod client_pin; pub mod command; +mod config_command; +mod credential_management; +mod crypto_wrapper; #[cfg(feature = "with_ctap1")] mod ctap1; +mod customization; pub mod data_formats; pub mod hid; mod key_material; -mod pin_protocol_v1; +mod large_blobs; +mod pin_protocol; pub mod response; pub mod status_code; mod storage; mod timed_permission; +mod token_state; -#[cfg(feature = "with_ctap2_1")] -use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; +use self::client_pin::{ClientPin, PinPermission}; use self::command::{ - AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, - AuthenticatorMakeCredentialParameters, Command, + AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, + AuthenticatorVendorConfigureParameters, Command, +}; +use self::config_command::process_config; +use self::credential_management::process_credential_management; +use self::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; +use self::customization::{ + DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, + MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_MSG_SIZE, + MAX_RP_IDS_LENGTH, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, }; -#[cfg(feature = "with_ctap2_1")] -use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, + GetAssertionExtensions, PackedAttestationStatement, PinUvAuthProtocol, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; -#[cfg(feature = "with_ctap2_1")] -use self::pin_protocol_v1::PinPermission; -use self::pin_protocol_v1::PinProtocolV1; +use self::large_blobs::LargeBlobs; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, - AuthenticatorMakeCredentialResponse, ResponseData, + AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; +use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; -use alloc::collections::BTreeMap; +use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; -use cbor::{cbor_map, cbor_map_options}; +use cbor::cbor_map_options; +use core::convert::TryFrom; #[cfg(feature = "debug_ctap")] use core::fmt::Write; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hmac::{hmac_256, verify_hmac_256}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock_drivers::console::Console; -use libtock_drivers::timer::{Duration, Timestamp}; +use libtock_drivers::crp; +use libtock_drivers::timer::{ClockValue, Duration}; -// This flag enables or disables basic attestation for FIDO2. U2F is unaffected by -// this setting. The basic attestation uses the signing key from key_material.rs -// as a batch key. Turn it on if you want attestation. In this case, be aware that -// it is your responsibility to generate your own key material and keep it secret. -const USE_BATCH_ATTESTATION: bool = false; -// The signature counter is currently implemented as a global counter, if you set -// this flag to true. The spec strongly suggests to have per-credential-counters, -// but it means you can't have an infinite amount of credentials anymore. Also, -// since this is the only piece of information that needs writing often, we might -// need a flash storage friendly way to implement this feature. The implemented -// solution is a compromise to be compatible with U2F and not wasting storage. -const USE_SIGNATURE_COUNTER: bool = true; +pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, // - 32 byte relying party ID hashed with SHA256, // - 32 byte HMAC-SHA256 over everything else. -pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112; +pub const CREDENTIAL_ID_SIZE: usize = 112; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -97,13 +100,14 @@ const ED_FLAG: u8 = 0x80; pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); -const RESET_TIMEOUT_MS: isize = 10000; +// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? +const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] pub const U2F_VERSION_STRING: &str = "U2F_V2"; // TODO(#106) change to final string when ready -#[cfg(feature = "with_ctap2_1")] pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; // We currently only support one algorithm for signatures: ES256. @@ -112,10 +116,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa cred_type: PublicKeyCredentialType::PublicKey, alg: SignatureAlgorithm::ES256, }; -// You can change this value to one of the following for more privacy. -// - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) -// - Some(CredentialProtectionPolicy::UserVerificationRequired) -const DEFAULT_CRED_PROTECT: Option = None; // This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110 // (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding. @@ -131,6 +131,133 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +/// Holds data necessary to sign an assertion for a credential. +#[derive(Clone)] +pub struct AssertionInput { + client_data_hash: Vec, + auth_data: Vec, + extensions: GetAssertionExtensions, + has_uv: bool, +} + +/// Contains the state we need to store for GetNextAssertion. +pub struct AssertionState { + assertion_input: AssertionInput, + // Sorted by ascending order of creation, so the last element is the most recent one. + next_credential_keys: Vec, +} + +/// Stores which command currently holds state for subsequent calls. +pub enum StatefulCommand { + Reset, + GetAssertion(Box), + EnumerateRps(usize), + EnumerateCredentials(Vec), +} + +/// Stores the current CTAP command state and when it times out. +/// +/// Some commands are executed in a series of calls to the authenticator. +/// Interleaving calls to other commands interrupt the current command and +/// remove all state and permissions. Power cycling allows the Reset command, +/// and to prevent misuse or accidents, we disallow Reset after receiving +/// different commands. Therefore, Reset behaves just like all other stateful +/// commands and is included here. Please note that the allowed time for Reset +/// differs from all other stateful commands. +pub struct StatefulPermission { + permission: TimedPermission, + command_type: Option, +} + +impl StatefulPermission { + /// Creates the command state at device startup. + /// + /// Resets are only possible after a power cycle. Therefore, initialization + /// means allowing Reset, and Reset cannot be granted later. + pub fn new_reset(now: ClockValue) -> StatefulPermission { + StatefulPermission { + permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + command_type: Some(StatefulCommand::Reset), + } + } + + /// Clears all permissions and state. + pub fn clear(&mut self) { + self.permission = TimedPermission::waiting(); + self.command_type = None; + } + + /// Checks the permission timeout. + pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + if self.permission.is_granted(now) { + Ok(()) + } else { + self.clear(); + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Gets a reference to the current command state, if any exists. + pub fn get_command(&self) -> Result<&StatefulCommand, Ctap2StatusCode> { + self.command_type + .as_ref() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + + /// Sets a new command state, and starts a new clock for timeouts. + pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { + match &new_command_type { + // Reset is only allowed after a power cycle. + StatefulCommand::Reset => unreachable!(), + _ => { + self.permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.command_type = Some(new_command_type); + } + } + } + + /// Returns the state for the next assertion and advances it. + /// + /// The state includes all information from GetAssertion and the storage key + /// to the next credential that needs to be processed. + pub fn next_assertion_credential( + &mut self, + ) -> Result<(AssertionInput, usize), Ctap2StatusCode> { + if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.command_type { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + Ok((assertion_state.assertion_input.clone(), credential_key)) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the index to the next RP ID for enumeration and advances it. + pub fn next_enumerate_rp(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = &mut self.command_type { + let current_index = *rp_id_index; + *rp_id_index += 1; + Ok(current_index) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the next storage credential key for enumeration and advances it. + pub fn next_enumerate_credential(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut self.command_type + { + rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -140,11 +267,12 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( // false otherwise. check_user_presence: CheckUserPresence, persistent_store: PersistentStore, - pin_protocol_v1: PinProtocolV1, - // This variable will be irreversibly set to false RESET_TIMEOUT_MS milliseconds after boot. - accepts_reset: bool, + client_pin: ClientPin, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + // The state initializes to Reset and its timeout, and never goes back to Reset. + stateful_command_permission: StatefulPermission, + large_blobs: LargeBlobs, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -152,41 +280,53 @@ where R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>, { - pub const PIN_PROTOCOL_VERSION: u64 = 1; - pub fn new( rng: &'a mut R, check_user_presence: CheckUserPresence, + now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { let persistent_store = PersistentStore::new(rng); - let pin_protocol_v1 = PinProtocolV1::new(rng); + let client_pin = ClientPin::new(rng); CtapState { rng, check_user_presence, persistent_store, - pin_protocol_v1, - accepts_reset: true, + client_pin, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), + stateful_command_permission: StatefulPermission::new_reset(now), + large_blobs: LargeBlobs::new(), } } - pub fn check_disable_reset(&mut self, timestamp: Timestamp) { - if timestamp - Timestamp::::from_ms(0) > Duration::from_ms(RESET_TIMEOUT_MS) { - self.accepts_reset = false; - } + pub fn update_timeouts(&mut self, now: ClockValue) { + // Ignore the result, just update. + let _ = self + .stateful_command_permission + .check_command_permission(now); + self.client_pin.update_timeouts(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { if USE_SIGNATURE_COUNTER { - self.persistent_store.incr_global_signature_counter()?; + let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1; + self.persistent_store + .incr_global_signature_counter(increment)?; } Ok(()) } + // Returns whether CTAP1 commands are currently supported. + // If alwaysUv is enabled and the authenticator does not support internal UV, + // CTAP1 needs to be disabled. + #[cfg(feature = "with_ctap1")] + pub fn allows_ctap1(&self) -> Result { + Ok(!self.persistent_store.has_always_uv()?) + } + // Encrypts the private key and relying party ID hash into a credential ID. Other // information, such as a user name, are not stored, because encrypted credential IDs // are used for credentials stored server-side. Also, we want the key handle to be @@ -198,23 +338,11 @@ where ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let mut sk_bytes = [0; 32]; - private_key.to_bytes(&mut sk_bytes); - let mut iv = [0; 16]; - iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); + let mut plaintext = [0; 64]; + private_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); + plaintext[32..64].copy_from_slice(application); - let mut blocks = [[0u8; 16]; 4]; - blocks[0].copy_from_slice(&sk_bytes[..16]); - blocks[1].copy_from_slice(&sk_bytes[16..]); - blocks[2].copy_from_slice(&application[..16]); - blocks[3].copy_from_slice(&application[16..]); - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - - let mut encrypted_id = Vec::with_capacity(ENCRYPTED_CREDENTIAL_ID_SIZE); - encrypted_id.extend(&iv); - for b in &blocks { - encrypted_id.extend(b); - } + let mut encrypted_id = aes256_cbc_encrypt(self.rng, &aes_enc_key, &plaintext, true)?; let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); encrypted_id.extend(&id_hmac); Ok(encrypted_id) @@ -228,11 +356,11 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - if credential_id.len() != ENCRYPTED_CREDENTIAL_ID_SIZE { + if credential_id.len() != CREDENTIAL_ID_SIZE { return Ok(None); } let master_keys = self.persistent_store.master_keys()?; - let payload_size = ENCRYPTED_CREDENTIAL_ID_SIZE - 32; + let payload_size = credential_id.len() - 32; if !verify_hmac_256::( &master_keys.hmac, &credential_id[..payload_size], @@ -241,40 +369,34 @@ where return Ok(None); } let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - let mut iv = [0; 16]; - iv.copy_from_slice(&credential_id[..16]); - let mut blocks = [[0u8; 16]; 4]; - for i in 0..4 { - blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); - } - cbc_decrypt(&aes_dec_key, iv, &mut blocks); - let mut decrypted_sk = [0; 32]; - let mut decrypted_rp_id_hash = [0; 32]; - decrypted_sk[..16].clone_from_slice(&blocks[0]); - decrypted_sk[16..].clone_from_slice(&blocks[1]); - decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); - decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); - - if rp_id_hash != decrypted_rp_id_hash { + let decrypted_id = aes256_cbc_decrypt(&aes_enc_key, &credential_id[..payload_size], true)?; + if rp_id_hash != &decrypted_id[32..64] { return Ok(None); } - - let sk_option = crypto::ecdsa::SecKey::from_bytes(&decrypted_sk); + let sk_option = crypto::ecdsa::SecKey::from_bytes(array_ref!(decrypted_id, 0, 32)); Ok(sk_option.map(|sk| PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, private_key: sk, rp_id: String::from(""), user_handle: vec![], - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, })) } - pub fn process_command(&mut self, command_cbor: &[u8], cid: ChannelID) -> Vec { + pub fn process_command( + &mut self, + command_cbor: &[u8], + cid: ChannelID, + now: ClockValue, + ) -> Vec { let cmd = Command::deserialize(command_cbor); #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Received command: {:#?}", cmd).unwrap(); @@ -288,20 +410,62 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } + match (&command, self.stateful_command_permission.get_command()) { + (Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_))) + | (Command::AuthenticatorReset, Ok(StatefulCommand::Reset)) + // AuthenticatorGetInfo still allows Reset. + | (Command::AuthenticatorGetInfo, Ok(StatefulCommand::Reset)) + // AuthenticatorSelection still allows Reset. + | (Command::AuthenticatorSelection, Ok(StatefulCommand::Reset)) + // AuthenticatorCredentialManagement handles its subcommands later. + | ( + Command::AuthenticatorCredentialManagement(_), + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( + Command::AuthenticatorCredentialManagement(_), + Ok(StatefulCommand::EnumerateCredentials(_)), + ) => (), + (_, _) => self.stateful_command_permission.clear(), + } let response = match command { Command::AuthenticatorMakeCredential(params) => { self.process_make_credential(params, cid) } Command::AuthenticatorGetAssertion(params) => { - self.process_get_assertion(params, cid) + self.process_get_assertion(params, cid, now) } + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(now), Command::AuthenticatorGetInfo => self.process_get_info(), - Command::AuthenticatorClientPin(params) => self.process_client_pin(params), - Command::AuthenticatorReset => self.process_reset(cid), - #[cfg(feature = "with_ctap2_1")] + Command::AuthenticatorClientPin(params) => self.client_pin.process_command( + self.rng, + &mut self.persistent_store, + params, + now, + ), + Command::AuthenticatorReset => self.process_reset(cid, now), + Command::AuthenticatorCredentialManagement(params) => { + process_credential_management( + &mut self.persistent_store, + &mut self.stateful_command_permission, + &mut self.client_pin, + params, + now, + ) + } Command::AuthenticatorSelection => self.process_selection(cid), - // TODO(kaczmarczyck) implement GetNextAssertion and FIDO 2.1 commands - _ => self.process_unknown_command(), + Command::AuthenticatorLargeBlobs(params) => self.large_blobs.process_command( + &mut self.persistent_store, + &mut self.client_pin, + params, + ), + Command::AuthenticatorConfig(params) => { + process_config(&mut self.persistent_store, &mut self.client_pin, params) + } + // Vendor specific commands + Command::AuthenticatorVendorConfigure(params) => { + self.process_vendor_configure(params, cid) + } }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -310,10 +474,8 @@ where let mut response_vec = vec![0x00]; if let Some(value) = response_data.into() { if !cbor::write(value, &mut response_vec) { - response_vec = vec![ - Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR - as u8, - ]; + response_vec = + vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]; } } response_vec @@ -325,6 +487,27 @@ where } } + fn pin_uv_auth_precheck( + &mut self, + pin_uv_auth_param: &Option>, + pin_uv_auth_protocol: Option, + cid: ChannelID, + ) -> Result<(), Ctap2StatusCode> { + if let Some(auth_param) = &pin_uv_auth_param { + // This case was added in FIDO 2.1. + if auth_param.is_empty() { + (self.check_user_presence)(cid)?; + if self.persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + } + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + } + Ok(()) + } + fn process_make_credential( &mut self, make_credential_params: AuthenticatorMakeCredentialParameters, @@ -340,64 +523,82 @@ where options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation, } = make_credential_params; - if let Some(auth_param) = &pin_uv_auth_param { - // This case was added in FIDO 2.1. - if auth_param.is_empty() { - if self.persistent_store.pin_hash()?.is_none() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - - match pin_uv_auth_protocol { - Some(protocol) => { - if protocol != CtapState::::PIN_PROTOCOL_VERSION { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } - } + self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; if !pub_key_cred_params.contains(&ES256_CRED_PARAM) { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions { - let mut cred_protect = extensions.cred_protect; - if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - < DEFAULT_CRED_PROTECT - .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - { - cred_protect = DEFAULT_CRED_PROTECT; - } - (extensions.hmac_secret, cred_protect) - } else { - (false, DEFAULT_CRED_PROTECT) - }; - - let cred_random = if use_hmac_extension { - if !options.rk { - // The extension is actually supported, but we need resident keys. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - Some(self.rng.gen_uniform_u8x32().to_vec()) - } else { - None - }; - // TODO(kaczmarczyck) unsolicited output for default credProtect level - let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); - let rp_id = rp.rp_id; + let ep_att = if let Some(enterprise_attestation) = enterprise_attestation { + let authenticator_mode = + ENTERPRISE_ATTESTATION_MODE.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + if !self.persistent_store.enterprise_attestation()? { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + match ( + EnterpriseAttestationMode::try_from(enterprise_attestation)?, + authenticator_mode, + ) { + ( + EnterpriseAttestationMode::PlatformManaged, + EnterpriseAttestationMode::PlatformManaged, + ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id.as_str()), + _ => true, + } + } else { + false + }; + + // MakeCredential always requires user presence. + // User verification depends on the PIN auth inputs, which are checked here. + // The ED flag is added later, if applicable. + let has_uv = pin_uv_auth_param.is_some(); + let mut flags = match pin_uv_auth_param { + Some(pin_uv_auth_param) => { + // This case is not mentioned in CTAP2.1, so we keep 2.0 logic. + if self.persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + self.client_pin + .has_permission(PinPermission::MakeCredential)?; + self.client_pin.check_user_verified_flag()?; + // Checking for the correct permissions_rp_id is specified earlier. + // Error codes are identical though, so the implementation can be identical with + // GetAssertion. + self.client_pin.ensure_rp_id_permission(&rp_id)?; + UV_FLAG + } + None => { + if options.uv { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + if self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } + // Corresponds to makeCredUvNotRqd set to true. + if options.rk && self.persistent_store.pin_hash()?.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } + 0x00 + } + }; + flags |= UP_FLAG | AT_FLAG; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { for cred_desc in exclude_list { if self .persistent_store - .find_credential(&rp_id, &cred_desc.key_id, pin_uv_auth_param.is_none())? + .find_credential(&rp_id, &cred_desc.key_id, !has_uv)? .is_some() || self .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? @@ -405,47 +606,47 @@ where { // Perform this check, so bad actors can't brute force exclude_list // without user interaction. - (self.check_user_presence)(cid)?; + let _ = (self.check_user_presence)(cid); return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); } } } - // MakeCredential always requires user presence. - // User verification depends on the PIN auth inputs, which are checked here. - let ed_flag = if has_extension_output { ED_FLAG } else { 0 }; - let flags = match pin_uv_auth_param { - Some(pin_auth) => { - if self.persistent_store.pin_hash()?.is_none() { - // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } - if !self - .pin_protocol_v1 - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - #[cfg(feature = "with_ctap2_1")] - { - self.pin_protocol_v1 - .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; - } - UP_FLAG | UV_FLAG | AT_FLAG | ed_flag - } - None => { - if self.persistent_store.pin_hash()?.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED); - } - if options.uv { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); - } - UP_FLAG | AT_FLAG | ed_flag - } - }; - (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); + + let mut cred_protect_policy = extensions.cred_protect; + if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect_policy = DEFAULT_CRED_PROTECT; + } + let min_pin_length = extensions.min_pin_length + && self + .persistent_store + .min_pin_length_rp_ids()? + .contains(&rp_id); + // None for no input, false for invalid input, true for valid input. + let has_cred_blob_output = extensions.cred_blob.is_some(); + let cred_blob = extensions + .cred_blob + .filter(|c| options.rk && c.len() <= MAX_CRED_BLOB_LENGTH); + let cred_blob_output = if has_cred_blob_output { + Some(cred_blob.is_some()) + } else { + None + }; + let has_extension_output = extensions.hmac_secret + || extensions.cred_protect.is_some() + || min_pin_length + || has_cred_blob_output; + if has_extension_output { + flags |= ED_FLAG + }; + let large_blob_key = match (options.rk, extensions.large_blob_key) { + (true, Some(true)) => Some(self.rng.gen_uniform_u8x32().to_vec()), + _ => None, + }; let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -460,11 +661,19 @@ where user_handle: user.user_id, // This input is user provided, so we crop it to 64 byte for storage. // The UTF8 encoding is always preserved, so the string might end up shorter. - other_ui: user + user_display_name: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), - cred_random, cred_protect_policy, + creation_order: self.persistent_store.new_creation_order()?, + user_name: user + .user_name + .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + user_icon: user + .user_icon + .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + cred_blob, + large_blob_key: large_blob_key.clone(), }; self.persistent_store.store_credential(credential_source)?; random_id @@ -476,47 +685,59 @@ where auth_data.extend(&self.persistent_store.aaguid()?); // The length is fixed to 0x20 or 0x70 and fits one byte. if credential_id.len() > 0xFF { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); - let cose_key = match pk.to_cose_key() { - Some(cose_key) => cose_key, - None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR), - }; - auth_data.extend(cose_key); + if !cbor::write(cbor::Value::from(CoseKey::from(pk)), &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } if has_extension_output { - let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let hmac_secret_output = if extensions.hmac_secret { + Some(true) + } else { + None + }; + let min_pin_length_output = if min_pin_length { + Some(self.persistent_store.min_pin_length()? as u64) + } else { + None + }; + let cred_protect_output = extensions.cred_protect.and(cred_protect_policy); let extensions_output = cbor_map_options! { + "credBlob" => cred_blob_output, + "credProtect" => cred_protect_output, "hmac-secret" => hmac_secret_output, - "credProtect" => cred_protect_policy, + "minPinLength" => min_pin_length_output, }; if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - // We currently use the presence of the attestation private key in the persistent storage to - // decide whether batch attestation is needed. - let (signature, x5c) = match self.persistent_store.attestation_private_key()? { - Some(attestation_private_key) => { - let attestation_key = - crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); - let attestation_certificate = self - .persistent_store - .attestation_certificate()? - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; - ( - attestation_key.sign_rfc6979::(&signature_data), - Some(vec![attestation_certificate]), - ) - } - None => ( + + let (signature, x5c) = if USE_BATCH_ATTESTATION || ep_att { + let attestation_private_key = self + .persistent_store + .attestation_private_key()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + let attestation_key = + crypto::ecdsa::SecKey::from_bytes(&attestation_private_key).unwrap(); + let attestation_certificate = self + .persistent_store + .attestation_certificate()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + ( + attestation_key.sign_rfc6979::(&signature_data), + Some(vec![attestation_certificate]), + ) + } else { + ( sk.sign_rfc6979::(&signature_data), None, - ), + ) }; let attestation_statement = PackedAttestationStatement { alg: SignatureAlgorithm::ES256 as i64, @@ -524,19 +745,147 @@ where x5c, ecdaa_key_id: None, }; + let ep_att = if ep_att { Some(true) } else { None }; Ok(ResponseData::AuthenticatorMakeCredential( AuthenticatorMakeCredentialResponse { fmt: String::from("packed"), auth_data, att_stmt: attestation_statement, + ep_att, + large_blob_key, }, )) } + // Generates a different per-credential secret for each UV mode. + // The computation is deterministic, and private_key expected to be unique. + fn generate_cred_random( + &mut self, + private_key: &crypto::ecdsa::SecKey, + has_uv: bool, + ) -> Result<[u8; 32], Ctap2StatusCode> { + let mut private_key_bytes = [0u8; 32]; + private_key.to_bytes(&mut private_key_bytes); + let key = self.persistent_store.cred_random_secret(has_uv)?; + Ok(hmac_256::(&key, &private_key_bytes)) + } + + // Processes the input of a get_assertion operation for a given credential + // and returns the correct Get(Next)Assertion response. + fn assertion_response( + &mut self, + mut credential: PublicKeyCredentialSource, + assertion_input: AssertionInput, + number_of_credentials: Option, + ) -> Result { + let AssertionInput { + client_data_hash, + mut auth_data, + extensions, + has_uv, + } = assertion_input; + + // Process extensions. + if extensions.hmac_secret.is_some() || extensions.cred_blob { + let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret { + let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; + Some(self.client_pin.process_hmac_secret( + self.rng, + hmac_secret_input, + &cred_random, + )?) + } else { + None + }; + // This could be written more nicely with `then_some` when stable. + let cred_blob = if extensions.cred_blob { + Some(credential.cred_blob.unwrap_or_default()) + } else { + None + }; + let extensions_output = cbor_map_options! { + "credBlob" => cred_blob, + "hmac-secret" => encrypted_output, + }; + if !cbor::write(extensions_output, &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + let large_blob_key = match extensions.large_blob_key { + Some(true) => credential.large_blob_key, + _ => None, + }; + + let mut signature_data = auth_data.clone(); + signature_data.extend(client_data_hash); + let signature = credential + .private_key + .sign_rfc6979::(&signature_data); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential.credential_id, + transports: None, // You can set USB as a hint here. + }; + // Remove user identifiable information without uv. + if !has_uv { + credential.user_name = None; + credential.user_display_name = None; + credential.user_icon = None; + } + let user = if !credential.user_handle.is_empty() { + Some(PublicKeyCredentialUserEntity { + user_id: credential.user_handle, + user_name: credential.user_name, + user_display_name: credential.user_display_name, + user_icon: credential.user_icon, + }) + } else { + None + }; + Ok(ResponseData::AuthenticatorGetAssertion( + AuthenticatorGetAssertionResponse { + credential: Some(cred_desc), + auth_data, + signature: signature.to_asn1_der(), + user, + number_of_credentials: number_of_credentials.map(|n| n as u64), + large_blob_key, + }, + )) + } + + // Returns the first applicable credential from the allow list. + fn get_any_credential_from_allow_list( + &mut self, + allow_list: Vec, + rp_id: &str, + rp_id_hash: &[u8], + has_uv: bool, + ) -> Result, Ctap2StatusCode> { + for allowed_credential in allow_list { + let credential = self.persistent_store.find_credential( + rp_id, + &allowed_credential.key_id, + !has_uv, + )?; + if credential.is_some() { + return Ok(credential); + } + let credential = + self.decrypt_credential_source(allowed_credential.key_id, &rp_id_hash)?; + if credential.is_some() { + return Ok(credential); + } + } + Ok(None) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, cid: ChannelID, + now: ClockValue, ) -> Result { let AuthenticatorGetAssertionParameters { rp_id, @@ -548,223 +897,215 @@ where pin_uv_auth_protocol, } = get_assertion_params; - if let Some(auth_param) = &pin_uv_auth_param { - // This case was added in FIDO 2.1. - if auth_param.is_empty() { - if self.persistent_store.pin_hash()?.is_none() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } + self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; - match pin_uv_auth_protocol { - Some(protocol) => { - if protocol != CtapState::::PIN_PROTOCOL_VERSION { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } - } - - let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); - if hmac_secret_input.is_some() && !options.up { + if extensions.hmac_secret.is_some() && !options.up { // The extension is actually supported, but we need user presence. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } // The user verification bit depends on the existance of PIN auth, since we do // not support internal UV. User presence is requested as an option. let has_uv = pin_uv_auth_param.is_some(); let mut flags = match pin_uv_auth_param { - Some(pin_auth) => { + Some(pin_uv_auth_param) => { + // This case is not mentioned in CTAP2.1, so we keep 2.0 logic. if self.persistent_store.pin_hash()?.is_none() { - // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !self - .pin_protocol_v1 - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - #[cfg(feature = "with_ctap2_1")] - { - self.pin_protocol_v1 - .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; - } + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + self.client_pin + .has_permission(PinPermission::GetAssertion)?; + // Checking for the UV flag is specified earlier for GetAssertion. + // Error codes are identical though, so the implementation can be identical with + // MakeCredential. + self.client_pin.check_user_verified_flag()?; + self.client_pin.ensure_rp_id_permission(&rp_id)?; UV_FLAG } None => { if options.uv { - // The specification (inconsistently) wants CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } + if options.up && self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } 0x00 } }; if options.up { flags |= UP_FLAG; } - if hmac_secret_input.is_some() { + if extensions.hmac_secret.is_some() || extensions.cred_blob { flags |= ED_FLAG; } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut decrypted_credential = None; - let credentials = if let Some(allow_list) = allow_list { - let mut found_credentials = vec![]; - for allowed_credential in allow_list { - match self.persistent_store.find_credential( - &rp_id, - &allowed_credential.key_id, - !has_uv, - )? { - Some(credential) => { - found_credentials.push(credential); - } - None => { - if decrypted_credential.is_none() { - decrypted_credential = self.decrypt_credential_source( - allowed_credential.key_id, - &rp_id_hash, - )?; - } - } - } - } - found_credentials + let (credential, next_credential_keys) = if let Some(allow_list) = allow_list { + ( + self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)?, + vec![], + ) } else { - // TODO(kaczmarczyck) use GetNextAssertion - self.persistent_store.filter_credential(&rp_id, !has_uv)? + let mut iter_result = Ok(()); + let iter = self.persistent_store.iter_credentials(&mut iter_result)?; + let mut stored_credentials: Vec<(usize, u64)> = iter + .filter_map(|(key, credential)| { + if credential.rp_id == rp_id && (has_uv || credential.is_discoverable()) { + Some((key, credential.creation_order)) + } else { + None + } + }) + .collect(); + iter_result?; + stored_credentials.sort_unstable_by_key(|&(_key, order)| order); + let mut stored_credentials: Vec = stored_credentials + .into_iter() + .map(|(key, _order)| key) + .collect(); + let credential = stored_credentials + .pop() + .map(|key| self.persistent_store.get_credential(key)) + .transpose()?; + (credential, stored_credentials) }; - let credential = if let Some(credential) = credentials.first() { - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. if options.up { (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); } self.increment_global_signature_counter()?; - let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; - // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - let encrypted_output = self - .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; - let extensions_output = cbor_map! { - "hmac-secret" => encrypted_output, - }; - if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); - } - } - - let mut signature_data = auth_data.clone(); - signature_data.extend(client_data_hash); - let signature = credential - .private_key - .sign_rfc6979::(&signature_data); - - let cred_desc = PublicKeyCredentialDescriptor { - key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), - transports: None, // You can set USB as a hint here. + let assertion_input = AssertionInput { + client_data_hash, + auth_data: self.generate_auth_data(&rp_id_hash, flags)?, + extensions, + has_uv, }; - let user = if (flags & UV_FLAG != 0) && !credential.user_handle.is_empty() { - Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), - user_name: None, - user_display_name: credential.other_ui.clone(), - user_icon: None, - }) - } else { + let number_of_credentials = if next_credential_keys.is_empty() { None + } else { + let number_of_credentials = Some(next_credential_keys.len() + 1); + let assertion_state = StatefulCommand::GetAssertion(Box::new(AssertionState { + assertion_input: assertion_input.clone(), + next_credential_keys, + })); + self.stateful_command_permission + .set_command(now, assertion_state); + number_of_credentials }; - Ok(ResponseData::AuthenticatorGetAssertion( - AuthenticatorGetAssertionResponse { - credential: Some(cred_desc), - auth_data, - signature: signature.to_asn1_der(), - user, - number_of_credentials: None, - }, - )) + self.assertion_response(credential, assertion_input, number_of_credentials) + } + + fn process_get_next_assertion( + &mut self, + now: ClockValue, + ) -> Result { + self.stateful_command_permission + .check_command_permission(now)?; + let (assertion_input, credential_key) = self + .stateful_command_permission + .next_assertion_credential()?; + let credential = self.persistent_store.get_credential(credential_key)?; + self.assertion_response(credential, assertion_input, None) } fn process_get_info(&self) -> Result { - let mut options_map = BTreeMap::new(); - // TODO(kaczmarczyck) add authenticatorConfig and credProtect options - options_map.insert(String::from("rk"), true); - options_map.insert(String::from("up"), true); - options_map.insert( - String::from("clientPin"), - self.persistent_store.pin_hash()?.is_some(), - ); + let has_always_uv = self.persistent_store.has_always_uv()?; + #[cfg_attr(not(feature = "with_ctap1"), allow(unused_mut))] + let mut versions = vec![ + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]; + #[cfg(feature = "with_ctap1")] + { + if !has_always_uv { + versions.insert(0, String::from(U2F_VERSION_STRING)) + } + } + let mut options = vec![]; + if ENTERPRISE_ATTESTATION_MODE.is_some() { + options.push(( + String::from("ep"), + self.persistent_store.enterprise_attestation()?, + )); + } + options.append(&mut vec![ + (String::from("rk"), true), + (String::from("up"), true), + (String::from("alwaysUv"), has_always_uv), + (String::from("credMgmt"), true), + (String::from("authnrCfg"), true), + ( + String::from("clientPin"), + self.persistent_store.pin_hash()?.is_some(), + ), + (String::from("largeBlobs"), true), + (String::from("pinUvAuthToken"), true), + (String::from("setMinPINLength"), true), + (String::from("makeCredUvNotRqd"), !has_always_uv), + ]); + Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { - versions: vec![ - #[cfg(feature = "with_ctap1")] - String::from(U2F_VERSION_STRING), - String::from(FIDO2_VERSION_STRING), - #[cfg(feature = "with_ctap2_1")] - String::from(FIDO2_1_VERSION_STRING), - ], - extensions: Some(vec![String::from("hmac-secret")]), - aaguid: self.persistent_store.aaguid()?, - options: Some(options_map), - max_msg_size: Some(1024), - pin_protocols: Some(vec![ - CtapState::::PIN_PROTOCOL_VERSION, + versions, + extensions: Some(vec![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + String::from("credBlob"), + String::from("largeBlobKey"), + ]), + aaguid: self.persistent_store.aaguid()?, + options: Some(options), + max_msg_size: Some(MAX_MSG_SIZE as u64), + // The order implies preference. We favor the new V2. + pin_protocols: Some(vec![ + PinUvAuthProtocol::V2 as u64, + PinUvAuthProtocol::V1 as u64, ]), - #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // You can use ENCRYPTED_CREDENTIAL_ID_SIZE here, but if your - // browser passes that value, it might be used to fingerprint. - #[cfg(feature = "with_ctap2_1")] - max_credential_id_length: None, - #[cfg(feature = "with_ctap2_1")] + max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), - #[cfg(feature = "with_ctap2_1")] algorithms: Some(vec![ES256_CRED_PARAM]), - default_cred_protect: DEFAULT_CRED_PROTECT, - #[cfg(feature = "with_ctap2_1")] + max_serialized_large_blob_array: Some(MAX_LARGE_BLOB_ARRAY_SIZE as u64), + force_pin_change: Some(self.persistent_store.has_force_pin_change()?), min_pin_length: self.persistent_store.min_pin_length()?, - #[cfg(feature = "with_ctap2_1")] firmware_version: None, + max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), + max_rp_ids_for_set_min_pin_length: Some(MAX_RP_IDS_LENGTH as u64), + certifications: None, + remaining_discoverable_credentials: Some( + self.persistent_store.remaining_credentials()? as u64, + ), }, )) } - fn process_client_pin( + fn process_reset( &mut self, - client_pin_params: AuthenticatorClientPinParameters, + cid: ChannelID, + now: ClockValue, ) -> Result { - self.pin_protocol_v1.process_subcommand( - self.rng, - &mut self.persistent_store, - client_pin_params, - ) - } - - fn process_reset(&mut self, cid: ChannelID) -> Result { - // Resets are only possible in the first 10 seconds after booting. - if !self.accepts_reset { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + self.stateful_command_permission + .check_command_permission(now)?; + match self.stateful_command_permission.get_command()? { + StatefulCommand::Reset => (), + _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; self.persistent_store.reset(self.rng)?; - self.pin_protocol_v1.reset(self.rng); + self.client_pin.reset(self.rng); #[cfg(feature = "with_ctap1")] { self.u2f_up_state = U2fUserPresenceState::new( @@ -775,14 +1116,77 @@ where Ok(ResponseData::AuthenticatorReset) } - #[cfg(feature = "with_ctap2_1")] fn process_selection(&self, cid: ChannelID) -> Result { (self.check_user_presence)(cid)?; Ok(ResponseData::AuthenticatorSelection) } - fn process_unknown_command(&self) -> Result { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) + fn process_vendor_configure( + &mut self, + params: AuthenticatorVendorConfigureParameters, + cid: ChannelID, + ) -> Result { + (self.check_user_presence)(cid)?; + + // Sanity checks + let current_priv_key = self.persistent_store.attestation_private_key()?; + let current_cert = self.persistent_store.attestation_certificate()?; + + let response = match params.attestation_material { + // Only reading values. + None => AuthenticatorVendorResponse { + cert_programmed: current_cert.is_some(), + pkey_programmed: current_priv_key.is_some(), + }, + // Device is already fully programmed. We don't leak information. + Some(_) if current_cert.is_some() && current_priv_key.is_some() => { + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + // Device is partially or not programmed. We complete the process. + Some(data) => { + if let Some(current_cert) = ¤t_cert { + if current_cert != &data.certificate { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + } + if let Some(current_priv_key) = ¤t_priv_key { + if current_priv_key != &data.private_key { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + } + if current_cert.is_none() { + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + } + if current_priv_key.is_none() { + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + } + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + }; + if params.lockdown { + // To avoid bricking the authenticator, we only allow lockdown + // to happen if both values are programmed or if both U2F/CTAP1 and + // batch attestation are disabled. + #[cfg(feature = "with_ctap1")] + let need_certificate = true; + #[cfg(not(feature = "with_ctap1"))] + let need_certificate = USE_BATCH_ATTESTATION; + + if (need_certificate && !(response.pkey_programmed && response.cert_programmed)) + || crp::set_protection(crp::ProtectionLevel::FullyLocked).is_err() + { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + Ok(ResponseData::AuthenticatorVendor(response)) } pub fn generate_auth_data( @@ -807,71 +1211,120 @@ where #[cfg(test)] mod test { + use super::client_pin::PIN_TOKEN_LENGTH; + use super::command::{AuthenticatorAttestationMaterial, AuthenticatorClientPinParameters}; use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionHmacSecretInput, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; + use super::pin_protocol::{authenticate_pin_uv_auth_token, PinProtocol}; use super::*; + use cbor::{cbor_array, cbor_array_vec, cbor_map}; use crypto::rng256::ThreadRng256; + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); // The keep-alive logic in the processing of some commands needs a channel ID to send // keep-alive packets to. // In tests where we define a dummy user-presence check that immediately returns, the channel // ID is irrelevant, so we pass this (dummy but valid) value. const DUMMY_CHANNEL_ID: ChannelID = [0x12, 0x34, 0x56, 0x78]; + fn check_make_response( + make_credential_response: Result, + flags: u8, + expected_aaguid: &[u8], + expected_credential_id_size: u8, + expected_extension_cbor: &[u8], + ) { + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let AuthenticatorMakeCredentialResponse { + fmt, + auth_data, + att_stmt, + ep_att, + large_blob_key, + } = make_credential_response; + // The expected response is split to only assert the non-random parts. + assert_eq!(fmt, "packed"); + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, + ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); + expected_auth_data.extend(expected_aaguid); + expected_auth_data.extend(&[0x00, expected_credential_id_size]); + assert_eq!( + auth_data[0..expected_auth_data.len()], + expected_auth_data[..] + ); + assert_eq!( + &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], + expected_extension_cbor + ); + assert!(ep_att.is_none()); + assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + assert_eq!(large_blob_key, None); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_get_info() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); - let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - #[cfg(feature = "with_ctap2_1")] - let mut expected_response = vec![0x00, 0xA9, 0x01]; - #[cfg(not(feature = "with_ctap2_1"))] - let mut expected_response = vec![0x00, 0xA6, 0x01]; - // The difference here is a longer array of supported versions. - let mut version_count = 0; - // CTAP 2 is always supported - version_count += 1; - #[cfg(feature = "with_ctap1")] - { - version_count += 1; - } - #[cfg(feature = "with_ctap2_1")] - { - version_count += 1; - } - expected_response.push(0x80 + version_count); - #[cfg(feature = "with_ctap1")] - expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); - expected_response.extend(&[0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30]); - #[cfg(feature = "with_ctap2_1")] - expected_response.extend(&[ - 0x6C, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, - ]); - expected_response.extend(&[ - 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x03, 0x50, - ]); - expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_response.extend(&[ - 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, - 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, - ]); - #[cfg(feature = "with_ctap2_1")] - expected_response.extend( - [ - 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, - 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, - 0x65, 0x79, 0x0D, 0x04, - ] - .iter(), - ); + let expected_cbor = cbor_map_options! { + 0x01 => cbor_array_vec![vec![ + #[cfg(feature = "with_ctap1")] + String::from(U2F_VERSION_STRING), + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]], + 0x02 => cbor_array![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + String::from("credBlob"), + String::from("largeBlobKey"), + ], + 0x03 => ctap_state.persistent_store.aaguid().unwrap(), + 0x04 => cbor_map_options! { + "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), + "rk" => true, + "up" => true, + "alwaysUv" => false, + "credMgmt" => true, + "authnrCfg" => true, + "clientPin" => false, + "largeBlobs" => true, + "pinUvAuthToken" => true, + "setMinPINLength" => true, + "makeCredUvNotRqd" => true, + }, + 0x05 => MAX_MSG_SIZE as u64, + 0x06 => cbor_array![2, 1], + 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), + 0x08 => CREDENTIAL_ID_SIZE as u64, + 0x09 => cbor_array!["usb"], + 0x0A => cbor_array![ES256_CRED_PARAM], + 0x0B => MAX_LARGE_BLOB_ARRAY_SIZE as u64, + 0x0C => false, + 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, + 0x0F => MAX_CRED_BLOB_LENGTH as u64, + 0x10 => MAX_RP_IDS_LENGTH as u64, + 0x14 => ctap_state.persistent_store.remaining_credentials().unwrap() as u64, + }; - assert_eq!(info_reponse, expected_response); + let mut response_cbor = vec![0x00]; + assert!(cbor::write(expected_cbor, &mut response_cbor)); + assert_eq!(info_reponse, response_cbor); } fn create_minimal_make_credential_parameters() -> AuthenticatorMakeCredentialParameters { @@ -882,7 +1335,7 @@ mod test { rp_icon: None, }; let user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], + user_id: vec![0x1D], user_name: None, user_display_name: None, user_icon: None, @@ -898,10 +1351,11 @@ mod test { user, pub_key_cred_params, exclude_list: None, - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: None, pin_uv_auth_protocol: None, + enterprise_attestation: None, } } @@ -922,93 +1376,59 @@ mod test { fn create_make_credential_parameters_with_cred_protect_policy( policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { - let extensions = Some(MakeCredentialExtensions { - hmac_secret: false, + let extensions = MakeCredentialExtensions { cred_protect: Some(policy), - }); + ..Default::default() + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; make_credential_params } #[test] - fn test_residential_process_make_credential() { + fn test_resident_process_make_credential() { 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 make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, - ]; - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, 0x20]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); } #[test] - fn test_non_residential_process_make_credential() { + fn test_non_resident_process_make_credential() { 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 make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, - ]; - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, ENCRYPTED_CREDENTIAL_ID_SIZE as u8]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + CREDENTIAL_ID_SIZE as u8, + &[], + ); } #[test] fn test_process_make_credential_unsupported_algorithm() { 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 make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.pub_key_cred_params = vec![]; @@ -1026,7 +1446,7 @@ mod test { let mut rng = ThreadRng256 {}; let excluded_private_key = crypto::ecdsa::SecKey::gensk(&mut rng); 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 excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; let make_credential_params = @@ -1037,9 +1457,13 @@ mod test { private_key: excluded_private_key, rp_id: String::from("example.com"), user_handle: vec![], - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1058,7 +1482,7 @@ mod test { fn test_process_make_credential_credential_with_cred_protect() { 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 test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; let make_credential_params = @@ -1067,12 +1491,14 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let stored_credential = ctap_state + let mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credential("example.com", false) - .unwrap() - .pop() + .iter_credentials(&mut iter_result) .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); @@ -1092,12 +1518,14 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let stored_credential = ctap_state + let mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credential("example.com", false) - .unwrap() - .pop() + .iter_credentials(&mut iter_result) .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); @@ -1112,56 +1540,337 @@ mod test { fn test_process_make_credential_hmac_secret() { 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 extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - }); + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + let expected_extension_cbor = [ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + CREDENTIAL_ID_SIZE as u8, + &expected_extension_cbor, + ); + } + + #[test] + fn test_process_make_credential_hmac_secret_resident_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: true, + ..Default::default() + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { + let expected_extension_cbor = [ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + } + + #[test] + fn test_process_make_credential_min_pin_length() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // First part: The extension is ignored, since the RP ID is not on the list. + let extensions = MakeCredentialExtensions { + min_pin_length: true, + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); + + // Second part: The extension is used. + assert_eq!( + ctap_state + .persistent_store + .set_min_pin_length_rp_ids(vec!["example.com".to_string()]), + Ok(()) + ); + + let extensions = MakeCredentialExtensions { + min_pin_length: true, + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, + 0x04, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + } + + #[test] + fn test_process_make_credential_cred_blob_ok() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + cred_blob: Some(vec![0xCB]), + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, Some(vec![0xCB])); + } + + #[test] + fn test_process_make_credential_cred_blob_too_big() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]), + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF4, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, None); + } + + #[test] + fn test_process_make_credential_large_blob_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + large_blob_key: Some(true), + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let large_blob_key = match make_credential_response.unwrap() { ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, - ]; - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, 0x20]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - let expected_extension_cbor = vec![ - 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0xF5, - ]; - assert_eq!( - auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], - expected_extension_cbor[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + make_credential_response.large_blob_key.unwrap() } _ => panic!("Invalid response type"), - } + }; + assert_eq!(large_blob_key.len(), 32); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); + } + + fn test_helper_process_make_credential_with_pin_and_uv( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + + 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(&[0x88; 16], 4).unwrap(); + + let client_data_hash = [0xCD]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &client_data_hash, + pin_uv_auth_protocol, + ); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.uv = true; + make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); + make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params.clone(), DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x45, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); + + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ) + } + + #[test] + fn test_process_make_credential_with_pin_and_uv_v1() { + test_helper_process_make_credential_with_pin_and_uv(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_make_credential_with_pin_and_uv_v2() { + test_helper_process_make_credential_with_pin_and_uv(PinUvAuthProtocol::V2); + } + + #[test] + fn test_non_resident_process_make_credential_with_pin() { + 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(&[0x88; 16], 4).unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x70, + &[], + ); + } + + #[test] + fn test_resident_process_make_credential_with_pin() { + 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(&[0x88; 16], 4).unwrap(); + + let make_credential_params = create_minimal_make_credential_parameters(); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) + ); + } + + #[test] + fn test_process_make_credential_with_pin_always_uv() { + 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.toggle_always_uv().unwrap(); + let make_credential_params = create_minimal_make_credential_parameters(); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) + ); + + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.pin_uv_auth_param = Some(vec![0xA4; 16]); + make_credential_params.pin_uv_auth_protocol = Some(PinUvAuthProtocol::V1); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); } #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1173,11 +1882,91 @@ mod test { ); } + fn check_assertion_response_with_user( + response: Result, + expected_user: PublicKeyCredentialUserEntity, + flags: u8, + signature_counter: u32, + expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], + ) { + match response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x00, + ]; + let signature_counter_position = expected_auth_data.len() - 4; + BigEndian::write_u32( + &mut expected_auth_data[signature_counter_position..], + signature_counter, + ); + expected_auth_data.extend(expected_extension_cbor); + assert_eq!(auth_data, expected_auth_data); + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, expected_number_of_credentials); + } + _ => panic!("Invalid response type"), + } + } + + fn check_assertion_response_with_extension( + response: Result, + expected_user_id: Vec, + signature_counter: u32, + expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x80, + signature_counter, + expected_number_of_credentials, + expected_extension_cbor, + ); + } + + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + signature_counter: u32, + expected_number_of_credentials: Option, + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x00, + signature_counter, + expected_number_of_credentials, + &[], + ); + } + #[test] - fn test_residential_process_get_assertion() { + fn test_resident_process_get_assertion() { 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 make_credential_params = create_minimal_make_credential_parameters(); assert!(ctap_state @@ -1188,7 +1977,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1196,102 +1985,224 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); + } - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - assert!(user.is_none()); - assert!(number_of_credentials.is_none()); + fn get_assertion_hmac_secret_params( + key_agreement_key: crypto::ecdh::SecKey, + key_agreement_response: ResponseData, + credential_id: Option>, + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> AuthenticatorGetAssertionParameters { + let mut rng = ThreadRng256 {}; + let platform_public_key = key_agreement_key.genpk(); + let public_key = match key_agreement_response { + ResponseData::AuthenticatorClientPin(Some(client_pin_response)) => { + client_pin_response.key_agreement.unwrap() } _ => panic!("Invalid response type"), + }; + let pin_protocol = PinProtocol::new_test(key_agreement_key, [0x91; 32]); + let shared_secret = pin_protocol + .decapsulate(public_key, pin_uv_auth_protocol) + .unwrap(); + + let salt = vec![0x01; 32]; + let salt_enc = shared_secret.as_ref().encrypt(&mut rng, &salt).unwrap(); + let salt_auth = shared_secret.authenticate(&salt_enc); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(platform_public_key), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let get_extensions = GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + ..Default::default() + }; + + let credential_descriptor = credential_id.map(|key_id| PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id, + transports: None, + }); + let allow_list = credential_descriptor.map(|c| vec![c]); + AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list, + extensions: get_extensions, + options: GetAssertionOptions { + up: true, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, } } - #[test] - fn test_residential_process_get_assertion_hmac_secret() { + fn test_helper_process_get_assertion_hmac_secret(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; - let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); 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 make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - }); + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = make_extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + let credential_id = match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let auth_data = make_credential_response.auth_data; + let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); + assert_eq!(auth_data[offset], 0x00); + assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_SIZE); + auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_SIZE].to_vec() + } + _ => panic!("Invalid response type"), + }; + + let client_pin_params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command: ClientPinSubCommand::GetKeyAgreement, + key_agreement: None, + pin_uv_auth_param: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + permissions_rp_id: None, + }; + let key_agreement_response = ctap_state.client_pin.process_command( + ctap_state.rng, + &mut ctap_state.persistent_store, + client_pin_params, + DUMMY_CLOCK_VALUE, + ); + let get_assertion_params = get_assertion_hmac_secret_params( + key_agreement_key, + key_agreement_response.unwrap(), + Some(credential_id), + pin_uv_auth_protocol, + ); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + assert!(get_assertion_response.is_ok()); + } + + #[test] + fn test_process_get_assertion_hmac_secret_v1() { + test_helper_process_get_assertion_hmac_secret(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_assertion_hmac_secret_v2() { + test_helper_process_get_assertion_hmac_secret(PinUvAuthProtocol::V2); + } + + fn test_helper_resident_process_get_assertion_hmac_secret( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let make_extensions = MakeCredentialExtensions { + hmac_secret: true, + ..Default::default() + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); - let pk = sk.genpk(); - let hmac_secret_input = GetAssertionHmacSecretInput { - key_agreement: CoseKey::from(pk), - salt_enc: vec![0x02; 32], - salt_auth: vec![0x03; 16], - }; - let get_extensions = Some(GetAssertionExtensions { - hmac_secret: Some(hmac_secret_input), - }); - - let get_assertion_params = AuthenticatorGetAssertionParameters { - rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], - allow_list: None, - extensions: get_extensions, - options: GetAssertionOptions { - up: false, - uv: false, - }, + let client_pin_params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command: ClientPinSubCommand::GetKeyAgreement, + key_agreement: None, pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + permissions_rp_id: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - - assert_eq!( - get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + let key_agreement_response = ctap_state.client_pin.process_command( + ctap_state.rng, + &mut ctap_state.persistent_store, + client_pin_params, + DUMMY_CLOCK_VALUE, ); + let get_assertion_params = get_assertion_hmac_secret_params( + key_agreement_key, + key_agreement_response.unwrap(), + None, + pin_uv_auth_protocol, + ); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + assert!(get_assertion_response.is_ok()); } #[test] - fn test_residential_process_get_assertion_with_cred_protect() { + fn test_process_resident_get_assertion_hmac_secret_v1() { + test_helper_resident_process_get_assertion_hmac_secret(PinUvAuthProtocol::V1); + } + + #[test] + fn test_resident_process_get_assertion_hmac_secret_v2() { + test_helper_resident_process_get_assertion_hmac_secret(PinUvAuthProtocol::V2); + } + + #[test] + fn test_resident_process_get_assertion_with_cred_protect() { let mut rng = ThreadRng256 {}; let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let credential_id = rng.gen_uniform_u8x32().to_vec(); 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 cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, key_id: credential_id.clone(), - transports: None, // You can set USB as a hint here. + transports: None, }; let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id: credential_id.clone(), private_key: private_key.clone(), rp_id: String::from("example.com"), - user_handle: vec![0x00], - other_ui: None, - cred_random: None, + user_handle: vec![0x1D], + user_display_name: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1302,7 +2213,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1310,8 +2221,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1321,7 +2235,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc.clone()]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1329,19 +2243,30 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - assert!(get_assertion_response.is_ok()); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, private_key, rp_id: String::from("example.com"), - user_handle: vec![0x00], - other_ui: None, - cred_random: None, + user_handle: vec![0x1D], + user_display_name: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1352,7 +2277,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1360,20 +2285,382 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), ); } + #[test] + fn test_process_get_assertion_with_cred_blob() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: Some(vec![0xCB]), + large_blob_key: None, + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + cred_blob: true, + ..Default::default() + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB, + ]; + check_assertion_response_with_extension( + get_assertion_response, + vec![0x1D], + signature_counter, + None, + &expected_extension_cbor, + ); + } + + #[test] + fn test_process_get_assertion_with_large_blob_key() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: Some(vec![0x1C; 32]), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + large_blob_key: Some(true), + ..Default::default() + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let large_blob_key = match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + get_assertion_response.large_blob_key.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!(large_blob_key, vec![0x1C; 32]); + } + + fn test_helper_process_get_next_assertion_two_credentials_with_uv( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x88; 32]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + let user1 = PublicKeyCredentialUserEntity { + user_id: vec![0x01], + user_name: Some("user1".to_string()), + user_display_name: Some("User One".to_string()), + user_icon: Some("icon1".to_string()), + }; + make_credential_params.user = user1.clone(); + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + let user2 = PublicKeyCredentialUserEntity { + user_id: vec![0x02], + user_name: Some("user2".to_string()), + user_display_name: Some("User Two".to_string()), + user_icon: Some("icon2".to_string()), + }; + make_credential_params.user = user2.clone(); + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + ctap_state.client_pin = client_pin; + // The PIN length is outside of the test scope and most likely incorrect. + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + let client_data_hash = vec![0xCD]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &client_data_hash, + pin_uv_auth_protocol, + ); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash, + allow_list: None, + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: true, + }, + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response_with_user( + get_assertion_response, + user2, + 0x04, + signature_counter, + Some(2), + &[], + ); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response_with_user( + get_assertion_response, + user1, + 0x04, + signature_counter, + None, + &[], + ); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_get_next_assertion_two_credentials_with_uv_v1() { + test_helper_process_get_next_assertion_two_credentials_with_uv(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_next_assertion_two_credentials_with_uv_v2() { + test_helper_process_get_next_assertion_two_credentials_with_uv(PinUvAuthProtocol::V2); + } + + #[test] + fn test_process_get_next_assertion_three_credentials_no_uv() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x03]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response( + get_assertion_response, + vec![0x03], + signature_counter, + Some(3), + ); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x02], signature_counter, None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], signature_counter, None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_get_next_assertion_not_allowed() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + assert!(get_assertion_response.is_ok()); + + // This is a MakeCredential command. + let mut command_cbor = vec![0x01]; + let cbor_value = cbor_map! { + 1 => vec![0xCD; 16], + 2 => cbor_map! { + "id" => "example.com", + }, + 3 => cbor_map! { + "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + }, + 4 => cbor_array![ES256_CRED_PARAM], + }; + assert!(cbor::write(cbor_value, &mut command_cbor)); + ctap_state.process_command(&command_cbor, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_reset() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - 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 credential_id = vec![0x01, 0x23, 0x45, 0x67]; let credential_source = PublicKeyCredentialSource { @@ -1382,9 +2669,13 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![], - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1392,7 +2683,8 @@ mod test { .is_ok()); assert!(ctap_state.persistent_store.count_credentials().unwrap() > 0); - let reset_reponse = ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID); + let reset_reponse = + ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![0x00]; assert_eq!(reset_reponse, expected_response); assert!(ctap_state.persistent_store.count_credentials().unwrap() == 0); @@ -1402,9 +2694,10 @@ mod test { fn test_process_reset_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); - let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID); + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); assert_eq!( reset_reponse, @@ -1412,16 +2705,45 @@ mod test { ); } + #[test] + fn test_process_reset_not_first() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // This is a GetNextAssertion command. + ctap_state.process_command(&[0x08], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); + } + + #[test] + fn test_process_credential_management_unknown_subcommand() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // The subcommand 0xEE does not exist. + let reponse = ctap_state.process_command( + &[0x0A, 0xA1, 0x01, 0x18, 0xEE], + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let expected_response = vec![Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND as u8]; + assert_eq!(reponse, expected_response); + } + #[test] fn test_process_unknown_command() { 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); // This command does not exist. - let reset_reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID); + let reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; - assert_eq!(reset_reponse, expected_response); + assert_eq!(reponse, expected_response); } #[test] @@ -1429,7 +2751,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - 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); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1450,7 +2772,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - 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); // Same as above. let rp_id_hash = [0x55; 32]; @@ -1466,4 +2788,146 @@ mod test { .is_none()); } } + + #[test] + fn test_signature_counter() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut last_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + assert!(last_counter > 0); + for _ in 0..100 { + assert!(ctap_state.increment_global_signature_counter().is_ok()); + let next_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + assert!(next_counter > last_counter); + last_counter = next_counter; + } + } + + #[test] + fn test_vendor_configure() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // Nothing should be configured at the beginning + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: false, + } + )) + ); + + // Inject dummy values + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Try to inject other dummy values and check that initial values are retained. + let other_dummy_key = [0x44u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: other_dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Now try to lock the device + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + )) + ); + } } diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs new file mode 100644 index 0000000..d1e8f2b --- /dev/null +++ b/src/ctap/pin_protocol.rs @@ -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, 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 { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => hmac_256::(token, message)[..16].to_vec(), + PinUvAuthProtocol::V2 => hmac_256::(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, Ctap2StatusCode>; + + /// Returns the decrypted ciphertext. + fn decrypt(&self, ciphertext: &[u8]) -> Result, 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; +} + +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::(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::(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, Ctap2StatusCode> { + aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, 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 { + hmac_256::(&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::(&handshake, b"CTAP2 AES key"); + SharedSecretV2 { + aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key), + hmac_key: hkdf_empty_salt_256::(&handshake, b"CTAP2 HMAC key"), + } + } +} + +impl SharedSecret for SharedSecretV2 { + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, 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 { + hmac_256::(&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) + ); + } +} diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs deleted file mode 100644 index c03b81b..0000000 --- a/src/ctap/pin_protocol_v1.rs +++ /dev/null @@ -1,1229 +0,0 @@ -// 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. - -use super::command::AuthenticatorClientPinParameters; -use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; -use super::response::{AuthenticatorClientPinResponse, ResponseData}; -use super::status_code::Ctap2StatusCode; -use super::storage::PersistentStore; -#[cfg(feature = "with_ctap2_1")] -use alloc::string::String; -#[cfg(feature = "with_ctap2_1")] -use alloc::vec; -use alloc::vec::Vec; -use arrayref::array_ref; -use core::convert::TryInto; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; -use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits}; -use crypto::rng256::Rng256; -use crypto::sha256::Sha256; -use crypto::Hash256; -#[cfg(all(test, feature = "with_ctap2_1"))] -use enum_iterator::IntoEnumIterator; -use subtle::ConstantTimeEq; - -// Those constants have to be multiples of 16, the AES block size. -pub const PIN_AUTH_LENGTH: usize = 16; -const PIN_PADDED_LENGTH: usize = 64; -const PIN_TOKEN_LENGTH: usize = 32; - -/// Checks the given pin_auth against the truncated output of HMAC-SHA256. -/// Returns LEFT(HMAC(hmac_key, hmac_contents), 16) == pin_auth). -fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - if pin_auth.len() != PIN_AUTH_LENGTH { - return false; - } - verify_hmac_256_first_128bits::( - hmac_key, - hmac_contents, - array_ref![pin_auth, 0, PIN_AUTH_LENGTH], - ) -} - -/// Encrypts the HMAC-secret outputs. To compute them, we first have to -/// decrypt the HMAC secret salt(s) that were encrypted with the shared secret. -/// The credRandom is used as a secret to HMAC those salts. -fn encrypt_hmac_secret_output( - shared_secret: &[u8; 32], - salt_enc: &[u8], - cred_random: &[u8], -) -> Result, Ctap2StatusCode> { - if salt_enc.len() != 32 && salt_enc.len() != 64 { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - if cred_random.len() != 32 { - // We are strict here. We need at least 32 byte, but expect exactly 32. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - // The specification specifically asks for a zero IV. - let iv = [0u8; 16]; - - let mut cred_random_secret = [0u8; 32]; - cred_random_secret.copy_from_slice(cred_random); - - // Initialization of 4 blocks in any case makes this function more readable. - let mut blocks = [[0u8; 16]; 4]; - // With the if clause restriction above, block_len can only be 2 or 4. - let block_len = salt_enc.len() / 16; - for i in 0..block_len { - blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); - } - cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); - - let mut decrypted_salt1 = [0u8; 32]; - decrypted_salt1[..16].copy_from_slice(&blocks[0]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); - decrypted_salt1[16..].copy_from_slice(&blocks[1]); - for i in 0..2 { - blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); - } - - if block_len == 4 { - let mut decrypted_salt2 = [0u8; 32]; - decrypted_salt2[..16].copy_from_slice(&blocks[2]); - decrypted_salt2[16..].copy_from_slice(&blocks[3]); - let output2 = hmac_256::(&cred_random_secret, &decrypted_salt2[..]); - for i in 0..2 { - blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]); - } - } - - cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]); - let mut encrypted_output = Vec::with_capacity(salt_enc.len()); - for b in &blocks[..block_len] { - encrypted_output.extend(b); - } - Ok(encrypted_output) -} - -/// Decrypts the new_pin_enc and outputs the found PIN. -fn decrypt_pin( - aes_dec_key: &crypto::aes256::DecryptionKey, - new_pin_enc: Vec, -) -> Option> { - if new_pin_enc.len() != PIN_PADDED_LENGTH { - return None; - } - let iv = [0u8; 16]; - // Assuming PIN_PADDED_LENGTH % block_size == 0 here. - const BLOCK_COUNT: usize = PIN_PADDED_LENGTH / 16; - let mut blocks = [[0u8; 16]; BLOCK_COUNT]; - for i in 0..BLOCK_COUNT { - blocks[i].copy_from_slice(&new_pin_enc[i * 16..(i + 1) * 16]); - } - cbc_decrypt(aes_dec_key, iv, &mut blocks); - // In CTAP 2.1, the specification changed. The new wording might lead to - // different behavior when there are non-zero bytes after zero bytes. - // This implementation consistently ignores those degenerate cases. - Some( - blocks - .iter() - .flatten() - .cloned() - .take_while(|&c| c != 0) - .collect::>(), - ) -} - -/// Stores the encrypted new PIN in the persistent storage, if it satisfies the -/// PIN policy. The PIN is decrypted and stripped from its padding. Next, the -/// length of the PIN is checked to fulfill policy requirements. Last, the PIN -/// is hashed, truncated to 16 bytes and persistently stored. -fn check_and_store_new_pin( - persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, - new_pin_enc: Vec, -) -> Result<(), Ctap2StatusCode> { - let pin = decrypt_pin(aes_dec_key, new_pin_enc) - .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - - #[cfg(feature = "with_ctap2_1")] - let min_pin_length = persistent_store.min_pin_length()? as usize; - #[cfg(not(feature = "with_ctap2_1"))] - let min_pin_length = 4; - if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { - // TODO(kaczmarczyck) check 4 code point minimum instead - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } - let mut pin_hash = [0u8; 16]; - pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - persistent_store.set_pin_hash(&pin_hash)?; - Ok(()) -} - -#[cfg(feature = "with_ctap2_1")] -#[cfg_attr(test, derive(IntoEnumIterator))] -// TODO remove when all variants are used -#[allow(dead_code)] -pub enum PinPermission { - // All variants should use integers with a single bit set. - MakeCredential = 0x01, - GetAssertion = 0x02, - CredentialManagement = 0x04, - BioEnrollment = 0x08, - PlatformConfiguration = 0x10, - AuthenticatorConfiguration = 0x20, -} - -pub struct PinProtocolV1 { - key_agreement_key: crypto::ecdh::SecKey, - pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], - consecutive_pin_mismatches: u8, - #[cfg(feature = "with_ctap2_1")] - permissions: u8, - #[cfg(feature = "with_ctap2_1")] - permissions_rp_id: Option, -} - -impl PinProtocolV1 { - pub fn new(rng: &mut impl Rng256) -> PinProtocolV1 { - let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - let pin_uv_auth_token = rng.gen_uniform_u8x32(); - PinProtocolV1 { - key_agreement_key, - pin_uv_auth_token, - consecutive_pin_mismatches: 0, - #[cfg(feature = "with_ctap2_1")] - permissions: 0, - #[cfg(feature = "with_ctap2_1")] - permissions_rp_id: None, - } - } - - /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. - /// Resets or decreases the PIN retries, depending on success or failure. - /// Also, in case of failure, the key agreement key is randomly reset. - fn verify_pin_hash_enc( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, - pin_hash_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - match persistent_store.pin_hash()? { - Some(pin_hash) => { - if self.consecutive_pin_mismatches >= 3 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); - } - persistent_store.decr_pin_retries()?; - if pin_hash_enc.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - - let iv = [0u8; 16]; - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash_enc); - cbc_decrypt(aes_dec_key, iv, &mut blocks); - - if !bool::from(pin_hash.ct_eq(&blocks[0])) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - if persistent_store.pin_retries()? == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - self.consecutive_pin_mismatches += 1; - if self.consecutive_pin_mismatches >= 3 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); - } - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - // This status code is not explicitly mentioned in the specification. - None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED), - } - persistent_store.reset_pin_retries()?; - self.consecutive_pin_mismatches = 0; - Ok(()) - } - - /// Uses the self-owned and passed halves of the key agreement to generate the - /// shared secret for checking pin_auth and generating a decryption key. - fn exchange_decryption_key( - &self, - key_agreement: CoseKey, - pin_auth: &[u8], - authenticated_message: &[u8], - ) -> Result { - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - if !verify_pin_auth(&shared_secret, authenticated_message, pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - Ok(aes_dec_key) - } - - fn process_get_pin_retries( - &self, - persistent_store: &PersistentStore, - ) -> Result { - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(persistent_store.pin_retries()? as u64), - }) - } - - fn process_get_key_agreement(&self) -> Result { - let pk = self.key_agreement_key.genpk(); - Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), - pin_token: None, - retries: None, - }) - } - - fn process_set_pin( - &mut self, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - if persistent_store.pin_hash()?.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - let pin_decryption_key = - self.exchange_decryption_key(key_agreement, &pin_auth, &new_pin_enc)?; - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; - persistent_store.reset_pin_retries()?; - Ok(()) - } - - fn process_change_pin( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - pin_hash_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - if persistent_store.pin_retries()? == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - let mut auth_param_data = new_pin_enc.clone(); - auth_param_data.extend(&pin_hash_enc); - let pin_decryption_key = - self.exchange_decryption_key(key_agreement, &pin_auth, &auth_param_data)?; - self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; - - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); - Ok(()) - } - - fn process_get_pin_token( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, - ) -> Result { - if persistent_store.pin_retries()? == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - let token_encryption_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let pin_decryption_key = crypto::aes256::DecryptionKey::new(&token_encryption_key); - self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; - - // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. - let iv = [0u8; 16]; - let mut blocks = [[0u8; 16]; PIN_TOKEN_LENGTH / 16]; - for (i, item) in blocks.iter_mut().take(PIN_TOKEN_LENGTH / 16).enumerate() { - item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]); - } - cbc_encrypt(&token_encryption_key, iv, &mut blocks); - let pin_token: Vec = blocks.iter().flatten().cloned().collect(); - - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0x03; - self.permissions_rp_id = None; - } - - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: Some(pin_token), - retries: None, - }) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_pin_uv_auth_token_using_uv_with_permissions( - &self, - // If you want to support local user verification, implement this function. - // Lacking a fingerprint reader, this subcommand is currently unsupported. - _key_agreement: CoseKey, - _permissions: u8, - _permissions_rp_id: Option, - ) -> Result { - // User verifications is only supported through PIN currently. - #[cfg(not(feature = "with_ctap2_1"))] - { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - #[cfg(feature = "with_ctap2_1")] - { - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) - } - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_uv_retries(&self) -> Result { - // User verifications is only supported through PIN currently. - #[cfg(not(feature = "with_ctap2_1"))] - { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - #[cfg(feature = "with_ctap2_1")] - { - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) - } - } - - #[cfg(feature = "with_ctap2_1")] - fn process_set_min_pin_length( - &mut self, - persistent_store: &mut PersistentStore, - min_pin_length: u8, - min_pin_length_rp_ids: Option>, - pin_auth: Option>, - ) -> Result<(), Ctap2StatusCode> { - if min_pin_length_rp_ids.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - if persistent_store.pin_hash()?.is_some() { - match pin_auth { - Some(pin_auth) => { - if self.consecutive_pin_mismatches >= 3 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); - } - // TODO(kaczmarczyck) Values are taken from the (not yet public) new revision - // of CTAP 2.1. The code should link the specification when published. - // From CTAP2.1: "If request contains pinUvAuthParam, the Authenticator calls - // verify(pinUvAuthToken, 32×0xff || 0x0608 || uint32LittleEndian(minPINLength) - // || minPinLengthRPIDs, pinUvAuthParam)" - let mut message = vec![0xFF; 32]; - message.extend(&[0x06, 0x08]); - message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]); - // TODO(kaczmarczyck) commented code is useful for the extension - // https://github.com/google/OpenSK/issues/129 - // if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) { - // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); - // } - if !verify_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - }; - } - if min_pin_length < persistent_store.min_pin_length()? { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } - persistent_store.set_min_pin_length(min_pin_length)?; - // TODO(kaczmarczyck) commented code is useful for the extension - // https://github.com/google/OpenSK/issues/129 - // if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { - // persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?; - // } - Ok(()) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, - permissions: u8, - permissions_rp_id: Option, - ) -> Result { - if permissions == 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - // This check is not mentioned protocol steps, but mentioned in a side note. - if permissions & 0x03 != 0 && permissions_rp_id.is_none() { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - - let response = - self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; - - self.permissions = permissions; - self.permissions_rp_id = permissions_rp_id; - - Ok(response) - } - - pub fn process_subcommand( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - client_pin_params: AuthenticatorClientPinParameters, - ) -> Result { - let AuthenticatorClientPinParameters { - pin_protocol, - sub_command, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - #[cfg(feature = "with_ctap2_1")] - min_pin_length, - #[cfg(feature = "with_ctap2_1")] - min_pin_length_rp_ids, - #[cfg(feature = "with_ctap2_1")] - permissions, - #[cfg(feature = "with_ctap2_1")] - permissions_rp_id, - } = client_pin_params; - - if pin_protocol != 1 { - #[cfg(not(feature = "with_ctap2_1"))] - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - #[cfg(feature = "with_ctap2_1")] - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - - let response = match sub_command { - ClientPinSubCommand::GetPinRetries => { - Some(self.process_get_pin_retries(persistent_store)?) - } - ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?), - ClientPinSubCommand::SetPin => { - self.process_set_pin( - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - None - } - ClientPinSubCommand::ChangePin => { - self.process_change_pin( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - None - } - ClientPinSubCommand::GetPinToken => Some(self.process_get_pin_token( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?), - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( - self.process_get_pin_uv_auth_token_using_uv_with_permissions( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, - )?, - ), - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::SetMinPinLength => { - self.process_set_min_pin_length( - persistent_store, - min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - min_pin_length_rp_ids, - pin_auth, - )?; - None - } - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( - self.process_get_pin_uv_auth_token_using_pin_with_permissions( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, - )?, - ), - }; - Ok(ResponseData::AuthenticatorClientPin(response)) - } - - pub fn verify_pin_auth_token(&self, hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - verify_pin_auth(&self.pin_uv_auth_token, &hmac_contents, &pin_auth) - } - - pub fn reset(&mut self, rng: &mut impl Rng256) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); - self.consecutive_pin_mismatches = 0; - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0; - self.permissions_rp_id = None; - } - } - - pub fn process_hmac_secret( - &self, - hmac_secret_input: GetAssertionHmacSecretInput, - cred_random: &Option>, - ) -> Result, Ctap2StatusCode> { - let GetAssertionHmacSecretInput { - key_agreement, - salt_enc, - salt_auth, - } = hmac_secret_input; - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - // HMAC-secret does the same 16 byte truncated check. - if !verify_pin_auth(&shared_secret, &salt_enc, &salt_auth) { - // Hard to tell what the correct error code here is. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - - match cred_random { - Some(cr) => encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cr), - // This is the case if the credential was not created with HMAC-secret. - None => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION), - } - } - - #[cfg(feature = "with_ctap2_1")] - pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { - // Relies on the fact that all permissions are represented by powers of two. - if permission as u8 & self.permissions != 0 { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - } - } - - #[cfg(feature = "with_ctap2_1")] - pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - if let Some(permissions_rp_id) = &self.permissions_rp_id { - if rp_id != permissions_rp_id { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } else { - self.permissions_rp_id = Some(String::from(rp_id)); - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use arrayref::array_refs; - use crypto::rng256::ThreadRng256; - - // Stores a PIN hash corresponding to the dummy PIN "1234". - fn set_standard_pin(persistent_store: &mut PersistentStore) { - let mut pin = [0u8; 64]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; - let mut pin_hash = [0u8; 16]; - pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - persistent_store.set_pin_hash(&pin_hash).unwrap(); - } - - // Fails on PINs bigger than 64 bytes. - fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { - assert!(pin.len() <= 64); - let mut padded_pin = [0u8; 64]; - padded_pin[..pin.len()].copy_from_slice(&pin[..]); - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let mut blocks = [[0u8; 16]; 4]; - let (b0, b1, b2, b3) = array_refs!(&padded_pin, 16, 16, 16, 16); - blocks[0][..].copy_from_slice(b0); - blocks[1][..].copy_from_slice(b1); - blocks[2][..].copy_from_slice(b2); - blocks[3][..].copy_from_slice(b3); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - blocks.iter().flatten().cloned().collect::>() - } - - // Encrypts the dummy PIN "1234". - fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { - encrypt_pin(shared_secret, b"1234".to_vec()) - } - - // Encrypts the PIN hash corresponding to the dummy PIN "1234". - fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let mut pin = [0u8; 64]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; - let pin_hash = Sha256::hash(&pin); - - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash[..16]); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - - let mut encrypted_pin_hash = Vec::with_capacity(16); - encrypted_pin_hash.extend(&blocks[0]); - encrypted_pin_hash - } - - #[test] - fn test_verify_pin_hash_enc() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - // The PIN is "1234". - let pin_hash = [ - 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, - 0xC4, 0x12, - ]; - persistent_store.set_pin_hash(&pin_hash).unwrap(); - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pin_hash_enc = vec![ - 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, - 0x99, 0x66, - ]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Ok(()) - ); - - let pin_hash_enc = vec![0xEE; 16]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - - let pin_hash_enc = vec![ - 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, - 0x99, 0x66, - ]; - pin_protocol_v1.consecutive_pin_mismatches = 3; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) - ); - pin_protocol_v1.consecutive_pin_mismatches = 0; - - let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - - let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - } - - #[test] - fn test_process_get_pin_retries() { - let mut rng = ThreadRng256 {}; - let persistent_store = PersistentStore::new(&mut rng); - let pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let expected_response = Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(persistent_store.pin_retries().unwrap() as u64), - }); - assert_eq!( - pin_protocol_v1.process_get_pin_retries(&persistent_store), - expected_response - ); - } - - #[test] - fn test_process_get_key_agreement() { - let mut rng = ThreadRng256 {}; - let pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let expected_response = Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), - pin_token: None, - retries: None, - }); - assert_eq!( - pin_protocol_v1.process_get_key_agreement(), - expected_response - ); - } - - #[test] - fn test_process_set_pin() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); - let key_agreement = CoseKey::from(pk); - let new_pin_enc = encrypt_standard_pin(&shared_secret); - let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); - assert_eq!( - pin_protocol_v1.process_set_pin( - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc - ), - Ok(()) - ); - } - - #[test] - fn test_process_change_pin() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); - let key_agreement = CoseKey::from(pk); - let new_pin_enc = encrypt_standard_pin(&shared_secret); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - let mut auth_param_data = new_pin_enc.clone(); - auth_param_data.extend(&pin_hash_enc); - let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); - assert_eq!( - pin_protocol_v1.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - pin_hash_enc.clone() - ), - Ok(()) - ); - - let bad_pin_hash_enc = vec![0xEE; 16]; - assert_eq!( - pin_protocol_v1.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - bad_pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - - while persistent_store.pin_retries().unwrap() > 0 { - persistent_store.decr_pin_retries().unwrap(); - } - assert_eq!( - pin_protocol_v1.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) - ); - } - - #[test] - fn test_process_get_pin_token() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 - .process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc - ) - .is_ok()); - - let pin_hash_enc = vec![0xEE; 16]; - assert_eq!( - pin_protocol_v1.process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 - .process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x03, - Some(String::from("example.com")), - ) - .is_ok()); - assert_eq!(pin_protocol_v1.permissions, 0x03); - assert_eq!( - pin_protocol_v1.permissions_rp_id, - Some(String::from("example.com")) - ); - - assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x00, - Some(String::from("example.com")), - ), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - - assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x03, - None, - ), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - - let pin_hash_enc = vec![0xEE; 16]; - assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc, - 0x03, - Some(String::from("example.com")), - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_process_set_min_pin_length() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let min_pin_length = 8; - pin_protocol_v1.pin_uv_auth_token = [0x55; PIN_TOKEN_LENGTH]; - let pin_auth = vec![ - 0x94, 0x86, 0xEF, 0x4C, 0xB3, 0x84, 0x2C, 0x85, 0x72, 0x02, 0xBF, 0xE4, 0x36, 0x22, - 0xFE, 0xC9, - ]; - // TODO(kaczmarczyck) implement test for the min PIN length extension - // https://github.com/google/OpenSK/issues/129 - let response = pin_protocol_v1.process_set_min_pin_length( - &mut persistent_store, - min_pin_length, - None, - Some(pin_auth.clone()), - ); - assert_eq!(response, Ok(())); - assert_eq!(persistent_store.min_pin_length().unwrap(), min_pin_length); - let response = pin_protocol_v1.process_set_min_pin_length( - &mut persistent_store, - 7, - None, - Some(pin_auth), - ); - assert_eq!( - response, - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) - ); - assert_eq!(persistent_store.min_pin_length().unwrap(), min_pin_length); - } - - #[test] - fn test_process() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 1, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - pin_auth: None, - new_pin_enc: None, - pin_hash_enc: None, - #[cfg(feature = "with_ctap2_1")] - min_pin_length: None, - #[cfg(feature = "with_ctap2_1")] - min_pin_length_rp_ids: None, - #[cfg(feature = "with_ctap2_1")] - permissions: None, - #[cfg(feature = "with_ctap2_1")] - permissions_rp_id: None, - }; - assert!(pin_protocol_v1 - .process_subcommand(&mut rng, &mut persistent_store, client_pin_params) - .is_ok()); - - let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 2, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - pin_auth: None, - new_pin_enc: None, - pin_hash_enc: None, - #[cfg(feature = "with_ctap2_1")] - min_pin_length: None, - #[cfg(feature = "with_ctap2_1")] - min_pin_length_rp_ids: None, - #[cfg(feature = "with_ctap2_1")] - permissions: None, - #[cfg(feature = "with_ctap2_1")] - permissions_rp_id: None, - }; - #[cfg(not(feature = "with_ctap2_1"))] - let error_code = Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID; - #[cfg(feature = "with_ctap2_1")] - let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; - assert_eq!( - pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params), - Err(error_code) - ); - } - - #[test] - fn test_decrypt_pin() { - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - - // "1234" - let new_pin_enc = vec![ - 0xC0, 0xCF, 0xAE, 0x4C, 0x79, 0x56, 0x87, 0x99, 0xE5, 0x83, 0x4F, 0xE6, 0x4D, 0xFE, - 0x53, 0x32, 0x36, 0x0D, 0xF9, 0x1E, 0x47, 0x66, 0x10, 0x5C, 0x63, 0x30, 0x1D, 0xCC, - 0x00, 0x09, 0x91, 0xA4, 0x20, 0x6B, 0x78, 0x10, 0xFE, 0xC6, 0x2E, 0x7E, 0x75, 0x14, - 0xEE, 0x01, 0x99, 0x6C, 0xD7, 0xE5, 0x2B, 0xA5, 0x7A, 0x5A, 0xE1, 0xEC, 0x69, 0x31, - 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, - ]; - assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(b"1234".to_vec()), - ); - - // "123" - let new_pin_enc = vec![ - 0xF3, 0x54, 0x29, 0x17, 0xD4, 0xF8, 0xCD, 0x23, 0x1D, 0x59, 0xED, 0xE5, 0x33, 0x42, - 0x13, 0x39, 0x22, 0xBB, 0x91, 0x28, 0x87, 0x6A, 0xF9, 0xB1, 0x80, 0x9C, 0x9D, 0x76, - 0xFF, 0xDD, 0xB8, 0xD6, 0x8D, 0x66, 0x99, 0xA2, 0x42, 0x67, 0xB0, 0x5C, 0x82, 0x3F, - 0x08, 0x55, 0x8C, 0x04, 0xC5, 0x91, 0xF0, 0xF9, 0x58, 0x44, 0x00, 0x1B, 0x99, 0xA6, - 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, - ]; - assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(b"123".to_vec()), - ); - - // Encrypted PIN is too short. - let new_pin_enc = vec![0x44; 63]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); - - // Encrypted PIN is too long. - let new_pin_enc = vec![0x44; 65]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); - } - - #[test] - fn test_check_and_store_new_pin() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - - let test_cases = vec![ - // Accept PIN "1234". - (b"1234".to_vec(), Ok(())), - // Reject PIN "123" since it is too short. - ( - b"123".to_vec(), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), - ), - // Reject PIN "12'\0'4" (a zero byte at index 2). - ( - b"12\04".to_vec(), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), - ), - // PINs must be at most 63 bytes long, to allow for a trailing 0u8 padding. - ( - vec![0x30; 64], - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), - ), - ]; - for (pin, result) in test_cases { - let old_pin_hash = persistent_store.pin_hash().unwrap(); - let new_pin_enc = encrypt_pin(&shared_secret, pin); - assert_eq!( - check_and_store_new_pin(&mut persistent_store, &aes_dec_key, new_pin_enc), - result - ); - if result.is_ok() { - assert_ne!(old_pin_hash, persistent_store.pin_hash().unwrap()); - } else { - assert_eq!(old_pin_hash, persistent_store.pin_hash().unwrap()); - } - } - } - - #[test] - fn test_verify_pin_auth() { - let hmac_key = [0x88; 16]; - let pin_auth = [ - 0x88, 0x09, 0x41, 0x13, 0xF7, 0x97, 0x32, 0x0B, 0x3E, 0xD9, 0xBC, 0x76, 0x4F, 0x18, - 0x56, 0x5D, - ]; - assert!(verify_pin_auth(&hmac_key, &[], &pin_auth)); - assert!(!verify_pin_auth(&hmac_key, &[0x00], &pin_auth)); - } - - #[test] - fn test_encrypt_hmac_secret_output() { - let shared_secret = [0x55; 32]; - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 32); - - let salt_enc = [0x5E; 48]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); - - let salt_enc = [0x5E; 64]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 64); - - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 33]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_has_permission() { - let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - pin_protocol_v1.permissions = 0x7F; - for permission in PinPermission::into_enum_iter() { - assert_eq!(pin_protocol_v1.has_permission(permission), Ok(())); - } - pin_protocol_v1.permissions = 0x00; - for permission in PinPermission::into_enum_iter() { - assert_eq!( - pin_protocol_v1.has_permission(permission), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - } - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_has_permission_for_rp_id() { - let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.permissions_rp_id, - Some(String::from("example.com")) - ); - assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("counter-example.com"), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - } -} diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 47e1d54..765403d 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -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), AuthenticatorReset, - #[cfg(feature = "with_ctap2_1")] + AuthenticatorCredentialManagement(Option), AuthenticatorSelection, + AuthenticatorLargeBlobs(Option), + // TODO(kaczmarczyck) dummy, extend + AuthenticatorConfig, + AuthenticatorVendor(AuthenticatorVendorResponse), } impl From for Option { @@ -43,21 +44,24 @@ impl From for Option { 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, pub att_stmt: PackedAttestationStatement, + pub ep_att: Option, + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -66,24 +70,29 @@ impl From 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, pub auth_data: Vec, pub signature: Vec, pub user: Option, pub number_of_credentials: Option, + // 0x06: userSelected missing as we don't support displays. + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -94,45 +103,49 @@ impl From 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, pub extensions: Option>, pub aaguid: [u8; 16], - pub options: Option>, + pub options: Option>, pub max_msg_size: Option, pub pin_protocols: Option>, - #[cfg(feature = "with_ctap2_1")] pub max_credential_count_in_list: Option, - #[cfg(feature = "with_ctap2_1")] pub max_credential_id_length: Option, - #[cfg(feature = "with_ctap2_1")] pub transports: Option>, - #[cfg(feature = "with_ctap2_1")] pub algorithms: Option>, - pub default_cred_protect: Option, - #[cfg(feature = "with_ctap2_1")] + pub max_serialized_large_blob_array: Option, + pub force_pin_change: Option, pub min_pin_length: u8, - #[cfg(feature = "with_ctap2_1")] pub firmware_version: Option, + pub max_cred_blob_length: Option, + pub max_rp_ids_for_set_min_pin_length: Option, + // 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>, + pub remaining_discoverable_credentials: Option, + // - 0x15: vendorPrototypeConfigCommands missing as we don't support it. } impl From for cbor::Value { - #[cfg(feature = "with_ctap2_1")] fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { let AuthenticatorGetInfoResponse { versions, @@ -145,17 +158,30 @@ impl From 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 = 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 = 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 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 = 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, - pub pin_token: Option>, + pub pin_uv_auth_token: Option>, pub retries: Option, + pub power_cycle_state: Option, + // - 0x05: uvRetries missing as we don't support internal UV. } impl From 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, +} + +impl From 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, + pub max_possible_remaining_resident_credentials_count: Option, + pub rp: Option, + pub rp_id_hash: Option>, + pub total_rps: Option, + pub user: Option, + pub credential_id: Option, + pub public_key: Option, + pub total_credentials: Option, + pub cred_protect: Option, + pub large_blob_key: Option>, +} + +impl From 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 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 = 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 = 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 = 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 = 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 = 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 = + 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 = + 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 = + ResponseData::AuthenticatorCredentialManagement(None).into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_selection_into_cbor() { let response_cbor: Option = 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 = + 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 = ResponseData::AuthenticatorLargeBlobs(None).into(); + assert_eq!(response_cbor, None); + } + + #[test] + fn test_config_into_cbor() { + let response_cbor: Option = ResponseData::AuthenticatorConfig.into(); + assert_eq!(response_cbor, None); + } + + #[test] + fn test_vendor_response_into_cbor() { + let response_cbor: Option = + 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 = + ResponseData::AuthenticatorVendor(AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: true, + }) + .into(); + assert_eq!( + response_cbor, + Some(cbor_map_options! { + 0x01 => false, + 0x02 => true, + }) + ); + } } diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index adb84fd..a593dad 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -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, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..084cc1b 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -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,168 +12,53 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "with_ctap2_1")] -use crate::ctap::data_formats::{extract_array, extract_text_string}; -use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; -use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; +mod key; + +use crate::ctap::client_pin::PIN_AUTH_LENGTH; +use crate::ctap::customization::{ + DEFAULT_MIN_PIN_LENGTH, DEFAULT_MIN_PIN_LENGTH_RP_IDS, ENFORCE_ALWAYS_UV, + MAX_LARGE_BLOB_ARRAY_SIZE, MAX_PIN_RETRIES, MAX_RP_IDS_LENGTH, MAX_SUPPORTED_RESIDENT_KEYS, + NUM_PAGES, +}; +use crate::ctap::data_formats::{ + extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, +}; +use crate::ctap::key_material; use crate::ctap::status_code::Ctap2StatusCode; -use crate::ctap::{key_material, USE_BATCH_ATTESTATION}; -use crate::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; +use crate::ctap::INITIAL_SIGNATURE_COUNTER; +use crate::embedded_flash::{new_storage, Storage}; use alloc::string::String; -#[cfg(any(test, feature = "ram_storage", feature = "with_ctap2_1"))] use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; -#[cfg(feature = "with_ctap2_1")] use cbor::cbor_array_vec; +use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; +use persistent_store::{fragment, StoreUpdate}; -#[cfg(any(test, feature = "ram_storage"))] -type Storage = embedded_flash::BufferStorage; -#[cfg(not(any(test, feature = "ram_storage")))] -type Storage = embedded_flash::SyscallStorage; - -// Those constants may be modified before compilation to tune the behavior of the key. -// -// The number of pages should be at least 2 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 depends on the -// number of pages. This will improve in the future. Currently, using 20 pages gives 65ms per -// operation. The rule of thumb is 3.5ms per additional page. -// -// Limiting the number of residential keys permits to ensure a minimum number of counter increments. -// Let: -// - P the number of pages (NUM_PAGES) -// - K the maximum number of residential keys (MAX_SUPPORTED_RESIDENTIAL_KEYS) -// - S the maximum size of a residential key (about 500) -// - C the number of erase cycles (10000) -// - I the minimum number of counter increments -// -// We have: I = ((P - 1) * 4092 - K * S) / 12 * C -// -// With P=20 and K=150, we have I > 2M which is enough for 500 increments per day for 10 years. -#[cfg(feature = "ram_storage")] -const NUM_PAGES: usize = 2; -#[cfg(not(feature = "ram_storage"))] -const NUM_PAGES: usize = 20; -const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; - -// List of tags. They should all be unique. And there should be less than NUM_TAGS. -const TAG_CREDENTIAL: usize = 0; -const GLOBAL_SIGNATURE_COUNTER: usize = 1; -const MASTER_KEYS: usize = 2; -const PIN_HASH: usize = 3; -const PIN_RETRIES: usize = 4; -const ATTESTATION_PRIVATE_KEY: usize = 5; -const ATTESTATION_CERTIFICATE: usize = 6; -const AAGUID: usize = 7; -#[cfg(feature = "with_ctap2_1")] -const MIN_PIN_LENGTH: usize = 8; -#[cfg(feature = "with_ctap2_1")] -const MIN_PIN_LENGTH_RP_IDS: usize = 9; -// Different NUM_TAGS depending on the CTAP version make the storage incompatible, -// so we use the maximum. -const NUM_TAGS: usize = 10; - -const MAX_PIN_RETRIES: u8 = 8; -const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; -const AAGUID_LENGTH: usize = 16; -#[cfg(feature = "with_ctap2_1")] -const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -// TODO(kaczmarczyck) use this for the minPinLength extension -// https://github.com/google/OpenSK/issues/129 -#[cfg(feature = "with_ctap2_1")] -const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); -// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. -#[cfg(feature = "with_ctap2_1")] -const _MAX_RP_IDS_LENGTH: usize = 8; - -#[allow(clippy::enum_variant_names)] -#[derive(PartialEq, Eq, PartialOrd, Ord)] -enum Key { - // TODO(cretin): Test whether this doesn't consume too much memory. Otherwise, we can use less - // keys. Either only a simple enum value for all credentials, or group by rp_id. - Credential { - rp_id: Option, - credential_id: Option>, - user_handle: Option>, - }, - GlobalSignatureCounter, - MasterKeys, - PinHash, - PinRetries, - AttestationPrivateKey, - AttestationCertificate, - Aaguid, - #[cfg(feature = "with_ctap2_1")] - MinPinLength, - #[cfg(feature = "with_ctap2_1")] - MinPinLengthRpIds, -} - +/// Wrapper for master keys. pub struct MasterKeys { + /// Master encryption key. pub encryption: [u8; 32], + + /// Master hmac key. pub hmac: [u8; 32], } -struct Config; +/// Wrapper for PIN properties. +struct PinProperties { + /// 16 byte prefix of SHA256 of the currently set PIN. + hash: [u8; PIN_AUTH_LENGTH], -impl StoreConfig for Config { - type Key = Key; - - fn num_tags(&self) -> usize { - NUM_TAGS - } - - fn keys(&self, entry: StoreEntry, mut add: impl FnMut(Key)) { - match entry.tag { - TAG_CREDENTIAL => { - let credential = match deserialize_credential(entry.data) { - None => { - debug_assert!(false); - return; - } - Some(credential) => credential, - }; - add(Key::Credential { - rp_id: Some(credential.rp_id.clone()), - credential_id: Some(credential.credential_id), - user_handle: None, - }); - add(Key::Credential { - rp_id: Some(credential.rp_id.clone()), - credential_id: None, - user_handle: None, - }); - add(Key::Credential { - rp_id: Some(credential.rp_id), - credential_id: None, - user_handle: Some(credential.user_handle), - }); - add(Key::Credential { - rp_id: None, - credential_id: None, - user_handle: None, - }); - } - GLOBAL_SIGNATURE_COUNTER => add(Key::GlobalSignatureCounter), - MASTER_KEYS => add(Key::MasterKeys), - PIN_HASH => add(Key::PinHash), - PIN_RETRIES => add(Key::PinRetries), - ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey), - ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate), - AAGUID => add(Key::Aaguid), - #[cfg(feature = "with_ctap2_1")] - MIN_PIN_LENGTH => add(Key::MinPinLength), - #[cfg(feature = "with_ctap2_1")] - MIN_PIN_LENGTH_RP_IDS => add(Key::MinPinLengthRpIds), - _ => debug_assert!(false), - } - } + /// Length of the current PIN in code points. + code_point_length: u8, } +/// CTAP persistent storage. pub struct PersistentStore { - store: embedded_flash::Store, + store: persistent_store::Store, } impl PersistentStore { @@ -183,473 +68,674 @@ impl PersistentStore { /// /// This should be at most one instance of persistent store per program lifetime. pub fn new(rng: &mut impl Rng256) -> PersistentStore { - #[cfg(not(any(test, feature = "ram_storage")))] - let storage = PersistentStore::new_prod_storage(); - #[cfg(any(test, feature = "ram_storage"))] - let storage = PersistentStore::new_test_storage(); + let storage = new_storage(NUM_PAGES); let mut store = PersistentStore { - store: embedded_flash::Store::new(storage, Config).unwrap(), + store: persistent_store::Store::new(storage).ok().unwrap(), }; - store.init(rng); + store.init(rng).ok().unwrap(); store } - #[cfg(not(any(test, feature = "ram_storage")))] - fn new_prod_storage() -> Storage { - Storage::new(NUM_PAGES).unwrap() - } - - #[cfg(any(test, feature = "ram_storage"))] - fn new_test_storage() -> Storage { - #[cfg(not(test))] - const PAGE_SIZE: usize = 0x100; - #[cfg(test)] - const PAGE_SIZE: usize = 0x1000; - let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); - let options = embedded_flash::BufferOptions { - word_size: 4, - page_size: PAGE_SIZE, - max_word_writes: 2, - max_page_erases: 10000, - strict_write: true, - }; - Storage::new(store, options) - } - - fn init(&mut self, rng: &mut impl Rng256) { - if self.store.find_one(&Key::MasterKeys).is_none() { + /// Initializes the store by creating missing objects. + fn init(&mut self, rng: &mut impl Rng256) -> Result<(), Ctap2StatusCode> { + // Generate and store the master keys if they are missing. + if self.store.find_handle(key::MASTER_KEYS)?.is_none() { let master_encryption_key = rng.gen_uniform_u8x32(); let master_hmac_key = rng.gen_uniform_u8x32(); let mut master_keys = Vec::with_capacity(64); master_keys.extend_from_slice(&master_encryption_key); master_keys.extend_from_slice(&master_hmac_key); - self.store - .insert(StoreEntry { - tag: MASTER_KEYS, - data: &master_keys, - sensitive: true, - }) - .unwrap(); + self.store.insert(key::MASTER_KEYS, &master_keys)?; } - // The following 3 entries are meant to be written by vendor-specific commands. - if USE_BATCH_ATTESTATION { - if self.store.find_one(&Key::AttestationPrivateKey).is_none() { - self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) - .unwrap(); - } - if self.store.find_one(&Key::AttestationCertificate).is_none() { - self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) - .unwrap(); - } + + // Generate and store the CredRandom secrets if they are missing. + if self.store.find_handle(key::CRED_RANDOM_SECRET)?.is_none() { + let cred_random_with_uv = rng.gen_uniform_u8x32(); + let cred_random_without_uv = rng.gen_uniform_u8x32(); + let mut cred_random = Vec::with_capacity(64); + cred_random.extend_from_slice(&cred_random_without_uv); + cred_random.extend_from_slice(&cred_random_with_uv); + self.store.insert(key::CRED_RANDOM_SECRET, &cred_random)?; } - if self.store.find_one(&Key::Aaguid).is_none() { - self.set_aaguid(key_material::AAGUID).unwrap(); + + if self.store.find_handle(key::AAGUID)?.is_none() { + self.set_aaguid(key_material::AAGUID)?; } + Ok(()) } + /// Returns the credential at the given key. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_VENDOR_INTERNAL_ERROR` if the key does not hold a valid credential. + pub fn get_credential(&self, key: usize) -> Result { + let min_key = key::CREDENTIALS.start; + if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENT_KEYS { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let credential_entry = self + .store + .find(key)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + deserialize_credential(&credential_entry) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + + /// Finds the key and value for a given credential ID. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn find_credential_item( + &self, + credential_id: &[u8], + ) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> { + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + let mut credentials: Vec<(usize, PublicKeyCredentialSource)> = iter + .filter(|(_, credential)| credential.credential_id == credential_id) + .collect(); + iter_result?; + if credentials.len() > 1 { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + } + + /// Returns the first matching credential. + /// + /// Returns `None` if no credentials are matched or if `check_cred_protect` is set and the first + /// matched credential requires user verification. pub fn find_credential( &self, rp_id: &str, credential_id: &[u8], check_cred_protect: bool, ) -> Result, Ctap2StatusCode> { - let key = Key::Credential { - rp_id: Some(rp_id.into()), - credential_id: Some(credential_id.into()), - user_handle: None, + let credential = match self.find_credential_item(credential_id) { + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) => return Ok(None), + Err(e) => return Err(e), + Ok((_key, credential)) => credential, }; - let entry = match self.store.find_one(&key) { - None => return Ok(None), - Some((_, entry)) => entry, - }; - debug_assert_eq!(entry.tag, TAG_CREDENTIAL); - let result = deserialize_credential(entry.data); - debug_assert!(result.is_some()); - let user_verification_required = result.as_ref().map_or(false, |cred| { - cred.cred_protect_policy == Some(CredentialProtectionPolicy::UserVerificationRequired) - }); - if check_cred_protect && user_verification_required { - Ok(None) - } else { - Ok(result) + let is_protected = credential.cred_protect_policy + == Some(CredentialProtectionPolicy::UserVerificationRequired); + if credential.rp_id != rp_id || (check_cred_protect && is_protected) { + return Ok(None); } + Ok(Some(credential)) } + /// Stores or updates a credential. + /// + /// If a credential with the same RP id and user handle already exists, it is replaced. pub fn store_credential( &mut self, - credential: PublicKeyCredentialSource, + new_credential: PublicKeyCredentialSource, ) -> Result<(), Ctap2StatusCode> { - let key = Key::Credential { - rp_id: Some(credential.rp_id.clone()), - credential_id: None, - user_handle: Some(credential.user_handle.clone()), - }; - let old_entry = self.store.find_one(&key); - if old_entry.is_none() && self.count_credentials()? >= MAX_SUPPORTED_RESIDENTIAL_KEYS { + // Holds the key of the existing credential if this is an update. + let mut old_key = None; + let min_key = key::CREDENTIALS.start; + // Holds whether a key is used (indices are shifted by min_key). + let mut keys = vec![false; MAX_SUPPORTED_RESIDENT_KEYS]; + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + for (key, credential) in iter { + if key < min_key || key - min_key >= MAX_SUPPORTED_RESIDENT_KEYS || keys[key - min_key] + { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + keys[key - min_key] = true; + if credential.rp_id == new_credential.rp_id + && credential.user_handle == new_credential.user_handle + { + if old_key.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + old_key = Some(key); + } + } + iter_result?; + if old_key.is_none() && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENT_KEYS { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } - let credential = serialize_credential(credential)?; - let new_entry = StoreEntry { - tag: TAG_CREDENTIAL, - data: &credential, - sensitive: true, - }; - match old_entry { - None => self.store.insert(new_entry)?, - Some((index, old_entry)) => { - debug_assert_eq!(old_entry.tag, TAG_CREDENTIAL); - self.store.replace(index, new_entry)? - } + let key = match old_key { + // This is a new credential being added, we need to allocate a free key. We choose the + // first available key. + None => key::CREDENTIALS + .take(MAX_SUPPORTED_RESIDENT_KEYS) + .find(|key| !keys[key - min_key]) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, + // This is an existing credential being updated, we reuse its key. + Some(x) => x, }; + let value = serialize_credential(new_credential)?; + self.store.insert(key, &value)?; Ok(()) } - pub fn filter_credential( - &self, - rp_id: &str, - check_cred_protect: bool, - ) -> Result, Ctap2StatusCode> { - Ok(self - .store - .find_all(&Key::Credential { - rp_id: Some(rp_id.into()), - credential_id: None, - user_handle: None, - }) - .filter_map(|(_, entry)| { - debug_assert_eq!(entry.tag, TAG_CREDENTIAL); - let credential = deserialize_credential(entry.data); - debug_assert!(credential.is_some()); - credential - }) - .filter(|cred| !check_cred_protect || cred.is_discoverable()) - .collect()) + /// Deletes a credential. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { + let (key, _) = self.find_credential_item(credential_id)?; + Ok(self.store.remove(key)?) } + /// Updates a credential's user information. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn update_credential( + &mut self, + credential_id: &[u8], + user: PublicKeyCredentialUserEntity, + ) -> Result<(), Ctap2StatusCode> { + let (key, mut credential) = self.find_credential_item(credential_id)?; + credential.user_name = user.user_name; + credential.user_display_name = user.user_display_name; + credential.user_icon = user.user_icon; + let value = serialize_credential(credential)?; + Ok(self.store.insert(key, &value)?) + } + + /// Returns the number of credentials. pub fn count_credentials(&self) -> Result { - Ok(self - .store - .find_all(&Key::Credential { - rp_id: None, - credential_id: None, - user_handle: None, - }) - .count()) - } - - pub fn global_signature_counter(&self) -> Result { - Ok(self - .store - .find_one(&Key::GlobalSignatureCounter) - .map_or(0, |(_, entry)| { - u32::from_ne_bytes(*array_ref!(entry.data, 0, 4)) - })) - } - - pub fn incr_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { - let mut buffer = [0; core::mem::size_of::()]; - match self.store.find_one(&Key::GlobalSignatureCounter) { - None => { - buffer.copy_from_slice(&1u32.to_ne_bytes()); - self.store.insert(StoreEntry { - tag: GLOBAL_SIGNATURE_COUNTER, - data: &buffer, - sensitive: false, - })?; - } - Some((index, entry)) => { - let value = u32::from_ne_bytes(*array_ref!(entry.data, 0, 4)); - // In hopes that servers handle the wrapping gracefully. - buffer.copy_from_slice(&value.wrapping_add(1).to_ne_bytes()); - self.store.replace( - index, - StoreEntry { - tag: GLOBAL_SIGNATURE_COUNTER, - data: &buffer, - sensitive: false, - }, - )?; - } + let mut count = 0; + for handle in self.store.iter()? { + count += key::CREDENTIALS.contains(&handle?.get_key()) as usize; } + Ok(count) + } + + /// Returns the estimated number of credentials that can still be stored. + pub fn remaining_credentials(&self) -> Result { + MAX_SUPPORTED_RESIDENT_KEYS + .checked_sub(self.count_credentials()?) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + + /// Iterates through the credentials. + /// + /// If an error is encountered during iteration, it is written to `result`. + pub fn iter_credentials<'a>( + &'a self, + result: &'a mut Result<(), Ctap2StatusCode>, + ) -> Result, Ctap2StatusCode> { + IterCredentials::new(&self.store, result) + } + + /// Returns the next creation order. + pub fn new_creation_order(&self) -> Result { + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + let max = iter.map(|(_, credential)| credential.creation_order).max(); + iter_result?; + Ok(max.unwrap_or(0).wrapping_add(1)) + } + + /// Returns the global signature counter. + pub fn global_signature_counter(&self) -> Result { + match self.store.find(key::GLOBAL_SIGNATURE_COUNTER)? { + None => Ok(INITIAL_SIGNATURE_COUNTER), + Some(value) if value.len() == 4 => Ok(u32::from_ne_bytes(*array_ref!(&value, 0, 4))), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Increments the global signature counter. + pub fn incr_global_signature_counter(&mut self, increment: u32) -> Result<(), Ctap2StatusCode> { + let old_value = self.global_signature_counter()?; + // In hopes that servers handle the wrapping gracefully. + let new_value = old_value.wrapping_add(increment); + self.store + .insert(key::GLOBAL_SIGNATURE_COUNTER, &new_value.to_ne_bytes())?; Ok(()) } + /// Returns the master keys. pub fn master_keys(&self) -> Result { - let (_, entry) = self.store.find_one(&Key::MasterKeys).unwrap(); - if entry.data.len() != 64 { + let master_keys = self + .store + .find(key::MASTER_KEYS)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + if master_keys.len() != 64 { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(MasterKeys { - encryption: *array_ref![entry.data, 0, 32], - hmac: *array_ref![entry.data, 32, 32], + encryption: *array_ref![master_keys, 0, 32], + hmac: *array_ref![master_keys, 32, 32], }) } - pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { - let data = match self.store.find_one(&Key::PinHash) { - None => return Ok(None), - Some((_, entry)) => entry.data, - }; - if data.len() != PIN_AUTH_LENGTH { + /// Returns the CredRandom secret. + pub fn cred_random_secret(&self, has_uv: bool) -> Result<[u8; 32], Ctap2StatusCode> { + let cred_random_secret = self + .store + .find(key::CRED_RANDOM_SECRET)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + if cred_random_secret.len() != 64 { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(Some(*array_ref![data, 0, PIN_AUTH_LENGTH])) + let offset = if has_uv { 32 } else { 0 }; + Ok(*array_ref![cred_random_secret, offset, 32]) } - pub fn set_pin_hash( + /// Reads the PIN properties and wraps them into PinProperties. + fn pin_properties(&self) -> Result, Ctap2StatusCode> { + let pin_properties = match self.store.find(key::PIN_PROPERTIES)? { + None => return Ok(None), + Some(pin_properties) => pin_properties, + }; + const PROPERTIES_LENGTH: usize = PIN_AUTH_LENGTH + 1; + match pin_properties.len() { + PROPERTIES_LENGTH => Ok(Some(PinProperties { + hash: *array_ref![pin_properties, 1, PIN_AUTH_LENGTH], + code_point_length: pin_properties[0], + })), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Returns the PIN hash if defined. + pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { + Ok(self.pin_properties()?.map(|p| p.hash)) + } + + /// Returns the length of the currently set PIN if defined. + pub fn pin_code_point_length(&self) -> Result, Ctap2StatusCode> { + Ok(self.pin_properties()?.map(|p| p.code_point_length)) + } + + /// Sets the PIN hash and length. + /// + /// If it was already defined, it is updated. + pub fn set_pin( &mut self, pin_hash: &[u8; PIN_AUTH_LENGTH], + pin_code_point_length: u8, ) -> Result<(), Ctap2StatusCode> { - let entry = StoreEntry { - tag: PIN_HASH, - data: pin_hash, - sensitive: true, - }; - match self.store.find_one(&Key::PinHash) { - None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, - } - Ok(()) + let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; + pin_properties[0] = pin_code_point_length; + pin_properties[1..].clone_from_slice(pin_hash); + Ok(self.store.transaction(&[ + StoreUpdate::Insert { + key: key::PIN_PROPERTIES, + value: &pin_properties[..], + }, + StoreUpdate::Remove { + key: key::FORCE_PIN_CHANGE, + }, + ])?) } + /// Returns the number of remaining PIN retries. pub fn pin_retries(&self) -> Result { - Ok(self - .store - .find_one(&Key::PinRetries) - .map_or(MAX_PIN_RETRIES, |(_, entry)| { - debug_assert_eq!(entry.data.len(), 1); - entry.data[0] - })) + match self.store.find(key::PIN_RETRIES)? { + None => Ok(MAX_PIN_RETRIES), + Some(value) if value.len() == 1 => Ok(value[0]), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } } + /// Decrements the number of remaining PIN retries. pub fn decr_pin_retries(&mut self) -> Result<(), Ctap2StatusCode> { - match self.store.find_one(&Key::PinRetries) { - None => { - self.store.insert(StoreEntry { - tag: PIN_RETRIES, - data: &[MAX_PIN_RETRIES.saturating_sub(1)], - sensitive: false, - })?; - } - Some((index, entry)) => { - debug_assert_eq!(entry.data.len(), 1); - if entry.data[0] == 0 { - return Ok(()); - } - let new_value = entry.data[0].saturating_sub(1); - self.store.replace( - index, - StoreEntry { - tag: PIN_RETRIES, - data: &[new_value], - sensitive: false, - }, - )?; - } + let old_value = self.pin_retries()?; + let new_value = old_value.saturating_sub(1); + if new_value != old_value { + self.store.insert(key::PIN_RETRIES, &[new_value])?; } Ok(()) } + /// Resets the number of remaining PIN retries. pub fn reset_pin_retries(&mut self) -> Result<(), Ctap2StatusCode> { - if let Some((index, _)) = self.store.find_one(&Key::PinRetries) { - self.store.delete(index)?; - } - Ok(()) + Ok(self.store.remove(key::PIN_RETRIES)?) } - #[cfg(feature = "with_ctap2_1")] + /// Returns the minimum PIN length. pub fn min_pin_length(&self) -> Result { - Ok(self - .store - .find_one(&Key::MinPinLength) - .map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| { - debug_assert_eq!(entry.data.len(), 1); - entry.data[0] - })) + match self.store.find(key::MIN_PIN_LENGTH)? { + None => Ok(DEFAULT_MIN_PIN_LENGTH), + Some(value) if value.len() == 1 => Ok(value[0]), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } } - #[cfg(feature = "with_ctap2_1")] + /// Sets the minimum PIN length. pub fn set_min_pin_length(&mut self, min_pin_length: u8) -> Result<(), Ctap2StatusCode> { - let entry = StoreEntry { - tag: MIN_PIN_LENGTH, - data: &[min_pin_length], - sensitive: false, - }; - Ok(match self.store.find_one(&Key::MinPinLength) { - None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, - }) + Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) } - #[cfg(feature = "with_ctap2_1")] - pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { - let rp_ids = self - .store - .find_one(&Key::MinPinLengthRpIds) - .map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |(_, entry)| { - _deserialize_min_pin_length_rp_ids(entry.data) - }); + /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is + /// allowed. + pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { + let rp_ids = self.store.find(key::MIN_PIN_LENGTH_RP_IDS)?.map_or_else( + || { + Some( + DEFAULT_MIN_PIN_LENGTH_RP_IDS + .iter() + .map(|&s| String::from(s)) + .collect(), + ) + }, + |value| deserialize_min_pin_length_rp_ids(&value), + ); debug_assert!(rp_ids.is_some()); - Ok(rp_ids.unwrap_or(vec![])) + Ok(rp_ids.unwrap_or_default()) } - #[cfg(feature = "with_ctap2_1")] - pub fn _set_min_pin_length_rp_ids( + /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. + pub fn set_min_pin_length_rp_ids( &mut self, min_pin_length_rp_ids: Vec, ) -> Result<(), Ctap2StatusCode> { let mut min_pin_length_rp_ids = min_pin_length_rp_ids; - for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } } - if min_pin_length_rp_ids.len() > _MAX_RP_IDS_LENGTH { + if min_pin_length_rp_ids.len() > MAX_RP_IDS_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } - let entry = StoreEntry { - tag: MIN_PIN_LENGTH_RP_IDS, - data: &_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, - sensitive: false, - }; - match self.store.find_one(&Key::MinPinLengthRpIds) { - None => { - self.store.insert(entry).unwrap(); - } - Some((index, _)) => { - self.store.replace(index, entry).unwrap(); - } - } - Ok(()) + Ok(self.store.insert( + key::MIN_PIN_LENGTH_RP_IDS, + &serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, + )?) } - pub fn attestation_private_key( + /// Reads the byte vector stored as the serialized large blobs array. + /// + /// If too few bytes exist at that offset, return the maximum number + /// available. This includes cases of offset being beyond the stored array. + /// + /// If no large blob is committed to the store, get responds as if an empty + /// CBOR array (0x80) was written, together with the 16 byte prefix of its + /// SHA256, to a total length of 17 byte (which is the shortest legitimate + /// large blob entry possible). + pub fn get_large_blob_array( &self, - ) -> Result, Ctap2StatusCode> { - let data = match self.store.find_one(&Key::AttestationPrivateKey) { - None => return Ok(None), - Some((_, entry)) => entry.data, - }; - if data.len() != ATTESTATION_PRIVATE_KEY_LENGTH { + offset: usize, + byte_count: usize, + ) -> Result, Ctap2StatusCode> { + let byte_range = offset..offset + byte_count; + let output = fragment::read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; + Ok(output.unwrap_or_else(|| { + const EMPTY_LARGE_BLOB: [u8; 17] = [ + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, + ]; + let last_index = cmp::min(EMPTY_LARGE_BLOB.len(), offset + byte_count); + EMPTY_LARGE_BLOB + .get(offset..last_index) + .unwrap_or_default() + .to_vec() + })) + } + + /// Sets a byte vector as the serialized large blobs array. + pub fn commit_large_blob_array( + &mut self, + large_blob_array: &[u8], + ) -> Result<(), Ctap2StatusCode> { + // This input should have been caught at caller level. + if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(Some(array_ref!(data, 0, ATTESTATION_PRIVATE_KEY_LENGTH))) + Ok(fragment::write( + &mut self.store, + &key::LARGE_BLOB_SHARDS, + large_blob_array, + )?) } + /// Returns the attestation private key if defined. + pub fn attestation_private_key( + &self, + ) -> Result, Ctap2StatusCode> { + match self.store.find(key::ATTESTATION_PRIVATE_KEY)? { + None => Ok(None), + Some(key) if key.len() == key_material::ATTESTATION_PRIVATE_KEY_LENGTH => { + Ok(Some(*array_ref![ + key, + 0, + key_material::ATTESTATION_PRIVATE_KEY_LENGTH + ])) + } + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Sets the attestation private key. + /// + /// If it is already defined, it is overwritten. pub fn set_attestation_private_key( &mut self, - attestation_private_key: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH], + attestation_private_key: &[u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], ) -> Result<(), Ctap2StatusCode> { - let entry = StoreEntry { - tag: ATTESTATION_PRIVATE_KEY, - data: attestation_private_key, - sensitive: false, - }; - match self.store.find_one(&Key::AttestationPrivateKey) { - None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, + match self.store.find(key::ATTESTATION_PRIVATE_KEY)? { + None => Ok(self + .store + .insert(key::ATTESTATION_PRIVATE_KEY, attestation_private_key)?), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } - Ok(()) } + /// Returns the attestation certificate if defined. pub fn attestation_certificate(&self) -> Result>, Ctap2StatusCode> { - let data = match self.store.find_one(&Key::AttestationCertificate) { - None => return Ok(None), - Some((_, entry)) => entry.data, - }; - Ok(Some(data.to_vec())) + Ok(self.store.find(key::ATTESTATION_CERTIFICATE)?) } + /// Sets the attestation certificate. + /// + /// If it is already defined, it is overwritten. pub fn set_attestation_certificate( &mut self, attestation_certificate: &[u8], ) -> Result<(), Ctap2StatusCode> { - let entry = StoreEntry { - tag: ATTESTATION_CERTIFICATE, - data: attestation_certificate, - sensitive: false, - }; - match self.store.find_one(&Key::AttestationCertificate) { - None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, + match self.store.find(key::ATTESTATION_CERTIFICATE)? { + None => Ok(self + .store + .insert(key::ATTESTATION_CERTIFICATE, attestation_certificate)?), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } - Ok(()) } - pub fn aaguid(&self) -> Result<[u8; AAGUID_LENGTH], Ctap2StatusCode> { - let (_, entry) = self + /// Returns the AAGUID. + pub fn aaguid(&self) -> Result<[u8; key_material::AAGUID_LENGTH], Ctap2StatusCode> { + let aaguid = self .store - .find_one(&Key::Aaguid) + .find(key::AAGUID)? .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; - let data = entry.data; - if data.len() != AAGUID_LENGTH { + if aaguid.len() != key_material::AAGUID_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(*array_ref![data, 0, AAGUID_LENGTH]) + Ok(*array_ref![aaguid, 0, key_material::AAGUID_LENGTH]) } - pub fn set_aaguid(&mut self, aaguid: &[u8; AAGUID_LENGTH]) -> Result<(), Ctap2StatusCode> { - let entry = StoreEntry { - tag: AAGUID, - data: aaguid, - sensitive: false, - }; - match self.store.find_one(&Key::Aaguid) { - None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, - } - Ok(()) + /// Sets the AAGUID. + /// + /// If it is already defined, it is overwritten. + pub fn set_aaguid( + &mut self, + aaguid: &[u8; key_material::AAGUID_LENGTH], + ) -> Result<(), Ctap2StatusCode> { + Ok(self.store.insert(key::AAGUID, aaguid)?) } + /// Resets the store as for a CTAP reset. + /// + /// In particular persistent entries are not reset. pub fn reset(&mut self, rng: &mut impl Rng256) -> Result<(), Ctap2StatusCode> { - loop { - let index = { - let mut iter = self.store.iter().filter(|(_, entry)| should_reset(entry)); - match iter.next() { - None => break, - Some((index, _)) => index, - } - }; - self.store.delete(index)?; - } - self.init(rng); + self.store.clear(key::NUM_PERSISTENT_KEYS)?; + self.init(rng)?; Ok(()) } -} -impl From for Ctap2StatusCode { - fn from(error: StoreError) -> Ctap2StatusCode { - match error { - StoreError::StoreFull => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, - StoreError::InvalidTag => unreachable!(), - StoreError::InvalidPrecondition => unreachable!(), + /// Returns whether the PIN needs to be changed before its next usage. + pub fn has_force_pin_change(&self) -> Result { + match self.store.find(key::FORCE_PIN_CHANGE)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks the PIN as outdated with respect to the new PIN policy. + pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { + Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) + } + + /// Returns whether enterprise attestation is enabled. + pub fn enterprise_attestation(&self) -> Result { + match self.store.find(key::ENTERPRISE_ATTESTATION)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks enterprise attestation as enabled. + pub fn enable_enterprise_attestation(&mut self) -> Result<(), Ctap2StatusCode> { + if !self.enterprise_attestation()? { + self.store.insert(key::ENTERPRISE_ATTESTATION, &[])?; + } + Ok(()) + } + + /// Returns whether alwaysUv is enabled. + pub fn has_always_uv(&self) -> Result { + if ENFORCE_ALWAYS_UV { + return Ok(true); + } + match self.store.find(key::ALWAYS_UV)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Enables alwaysUv, when disabled, and vice versa. + pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { + if ENFORCE_ALWAYS_UV { + return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); + } + if self.has_always_uv()? { + Ok(self.store.remove(key::ALWAYS_UV)?) + } else { + Ok(self.store.insert(key::ALWAYS_UV, &[])?) } } } -fn should_reset(entry: &StoreEntry<'_>) -> bool { - match entry.tag { - ATTESTATION_PRIVATE_KEY | ATTESTATION_CERTIFICATE | AAGUID => false, - _ => true, +impl From for Ctap2StatusCode { + fn from(error: persistent_store::StoreError) -> Ctap2StatusCode { + use persistent_store::StoreError; + match error { + // This error is expected. The store is full. + StoreError::NoCapacity => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, + // This error is expected. The flash is out of life. + StoreError::NoLifetime => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, + // This error is expected if we don't satisfy the store preconditions. For example we + // try to store a credential which is too long. + StoreError::InvalidArgument => Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR, + // This error is not expected. The storage has been tempered with. We could erase the + // storage. + StoreError::InvalidStorage => Ctap2StatusCode::CTAP2_ERR_VENDOR_HARDWARE_FAILURE, + // This error is not expected. The kernel is failing our syscalls. + StoreError::StorageError => Ctap2StatusCode::CTAP1_ERR_OTHER, + } } } +/// Iterator for credentials. +pub struct IterCredentials<'a> { + /// The store being iterated. + store: &'a persistent_store::Store, + + /// The store iterator. + iter: persistent_store::StoreIter<'a>, + + /// The iteration result. + /// + /// It starts as success and gets written at most once with an error if something fails. The + /// iteration stops as soon as an error is encountered. + result: &'a mut Result<(), Ctap2StatusCode>, +} + +impl<'a> IterCredentials<'a> { + /// Creates a credential iterator. + fn new( + store: &'a persistent_store::Store, + result: &'a mut Result<(), Ctap2StatusCode>, + ) -> Result, Ctap2StatusCode> { + let iter = store.iter()?; + Ok(IterCredentials { + store, + iter, + result, + }) + } + + /// Marks the iteration as failed if the content is absent. + /// + /// For convenience, the function takes and returns ownership instead of taking a shared + /// reference and returning nothing. This permits to use it in both expressions and statements + /// instead of statements only. + fn unwrap(&mut self, x: Option) -> Option { + if x.is_none() { + *self.result = Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + x + } +} + +impl<'a> Iterator for IterCredentials<'a> { + type Item = (usize, PublicKeyCredentialSource); + + fn next(&mut self) -> Option<(usize, PublicKeyCredentialSource)> { + if self.result.is_err() { + return None; + } + while let Some(next) = self.iter.next() { + let handle = self.unwrap(next.ok())?; + let key = handle.get_key(); + if !key::CREDENTIALS.contains(&key) { + continue; + } + let value = self.unwrap(handle.get_value(&self.store).ok())?; + let credential = self.unwrap(deserialize_credential(&value))?; + return Some((key, credential)); + } + None + } +} + +/// Deserializes a credential from storage representation. fn deserialize_credential(data: &[u8]) -> Option { let cbor = cbor::read(data).ok()?; cbor.try_into().ok() } +/// Serializes a credential to storage representation. fn serialize_credential(credential: PublicKeyCredentialSource) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(credential.into(), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } -#[cfg(feature = "with_ctap2_1")] -fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { +/// Deserializes a list of RP IDs from storage representation. +fn deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; extract_array(cbor) .ok()? @@ -659,13 +745,13 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { .ok() } -#[cfg(feature = "with_ctap2_1")] -fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { +/// Serializes a list of RP IDs to storage representation. +fn serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(cbor_array_vec!(rp_ids), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } @@ -687,34 +773,16 @@ mod test { private_key, rp_id: String::from(rp_id), user_handle, - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, } } - #[test] - fn format_overhead() { - // nRF52840 NVMC - const WORD_SIZE: usize = 4; - const PAGE_SIZE: usize = 0x1000; - const NUM_PAGES: usize = 100; - let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); - let options = embedded_flash::BufferOptions { - word_size: WORD_SIZE, - page_size: PAGE_SIZE, - max_word_writes: 2, - max_page_erases: 10000, - strict_write: true, - }; - let storage = Storage::new(store, options); - let store = embedded_flash::Store::new(storage, Config).unwrap(); - // We can replace 3 bytes with minimal overhead. - assert_eq!(store.replace_len(false, 0), 2 * WORD_SIZE); - assert_eq!(store.replace_len(false, 3), 3 * WORD_SIZE); - assert_eq!(store.replace_len(false, 4), 3 * WORD_SIZE); - } - #[test] fn test_store() { let mut rng = ThreadRng256 {}; @@ -726,24 +794,96 @@ mod test { } #[test] - #[allow(clippy::assertions_on_constants)] + fn test_delete_credential() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!(persistent_store.count_credentials().unwrap(), 0); + + let mut credential_ids = vec![]; + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { + let user_handle = (i as u32).to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); + credential_ids.push(credential_source.credential_id.clone()); + assert!(persistent_store.store_credential(credential_source).is_ok()); + assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); + } + let mut count = persistent_store.count_credentials().unwrap(); + for credential_id in credential_ids { + assert!(persistent_store.delete_credential(&credential_id).is_ok()); + count -= 1; + assert_eq!(persistent_store.count_credentials().unwrap(), count); + } + } + + #[test] + fn test_update_credential() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let user = PublicKeyCredentialUserEntity { + // User ID is ignored. + user_id: vec![0x00], + user_name: Some("name".to_string()), + user_display_name: Some("display_name".to_string()), + user_icon: Some("icon".to_string()), + }; + assert_eq!( + persistent_store.update_credential(&[0x1D], user.clone()), + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + ); + + let credential_source = create_credential_source(&mut rng, "example.com", vec![0x1D]); + let credential_id = credential_source.credential_id.clone(); + assert!(persistent_store.store_credential(credential_source).is_ok()); + let stored_credential = persistent_store + .find_credential("example.com", &credential_id, false) + .unwrap() + .unwrap(); + assert_eq!(stored_credential.user_name, None); + assert_eq!(stored_credential.user_display_name, None); + assert_eq!(stored_credential.user_icon, None); + assert!(persistent_store + .update_credential(&credential_id, user.clone()) + .is_ok()); + let stored_credential = persistent_store + .find_credential("example.com", &credential_id, false) + .unwrap() + .unwrap(); + assert_eq!(stored_credential.user_name, user.user_name); + assert_eq!(stored_credential.user_display_name, user.user_display_name); + assert_eq!(stored_credential.user_icon, user.user_icon); + } + + #[test] + fn test_credential_order() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let credential_source = create_credential_source(&mut rng, "example.com", vec![]); + let current_latest_creation = credential_source.creation_order; + assert!(persistent_store.store_credential(credential_source).is_ok()); + let mut credential_source = create_credential_source(&mut rng, "example.com", vec![]); + credential_source.creation_order = persistent_store.new_creation_order().unwrap(); + assert!(credential_source.creation_order > current_latest_creation); + let current_latest_creation = credential_source.creation_order; + assert!(persistent_store.store_credential(credential_source).is_ok()); + assert!(persistent_store.new_creation_order().unwrap() > current_latest_creation); + } + + #[test] fn test_fill_store() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); - // To make this test work for bigger storages, implement better int -> Vec conversion. - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { - let credential_source = - create_credential_source(&mut rng, "example.com", vec![i as u8]); + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { + let user_handle = (i as u32).to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } let credential_source = create_credential_source( &mut rng, "example.com", - vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], + vec![MAX_SUPPORTED_RESIDENT_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), @@ -751,12 +891,11 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } #[test] - #[allow(clippy::assertions_on_constants)] fn test_overwrite() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -764,7 +903,8 @@ mod test { // These should have different IDs. let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x00]); - let expected_credential = credential_source1.clone(); + let credential_id0 = credential_source0.credential_id.clone(); + let credential_id1 = credential_source1.credential_id.clone(); assert!(persistent_store .store_credential(credential_source0) @@ -773,25 +913,26 @@ mod test { .store_credential(credential_source1) .is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), 1); - assert_eq!( - &persistent_store - .filter_credential("example.com", false) - .unwrap(), - &[expected_credential] - ); + assert!(persistent_store + .find_credential("example.com", &credential_id0, false) + .unwrap() + .is_none()); + assert!(persistent_store + .find_credential("example.com", &credential_id1, false) + .unwrap() + .is_some()); - // To make this test work for bigger storages, implement better int -> Vec conversion. - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { - let credential_source = - create_credential_source(&mut rng, "example.com", vec![i as u8]); + let mut persistent_store = PersistentStore::new(&mut rng); + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { + let user_handle = (i as u32).to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } let credential_source = create_credential_source( &mut rng, "example.com", - vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], + vec![MAX_SUPPORTED_RESIDENT_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), @@ -799,67 +940,26 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } #[test] - fn test_filter() { + fn test_get_credential() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - assert_eq!(persistent_store.count_credentials().unwrap(), 0); let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]); let credential_source2 = create_credential_source(&mut rng, "another.example.com", vec![0x02]); - let id0 = credential_source0.credential_id.clone(); - let id1 = credential_source1.credential_id.clone(); - assert!(persistent_store - .store_credential(credential_source0) - .is_ok()); - assert!(persistent_store - .store_credential(credential_source1) - .is_ok()); - assert!(persistent_store - .store_credential(credential_source2) - .is_ok()); - - let filtered_credentials = persistent_store - .filter_credential("example.com", false) - .unwrap(); - assert_eq!(filtered_credentials.len(), 2); - assert!( - (filtered_credentials[0].credential_id == id0 - && filtered_credentials[1].credential_id == id1) - || (filtered_credentials[1].credential_id == id0 - && filtered_credentials[0].credential_id == id1) - ); - } - - #[test] - fn test_filter_with_cred_protect() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - assert_eq!(persistent_store.count_credentials().unwrap(), 0); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let credential = PublicKeyCredentialSource { - key_type: PublicKeyCredentialType::PublicKey, - credential_id: rng.gen_uniform_u8x32().to_vec(), - private_key, - rp_id: String::from("example.com"), - user_handle: vec![0x00], - other_ui: None, - cred_random: None, - cred_protect_policy: Some( - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - ), - }; - assert!(persistent_store.store_credential(credential).is_ok()); - - let no_credential = persistent_store - .filter_credential("example.com", true) - .unwrap(); - assert_eq!(no_credential, vec![]); + let credential_sources = vec![credential_source0, credential_source1, credential_source2]; + for credential_source in credential_sources.into_iter() { + let cred_id = credential_source.credential_id.clone(); + assert!(persistent_store.store_credential(credential_source).is_ok()); + let (key, _) = persistent_store.find_credential_item(&cred_id).unwrap(); + let cred = persistent_store.get_credential(key).unwrap(); + assert_eq!(&cred_id, &cred.credential_id); + } } #[test] @@ -891,9 +991,13 @@ mod test { private_key: key0, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -910,9 +1014,13 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, - cred_random: None, + user_display_name: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -927,7 +1035,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // Master keys stay the same between resets. + // Master keys stay the same within the same CTAP reset cycle. let master_keys_1 = persistent_store.master_keys().unwrap(); let master_keys_2 = persistent_store.master_keys().unwrap(); assert_eq!(master_keys_2.encryption, master_keys_1.encryption); @@ -944,28 +1052,60 @@ mod test { } #[test] - fn test_pin_hash() { + fn test_cred_random_secret() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // CredRandom secrets stay the same within the same CTAP reset cycle. + let cred_random_with_uv_1 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_1 = persistent_store.cred_random_secret(false).unwrap(); + let cred_random_with_uv_2 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_2 = persistent_store.cred_random_secret(false).unwrap(); + assert_eq!(cred_random_with_uv_1, cred_random_with_uv_2); + assert_eq!(cred_random_without_uv_1, cred_random_without_uv_2); + + // CredRandom secrets change after reset. This test may fail if the random generator produces the + // same keys. + persistent_store.reset(&mut rng).unwrap(); + let cred_random_with_uv_3 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_3 = persistent_store.cred_random_secret(false).unwrap(); + assert!(cred_random_with_uv_1 != cred_random_with_uv_3); + assert!(cred_random_without_uv_1 != cred_random_without_uv_3); + } + + #[test] + fn test_pin_hash_and_length() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // Pin hash is initially not set. assert!(persistent_store.pin_hash().unwrap().is_none()); + assert!(persistent_store.pin_code_point_length().unwrap().is_none()); - // Setting the pin hash sets the pin hash. + // Setting the pin sets the pin hash. let random_data = rng.gen_uniform_u8x32(); assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH); let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH); let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH); - persistent_store.set_pin_hash(&pin_hash_1).unwrap(); + let pin_length_1 = 4; + let pin_length_2 = 63; + persistent_store.set_pin(&pin_hash_1, pin_length_1).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); - assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); - persistent_store.set_pin_hash(&pin_hash_2).unwrap(); - assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); + assert_eq!( + persistent_store.pin_code_point_length().unwrap(), + Some(pin_length_1) + ); + persistent_store.set_pin(&pin_hash_2, pin_length_2).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); + assert_eq!( + persistent_store.pin_code_point_length().unwrap(), + Some(pin_length_2) + ); // Resetting the storage resets the pin hash. persistent_store.reset(&mut rng).unwrap(); assert!(persistent_store.pin_hash().unwrap().is_none()); + assert!(persistent_store.pin_code_point_length().unwrap().is_none()); } #[test] @@ -974,21 +1114,21 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); // The pin retries is initially at the maximum. - assert_eq!(persistent_store.pin_retries().unwrap(), MAX_PIN_RETRIES); + assert_eq!(persistent_store.pin_retries(), Ok(MAX_PIN_RETRIES)); // Decrementing the pin retries decrements the pin retries. for pin_retries in (0..MAX_PIN_RETRIES).rev() { persistent_store.decr_pin_retries().unwrap(); - assert_eq!(persistent_store.pin_retries().unwrap(), pin_retries); + assert_eq!(persistent_store.pin_retries(), Ok(pin_retries)); } // Decrementing the pin retries after zero does not modify the pin retries. persistent_store.decr_pin_retries().unwrap(); - assert_eq!(persistent_store.pin_retries().unwrap(), 0); + assert_eq!(persistent_store.pin_retries(), Ok(0)); // Resetting the pin retries resets the pin retries. persistent_store.reset_pin_retries().unwrap(); - assert_eq!(persistent_store.pin_retries().unwrap(), MAX_PIN_RETRIES); + assert_eq!(persistent_store.pin_retries(), Ok(MAX_PIN_RETRIES)); } #[test] @@ -1006,29 +1146,30 @@ mod test { .unwrap() .is_none()); - // Make sure the persistent keys are initialized. + // Make sure the persistent keys are initialized to dummy values. + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; persistent_store - .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .set_attestation_private_key(&dummy_key) .unwrap(); persistent_store - .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .set_attestation_certificate(&dummy_cert) .unwrap(); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); // The persistent keys stay initialized and preserve their value after a reset. persistent_store.reset(&mut rng).unwrap(); assert_eq!( - persistent_store.attestation_private_key().unwrap().unwrap(), - key_material::ATTESTATION_PRIVATE_KEY + &persistent_store.attestation_private_key().unwrap().unwrap(), + &dummy_key ); assert_eq!( persistent_store.attestation_certificate().unwrap().unwrap(), - key_material::ATTESTATION_CERTIFICATE + &dummy_cert ); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_min_pin_length() { let mut rng = ThreadRng256 {}; @@ -1051,7 +1192,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_min_pin_length_rp_ids() { let mut rng = ThreadRng256 {}; @@ -1059,22 +1199,165 @@ mod test { // The minimum PIN length RP IDs are initially at the default. assert_eq!( - persistent_store._min_pin_length_rp_ids().unwrap(), - _DEFAULT_MIN_PIN_LENGTH_RP_IDS + persistent_store.min_pin_length_rp_ids().unwrap(), + DEFAULT_MIN_PIN_LENGTH_RP_IDS ); // Changes by the setter are reflected by the getter. let mut rp_ids = vec![String::from("example.com")]; assert_eq!( - persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()), + persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } } - assert_eq!(persistent_store._min_pin_length_rp_ids().unwrap(), rp_ids); + assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids); + } + + #[test] + fn test_max_large_blob_array_size() { + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + + assert!( + MAX_LARGE_BLOB_ARRAY_SIZE + <= persistent_store.store.max_value_length() + * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start) + ); + } + + #[test] + fn test_commit_get_large_blob_array() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0x01, 0x02, 0x03]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); + assert_eq!(vec![0x01], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 1).unwrap(); + assert_eq!(vec![0x02], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(2, 1).unwrap(); + assert_eq!(vec![0x03], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(2, 2).unwrap(); + assert_eq!(vec![0x03], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(3, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + } + + #[test] + fn test_commit_get_large_blob_array_overwrite() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0x11; 5]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let large_blob_array = vec![0x22; 4]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 5).unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + + assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 20).unwrap(); + // Committing an empty array resets to the default blob of 17 byte. + assert_eq!(restored_large_blob_array.len(), 17); + } + + #[test] + fn test_commit_get_large_blob_array_no_commit() { + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + + let empty_blob_array = vec![ + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, + ]; + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 17).unwrap(); + assert_eq!(empty_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); + assert_eq!(vec![0x80], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(16, 1).unwrap(); + assert_eq!(vec![0x3C], restored_large_blob_array); + } + + #[test] + fn test_global_signature_counter() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let mut counter_value = 1; + assert_eq!( + persistent_store.global_signature_counter().unwrap(), + counter_value + ); + for increment in 1..10 { + assert!(persistent_store + .incr_global_signature_counter(increment) + .is_ok()); + counter_value += increment; + assert_eq!( + persistent_store.global_signature_counter().unwrap(), + counter_value + ); + } + } + + #[test] + fn test_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + assert!(persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.set_pin(&[0x88; 16], 8), Ok(())); + assert!(!persistent_store.has_force_pin_change().unwrap()); + } + + #[test] + fn test_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.enterprise_attestation().unwrap()); + assert_eq!(persistent_store.enable_enterprise_attestation(), Ok(())); + assert!(persistent_store.enterprise_attestation().unwrap()); + persistent_store.reset(&mut rng).unwrap(); + assert!(!persistent_store.enterprise_attestation().unwrap()); + } + + #[test] + fn test_always_uv() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + if ENFORCE_ALWAYS_UV { + assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!( + persistent_store.toggle_always_uv(), + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + } else { + assert!(!persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(!persistent_store.has_always_uv().unwrap()); + } } #[test] @@ -1087,21 +1370,24 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, - cred_random: None, - cred_protect_policy: None, + user_display_name: Some(String::from("Display Name")), + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + creation_order: 0, + user_name: Some(String::from("name")), + user_icon: Some(String::from("icon")), + cred_blob: Some(vec![0xCB]), + large_blob_key: Some(vec![0x1B]), }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); assert_eq!(credential, reconstructed); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_serialize_deserialize_min_pin_length_rp_ids() { let rp_ids = vec![String::from("example.com")]; - let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); - let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap(); + let serialized = serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); + let reconstructed = deserialize_min_pin_length_rp_ids(&serialized).unwrap(); assert_eq!(rp_ids, reconstructed); } } diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs new file mode 100644 index 0000000..38a2a8b --- /dev/null +++ b/src/ctap/storage/key.rs @@ -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 = $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 = $range; + #[cfg(test)] + const ALL_KEYS: &[core::ops::Range] = &[$(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); + } + } +} diff --git a/src/ctap/timed_permission.rs b/src/ctap/timed_permission.rs index fcc0ada..868e9da 100644 --- a/src/ctap/timed_permission.rs +++ b/src/ctap/timed_permission.rs @@ -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. diff --git a/src/ctap/token_state.rs b/src/ctap/token_state.rs new file mode 100644 index 0000000..fea0f3c --- /dev/null +++ b/src/ctap/token_state.rs @@ -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 = 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, + 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) { + 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 = 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()); + } +} diff --git a/src/embedded_flash/buffer.rs b/src/embedded_flash/buffer.rs deleted file mode 100644 index 0e7171a..0000000 --- a/src/embedded_flash/buffer.rs +++ /dev/null @@ -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, 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, 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[..]); - } -} diff --git a/src/embedded_flash/mod.rs b/src/embedded_flash/mod.rs index 05407c0..e307a55 100644 --- a/src/embedded_flash/mod.rs +++ b/src/embedded_flash/mod.rs @@ -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}; diff --git a/src/embedded_flash/storage.rs b/src/embedded_flash/storage.rs deleted file mode 100644 index fe87ac4..0000000 --- a/src/embedded_flash/storage.rs +++ /dev/null @@ -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 = Result; - -/// 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> { - if self.is_valid(length, storage) { - let start = self.page * storage.page_size() + self.byte; - Ok(start..start + length) - } else { - Err(StorageError::OutOfBounds) - } - } -} diff --git a/src/embedded_flash/store/bitfield.rs b/src/embedded_flash/store/bitfield.rs deleted file mode 100644 index 797c78b..0000000 --- a/src/embedded_flash/store/bitfield.rs +++ /dev/null @@ -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::()); - 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::() || 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); -} diff --git a/src/embedded_flash/store/format.rs b/src/embedded_flash/store/format.rs deleted file mode 100644 index 03787cf..0000000 --- a/src/embedded_flash/store/format.rs +++ /dev/null @@ -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(storage: &S, config: &C) -> Option { - 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, user_entry: StoreEntry) -> Vec { - 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 { - 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); -} diff --git a/src/embedded_flash/store/mod.rs b/src/embedded_flash/store/mod.rs deleted file mode 100644 index 170fd87..0000000 --- a/src/embedded_flash/store/mod.rs +++ /dev/null @@ -1,1182 +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. - -//! Provides a multi-purpose data-structure. -//! -//! # Description -//! -//! The `Store` data-structure permits to iterate, find, insert, delete, and replace entries in a -//! multi-set. The mutable operations (insert, delete, and replace) are atomic, in the sense that if -//! power is lost during the operation, then the operation might either succeed or fail but the -//! store remains in a coherent state. The data-structure is flash-efficient, in the sense that it -//! tries to minimize the number of times a page is erased. -//! -//! An _entry_ is made of a _tag_, which is a number, and a _data_, which is a slice of bytes. The -//! tag is stored efficiently by using unassigned bits of the entry header and footer. For example, -//! it can be used to decide how to deserialize the data. It is not necessary to use tags since a -//! prefix of the data could be used to decide how to deserialize the rest. -//! -//! Entries can also be associated to a set of _keys_. The find operation permits to retrieve all -//! entries associated to a given key. The same key can be associated to multiple entries and the -//! same entry can be associated to multiple keys. -//! -//! # Storage -//! -//! The data-structure is parametric over its storage which must implement the `Storage` trait. -//! There are currently 2 implementations of this trait: -//! - `SyscallStorage` using the `embedded_flash` syscall API for production builds. -//! - `BufferStorage` using a heap-allocated buffer for testing. -//! -//! # Configuration -//! -//! The data-structure can be configured with the `StoreConfig` trait. By implementing this trait, -//! the number of possible tags and the association between keys and entries are defined. -//! -//! # Properties -//! -//! The data-structure provides the following properties: -//! - When an operation returns success, then the represented multi-set is updated accordingly. For -//! example, an inserted entry can be found without alteration until replaced or deleted. -//! - When an operation returns an error, the resulting multi-set state is described in the error -//! documentation. -//! - When power is lost before an operation returns, the operation will either succeed or be -//! rolled-back on the next initialization. So the multi-set would be either left unchanged or -//! updated accordingly. -//! -//! Those properties rely on the following assumptions: -//! - Writing a word to flash is atomic. When power is lost, the word is either fully written or not -//! written at all. -//! - Reading a word from flash is deterministic. When power is lost while writing or erasing a word -//! (erasing a page containing that word), reading that word repeatedly returns the same result -//! (until it is written 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. -//! -//! The properties may still hold outside those assumptions but with weaker probabilities as the -//! usage diverges from the assumptions. -//! -//! # Implementation -//! -//! The store is a page-aligned sequence of bits. It matches the following grammar: -//! -//! ```text -//! Store := Page* -//! Page := PageHeader (Entry | InternalEntry)* Padding(page) -//! PageHeader := // must fit in one word -//! initialized:1 -//! erase_count:erase_bits -//! compacting:1 -//! new_page:page_bits -//! Padding(word) -//! Entry := Header Data Footer -//! // Let X be the byte (word-aligned for sensitive queries) following `length` in `Info`. -//! Header := Info[..X] // must fit in one word -//! Footer := Info[X..] // must fit in one word -//! Info := -//! present=0 -//! deleted:1 -//! internal=1 -//! replace:1 -//! sensitive:1 -//! length:byte_bits -//! tag:tag_bits -//! [ // present if `replace` is 0 -//! replace_page:page_bits -//! replace_byte:byte_bits -//! ] -//! [Padding(bit)] // until `complete` is the last bit of a different word than `present` -//! committed:1 -//! complete=0 -//! InternalEntry := -//! present=0 -//! deleted:1 -//! internal=0 -//! old_page:page_bits -//! saved_erase_count:erase_bits -//! Padding(word) -//! Padding(X) := 1* until X-aligned -//! ``` -//! -//! For bit flags, a value of 0 means true and a value of 1 means false. So when erased, bits are -//! false. They can be set to true by writing 0. -//! -//! The `Entry` rule is for user entries and the `InternalEntry` rule is for internal entries of the -//! store. Currently, there is only one kind of internal entry: an entry to erase the page being -//! compacted. -//! -//! The `Header` and `Footer` rules are computed from the `Info` rule. An entry could simply be the -//! concatenation of internal metadata and the user data. However, to optimize the size in flash, we -//! splice the user data in the middle of the metadata. The reason is that we can only write twice -//! the same word and for replace entries we need to write the deleted bit and the committed bit -//! independently. Also, this is important for the complete bit to be the last written bit (since -//! slices are written to flash from low to high addresses). Here is the representation of a -//! specific replace entry for a specific configuration: -//! -//! ```text -//! page_bits=6 -//! byte_bits=9 -//! tag_bits=5 -//! -//! byte.bit name -//! 0.0 present -//! 0.1 deleted -//! 0.2 internal -//! 0.3 replace -//! 0.4 sensitive -//! 0.5 length (9 bits) -//! 1.6 tag (least significant 2 bits out of 5) -//! (the header ends at the first byte boundary after `length`) -//! 2.0 (2 bytes in this example) -//! (the footer starts immediately after the user data) -//! 4.0 tag (most significant 3 bits out of 5) -//! 4.3 replace_page (6 bits) -//! 5.1 replace_byte (9 bits) -//! 6.2 padding (make sure the 2 properties below hold) -//! 7.6 committed -//! 7.7 complete (on a different word than `present`) -//! 8.0 (word-aligned) -//! ``` -//! -//! The store should always contain at least one blank page, so that it is always possible to -//! compact. - -// TODO(cretin): We don't need inner padding for insert entries. The store format can be: -// InsertEntry | ReplaceEntry | InternalEntry (maybe rename to EraseEntry) -// InsertEntry padding is until `complete` is the last bit of a word. -// ReplaceEntry padding is until `complete` is the last bit of a different word than `present`. -// TODO(cretin): Add checksum (may play the same role as the completed bit) and recovery strategy? -// TODO(cretin): Add corruption (deterministic but undetermined reads) to fuzzing. -// TODO(cretin): Add more complex transactions? (this does not seem necessary yet) -// TODO(cretin): Add possibility to shred an entry (force compact page after delete)? - -mod bitfield; -mod format; - -use self::format::{Format, IsReplace}; -use super::{Index, Storage}; -#[cfg(any(test, feature = "ram_storage"))] -use crate::embedded_flash::BufferStorage; -#[cfg(any(test, feature = "ram_storage"))] -use alloc::boxed::Box; -use alloc::collections::BTreeMap; -use alloc::vec; -use alloc::vec::Vec; - -/// Configures a store. -pub trait StoreConfig { - /// How entries are keyed. - /// - /// To disable keys, this may be defined to `()` or even better a custom empty enum. - type Key: Ord; - - /// Number of entry tags. - /// - /// All tags must be smaller than this value. - /// - /// To disable tags, this function should return `1`. The only valid tag would then be `0`. - fn num_tags(&self) -> usize; - - /// Specifies the set of keys of an entry. - /// - /// If keys are not used, this function can immediately return. Otherwise, it should call - /// `associate_key` for each key that should be associated to `entry`. - fn keys(&self, entry: StoreEntry, associate_key: impl FnMut(Self::Key)); -} - -/// Errors returned by store operations. -#[derive(Debug, PartialEq, Eq)] -pub enum StoreError { - /// The operation could not proceed because the store is full. - StoreFull, - - /// The operation could not proceed because the provided tag is invalid. - InvalidTag, - - /// The operation could not proceed because the preconditions do not hold. - InvalidPrecondition, -} - -/// The position of an entry in the store. -#[cfg_attr(feature = "std", derive(Debug))] -#[derive(Copy, Clone)] -pub struct StoreIndex { - /// The index of this entry in the storage. - index: Index, - - /// The generation at which this index is valid. - /// - /// See the documentation of the field with the same name in the `Store` struct. - generation: usize, -} - -/// A user entry. -#[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))] -#[derive(Copy, Clone)] -pub struct StoreEntry<'a> { - /// The tag of the entry. - /// - /// Must be smaller than the configured number of tags. - pub tag: usize, - - /// The data of the entry. - pub data: &'a [u8], - - /// Whether the data is sensitive. - /// - /// Sensitive data is overwritten with zeroes when the entry is deleted. - pub sensitive: bool, -} - -/// Implements a configurable multi-set on top of any storage. -pub struct Store { - storage: S, - config: C, - format: Format, - - /// The index of the blank page reserved for compaction. - blank_page: usize, - - /// Counts the number of compactions since the store creation. - /// - /// A `StoreIndex` is valid only if they originate from the same generation. This is checked by - /// operations that take a `StoreIndex` as argument. - generation: usize, -} - -impl Store { - /// Creates a new store. - /// - /// Initializes the storage if it is fresh (filled with `0xff`). Rolls-back or completes an - /// operation if the store was powered off in the middle of that operation. In other words, - /// operations are atomic. - /// - /// # Errors - /// - /// Returns `None` if `storage` and/or `config` are not supported. - pub fn new(storage: S, config: C) -> Option> { - let format = Format::new(&storage, &config)?; - let blank_page = format.num_pages; - let mut store = Store { - storage, - config, - format, - blank_page, - generation: 0, - }; - // Finish any ongoing page compaction. - store.recover_compact_page(); - // Finish or roll-back any other entry-level operations. - store.recover_entry_operations(); - // Initialize uninitialized pages. - store.initialize_storage(); - Some(store) - } - - /// Iterates over all entries in the store. - pub fn iter(&self) -> impl Iterator { - Iter::new(self).filter_map(move |(index, entry)| { - if self.format.is_alive(entry) { - Some(( - StoreIndex { - index, - generation: self.generation, - }, - StoreEntry { - tag: self.format.get_tag(entry), - data: self.format.get_data(entry), - sensitive: self.format.is_sensitive(entry), - }, - )) - } else { - None - } - }) - } - - /// Iterates over all entries matching a key in the store. - pub fn find_all<'a>( - &'a self, - key: &'a C::Key, - ) -> impl Iterator + 'a { - self.iter().filter(move |&(_, entry)| { - let mut has_match = false; - self.config.keys(entry, |k| has_match |= key == &k); - has_match - }) - } - - /// Returns the first entry matching a key in the store. - /// - /// This is a convenience function for when at most one entry should match the key. - /// - /// # Panics - /// - /// In debug mode, panics if more than one entry matches the key. - pub fn find_one<'a>(&'a self, key: &'a C::Key) -> Option<(StoreIndex, StoreEntry<'a>)> { - let mut iter = self.find_all(key); - let first = iter.next()?; - let has_only_one_element = iter.next().is_none(); - debug_assert!(has_only_one_element); - Some(first) - } - - /// Deletes an entry from the store. - pub fn delete(&mut self, index: StoreIndex) -> Result<(), StoreError> { - if self.generation != index.generation { - return Err(StoreError::InvalidPrecondition); - } - self.delete_index(index.index); - Ok(()) - } - - /// Replaces an entry with another with the same tag in the store. - /// - /// This operation (like others) is atomic. If it returns successfully, then the old entry is - /// deleted and the new is inserted. If it fails, the old entry is not deleted and the new entry - /// is not inserted. If power is lost during the operation, during next startup, the operation - /// is either rolled-back (like in case of failure) or completed (like in case of success). - /// - /// # Errors - /// - /// Returns: - /// - `StoreFull` if the new entry does not fit in the store. - /// - `InvalidTag` if the tag of the new entry is not smaller than the configured number of - /// tags. - pub fn replace(&mut self, old: StoreIndex, new: StoreEntry) -> Result<(), StoreError> { - if self.generation != old.generation { - return Err(StoreError::InvalidPrecondition); - } - self.format.validate_entry(new)?; - let mut old_index = old.index; - // Find a slot. - let entry_len = self.replace_len(new.sensitive, new.data.len()); - let index = self.find_slot_for_write(entry_len, Some(&mut old_index))?; - // Build a new entry replacing the old one. - let entry = self.format.build_entry(Some(old_index), new); - debug_assert_eq!(entry.len(), entry_len); - // Write the new entry. - self.write_entry(index, &entry); - // Commit the new entry, which both deletes the old entry and commits the new one. - self.commit_index(index); - Ok(()) - } - - /// Inserts an entry in the store. - /// - /// # Errors - /// - /// Returns: - /// - `StoreFull` if the new entry does not fit in the store. - /// - `InvalidTag` if the tag of the new entry is not smaller than the configured number of - /// tags. - pub fn insert(&mut self, entry: StoreEntry) -> Result<(), StoreError> { - self.format.validate_entry(entry)?; - // Build entry. - let entry = self.format.build_entry(None, entry); - // Find a slot. - let index = self.find_slot_for_write(entry.len(), None)?; - // Write entry. - self.write_entry(index, &entry); - Ok(()) - } - - /// Returns the byte cost of a replace operation. - /// - /// Computes the length in bytes that would be used in the storage if a replace operation is - /// executed provided the data of the new entry has `length` bytes and whether this data is - /// sensitive. - pub fn replace_len(&self, sensitive: bool, length: usize) -> usize { - self.format - .entry_size(IsReplace::Replace, sensitive, length) - } - - /// Returns the byte cost of an insert operation. - /// - /// Computes the length in bytes that would be used in the storage if an insert operation is - /// executed provided the data of the inserted entry has `length` bytes and whether this data is - /// sensitive. - #[allow(dead_code)] - pub fn insert_len(&self, sensitive: bool, length: usize) -> usize { - self.format.entry_size(IsReplace::Insert, sensitive, length) - } - - /// Returns the erase count of all pages. - /// - /// The value at index `page` of the result is the number of times page `page` was erased. This - /// number is an underestimate in case power was lost when this page was erased. - #[allow(dead_code)] - pub fn compaction_info(&self) -> Vec { - let mut info = Vec::with_capacity(self.format.num_pages); - for page in 0..self.format.num_pages { - let (page_header, _) = self.read_page_header(page); - let erase_count = self.format.get_erase_count(page_header); - info.push(erase_count); - } - info - } - - /// Completes any ongoing page compaction. - fn recover_compact_page(&mut self) { - for page in 0..self.format.num_pages { - let (page_header, _) = self.read_page_header(page); - if self.format.is_compacting(page_header) { - let new_page = self.format.get_new_page(page_header); - self.compact_page(page, new_page); - } - } - } - - /// Rolls-back or completes any ongoing operation. - fn recover_entry_operations(&mut self) { - for page in 0..self.format.num_pages { - let (page_header, mut index) = self.read_page_header(page); - if !self.format.is_initialized(page_header) { - // Skip uninitialized pages. - continue; - } - while index.byte < self.format.page_size { - let entry_index = index; - let entry = self.read_entry(index); - index.byte += entry.len(); - if !self.format.is_present(entry) { - // Reached the end of the page. - } else if self.format.is_deleted(entry) { - // Wipe sensitive data if needed. - self.wipe_sensitive_data(entry_index); - } else if self.format.is_internal(entry) { - // Finish page compaction. - self.erase_page(entry_index); - } else if !self.format.is_complete(entry) { - // Roll-back incomplete operations. - self.delete_index(entry_index); - } else if !self.format.is_committed(entry) { - // Finish complete but uncommitted operations. - self.commit_index(entry_index) - } - } - } - } - - /// Initializes uninitialized pages. - fn initialize_storage(&mut self) { - for page in 0..self.format.num_pages { - let (header, index) = self.read_page_header(page); - if self.format.is_initialized(header) { - // Update blank page. - let first_entry = self.read_entry(index); - if !self.format.is_present(first_entry) { - self.blank_page = page; - } - } else { - // We set the erase count to zero the very first time we initialize a page. - self.initialize_page(page, 0); - } - } - debug_assert!(self.blank_page != self.format.num_pages); - } - - /// Marks an entry as deleted. - /// - /// The provided index must point to the beginning of an entry. - fn delete_index(&mut self, index: Index) { - self.update_word(index, |format, word| format.set_deleted(word)); - self.wipe_sensitive_data(index); - } - - /// Wipes the data of a sensitive entry. - /// - /// If the entry at the provided index is sensitive, overwrites the data with zeroes. Otherwise, - /// does nothing. - fn wipe_sensitive_data(&mut self, mut index: Index) { - let entry = self.read_entry(index); - debug_assert!(self.format.is_present(entry)); - debug_assert!(self.format.is_deleted(entry)); - if self.format.is_internal(entry) || !self.format.is_sensitive(entry) { - // No need to wipe the data. - return; - } - let gap = self.format.entry_gap(entry); - let data = gap.slice(entry); - if data.iter().all(|&byte| byte == 0x00) { - // The data is already wiped. - return; - } - index.byte += gap.start; - self.storage - .write_slice(index, &vec![0; gap.length]) - .unwrap(); - } - - /// Finds a page with enough free space. - /// - /// Returns an index to the free space of a page which can hold an entry of `length` bytes. If - /// necessary, pages may be compacted to free space. In that case, if provided, the `old_index` - /// is updated according to compaction. - fn find_slot_for_write( - &mut self, - length: usize, - mut old_index: Option<&mut Index>, - ) -> Result { - loop { - if let Some(index) = self.choose_slot_for_write(length) { - return Ok(index); - } - match self.choose_page_for_compact() { - None => return Err(StoreError::StoreFull), - Some(page) => { - let blank_page = self.blank_page; - // Compact the chosen page and update the old index to point to the entry in the - // new page if it happened to be in the old page. This is essentially a way to - // avoid index invalidation due to compaction. - let map = self.compact_page(page, blank_page); - if let Some(old_index) = &mut old_index { - map_index(page, blank_page, &map, old_index); - } - } - } - } - } - - /// Returns whether a page has enough free space. - /// - /// Returns an index to the free space of a page with smallest free space that may hold `length` - /// bytes. - fn choose_slot_for_write(&self, length: usize) -> Option { - Iter::new(self) - .filter(|(index, entry)| { - index.page != self.blank_page - && !self.format.is_present(entry) - && length <= entry.len() - }) - .min_by_key(|(_, entry)| entry.len()) - .map(|(index, _)| index) - } - - /// Returns the page that should be compacted. - fn choose_page_for_compact(&self) -> Option { - // TODO(cretin): This could be optimized by using some cost function depending on: - // - the erase count - // - the length of the free space - // - the length of the alive entries - // We want to minimize this cost. We could also take into account the length of the entry we - // want to write to bound the number of compaction before failing with StoreFull. - // - // We should also make sure that all pages (including if they have no deleted entries and no - // free space) are eventually compacted (ideally to a heavily used page) to benefit from the - // low erase count of those pages. - (0..self.format.num_pages) - .map(|page| (page, self.page_info(page))) - .filter(|&(page, ref info)| { - page != self.blank_page - && info.erase_count < self.format.max_page_erases - && info.deleted_length > self.format.internal_entry_size() - }) - .min_by(|(_, lhs_info), (_, rhs_info)| lhs_info.compare_for_compaction(rhs_info)) - .map(|(page, _)| page) - } - - fn page_info(&self, page: usize) -> PageInfo { - let (page_header, mut index) = self.read_page_header(page); - let mut info = PageInfo { - erase_count: self.format.get_erase_count(page_header), - deleted_length: 0, - free_length: 0, - }; - while index.byte < self.format.page_size { - let entry = self.read_entry(index); - index.byte += entry.len(); - if !self.format.is_present(entry) { - debug_assert_eq!(info.free_length, 0); - info.free_length = entry.len(); - } else if self.format.is_deleted(entry) { - info.deleted_length += entry.len(); - } - } - debug_assert_eq!(index.page, page); - info - } - - fn read_slice(&self, index: Index, length: usize) -> &[u8] { - self.storage.read_slice(index, length).unwrap() - } - - /// Reads an entry (with header and footer) at a given index. - /// - /// If no entry is present, returns the free space up to the end of the page. - fn read_entry(&self, index: Index) -> &[u8] { - let first_byte = self.read_slice(index, 1); - let max_length = self.format.page_size - index.byte; - let mut length = if !self.format.is_present(first_byte) { - max_length - } else if self.format.is_internal(first_byte) { - self.format.internal_entry_size() - } else { - // We don't know if the entry is sensitive or not, but it doesn't matter here. We just - // need to read the replace, sensitive, and length fields. - let header = self.read_slice(index, self.format.header_size(false)); - let replace = self.format.is_replace(header); - let sensitive = self.format.is_sensitive(header); - let length = self.format.get_length(header); - self.format.entry_size(replace, sensitive, length) - }; - // Truncate the length to fit the page. This can only happen in case of corruption or - // partial writes. - length = core::cmp::min(length, max_length); - self.read_slice(index, length) - } - - /// Reads a page header. - /// - /// Also returns the index after the page header. - fn read_page_header(&self, page: usize) -> (&[u8], Index) { - let mut index = Index { page, byte: 0 }; - let page_header = self.read_slice(index, self.format.page_header_size()); - index.byte += page_header.len(); - (page_header, index) - } - - /// Updates a word at a given index. - /// - /// The `update` function is called with the word at `index`. The input value is the current - /// value of the word. The output value is the value that will be written. It should only change - /// bits from 1 to 0. - fn update_word(&mut self, index: Index, update: impl FnOnce(&Format, &mut [u8])) { - let word_size = self.format.word_size; - let mut word = self.read_slice(index, word_size).to_vec(); - update(&self.format, &mut word); - self.storage.write_slice(index, &word).unwrap(); - } - - fn write_entry(&mut self, index: Index, entry: &[u8]) { - self.storage.write_slice(index, entry).unwrap(); - } - - /// Initializes a page by writing the page header. - /// - /// If the page is not erased, it is first erased. - fn initialize_page(&mut self, page: usize, erase_count: usize) { - let index = Index { page, byte: 0 }; - let page = self.read_slice(index, self.format.page_size); - if !page.iter().all(|&byte| byte == 0xff) { - self.storage.erase_page(index.page).unwrap(); - } - self.update_word(index, |format, header| { - format.set_initialized(header); - format.set_erase_count(header, erase_count); - }); - self.blank_page = index.page; - } - - /// Commits a replace entry. - /// - /// Deletes the old entry and commits the new entry. - fn commit_index(&mut self, mut index: Index) { - let entry = self.read_entry(index); - index.byte += entry.len(); - let word_size = self.format.word_size; - debug_assert!(entry.len() >= 2 * word_size); - match self.format.is_replace(entry) { - IsReplace::Replace => { - let delete_index = self.format.get_replace_index(entry); - self.delete_index(delete_index); - } - IsReplace::Insert => debug_assert!(false), - }; - index.byte -= word_size; - self.update_word(index, |format, word| format.set_committed(word)); - } - - /// Compacts a page to an other. - /// - /// Returns the mapping from the alive entries in the old page to their index in the new page. - fn compact_page(&mut self, old_page: usize, new_page: usize) -> BTreeMap { - // Write the old page as being compacted to the new page. - let mut erase_count = 0; - self.update_word( - Index { - page: old_page, - byte: 0, - }, - |format, header| { - erase_count = format.get_erase_count(header); - format.set_compacting(header); - format.set_new_page(header, new_page); - }, - ); - // Copy alive entries from the old page to the new page. - let page_header_size = self.format.page_header_size(); - let mut old_index = Index { - page: old_page, - byte: page_header_size, - }; - let mut new_index = Index { - page: new_page, - byte: page_header_size, - }; - let mut map = BTreeMap::new(); - while old_index.byte < self.format.page_size { - let old_entry = self.read_entry(old_index); - let old_entry_index = old_index.byte; - old_index.byte += old_entry.len(); - if !self.format.is_alive(old_entry) { - continue; - } - let previous_mapping = map.insert(old_entry_index, new_index.byte); - debug_assert!(previous_mapping.is_none()); - // We need to copy the old entry because it is in the storage and we are going to write - // to the storage. Rust cannot tell that both entries don't overlap. - let old_entry = old_entry.to_vec(); - self.write_entry(new_index, &old_entry); - new_index.byte += old_entry.len(); - } - // Save the old page index and erase count to the new page. - let erase_index = new_index; - let erase_entry = self.format.build_erase_entry(old_page, erase_count); - self.write_entry(new_index, &erase_entry); - // Erase the page. - self.erase_page(erase_index); - // Increase generation. - self.generation += 1; - map - } - - /// Commits an internal entry. - /// - /// The only kind of internal entry is to erase a page, which first erases the page, then - /// initializes it with the saved erase count, and finally deletes the internal entry. - fn erase_page(&mut self, erase_index: Index) { - let erase_entry = self.read_entry(erase_index); - debug_assert!(self.format.is_present(erase_entry)); - debug_assert!(!self.format.is_deleted(erase_entry)); - debug_assert!(self.format.is_internal(erase_entry)); - let old_page = self.format.get_old_page(erase_entry); - let erase_count = self.format.get_saved_erase_count(erase_entry) + 1; - // Erase the page. - self.storage.erase_page(old_page).unwrap(); - // Initialize the page. - self.initialize_page(old_page, erase_count); - // Delete the internal entry. - self.delete_index(erase_index); - } -} - -// Those functions are not meant for production. -#[cfg(any(test, feature = "ram_storage"))] -impl Store { - /// Takes a snapshot of the storage after a given amount of word operations. - pub fn arm_snapshot(&mut self, delay: usize) { - self.storage.arm_snapshot(delay); - } - - /// Unarms and returns the snapshot or the delay remaining. - pub fn get_snapshot(&mut self) -> Result, usize> { - self.storage.get_snapshot() - } - - /// Takes a snapshot of the storage. - pub fn take_snapshot(&self) -> Box<[u8]> { - self.storage.take_snapshot() - } - - /// Returns the storage. - pub fn get_storage(self) -> Box<[u8]> { - self.storage.get_storage() - } - - /// Erases and initializes a page with a given erase count. - pub fn set_erase_count(&mut self, page: usize, erase_count: usize) { - self.initialize_page(page, erase_count); - } - - /// Returns whether all deleted sensitive entries have been wiped. - pub fn deleted_entries_are_wiped(&self) -> bool { - for (_, entry) in Iter::new(self) { - if !self.format.is_present(entry) - || !self.format.is_deleted(entry) - || self.format.is_internal(entry) - || !self.format.is_sensitive(entry) - { - continue; - } - let gap = self.format.entry_gap(entry); - let data = gap.slice(entry); - if !data.iter().all(|&byte| byte == 0x00) { - return false; - } - } - true - } -} - -/// Maps an index from an old page to a new page if needed. -fn map_index(old_page: usize, new_page: usize, map: &BTreeMap, index: &mut Index) { - if index.page == old_page { - index.page = new_page; - index.byte = *map.get(&index.byte).unwrap(); - } -} - -/// Page information for compaction. -struct PageInfo { - /// How many times the page was erased. - erase_count: usize, - - /// Cumulative length of deleted entries (including header and footer). - deleted_length: usize, - - /// Length of the free space. - free_length: usize, -} - -impl PageInfo { - /// Returns whether a page should be compacted before another. - fn compare_for_compaction(&self, rhs: &PageInfo) -> core::cmp::Ordering { - self.erase_count - .cmp(&rhs.erase_count) - .then(rhs.deleted_length.cmp(&self.deleted_length)) - .then(self.free_length.cmp(&rhs.free_length)) - } -} - -/// Iterates over all entries (including free space) of a store. -struct Iter<'a, S: Storage, C: StoreConfig> { - store: &'a Store, - index: Index, -} - -impl<'a, S: Storage, C: StoreConfig> Iter<'a, S, C> { - fn new(store: &'a Store) -> Iter<'a, S, C> { - let index = Index { - page: 0, - byte: store.format.page_header_size(), - }; - Iter { store, index } - } -} - -impl<'a, S: Storage, C: StoreConfig> Iterator for Iter<'a, S, C> { - type Item = (Index, &'a [u8]); - - fn next(&mut self) -> Option<(Index, &'a [u8])> { - if self.index.byte == self.store.format.page_size { - self.index.page += 1; - self.index.byte = self.store.format.page_header_size(); - } - if self.index.page == self.store.format.num_pages { - return None; - } - let index = self.index; - let entry = self.store.read_entry(self.index); - self.index.byte += entry.len(); - Some((index, entry)) - } -} - -#[cfg(test)] -mod tests { - use super::super::{BufferOptions, BufferStorage}; - use super::*; - - struct Config; - - const WORD_SIZE: usize = 4; - const PAGE_SIZE: usize = 8 * WORD_SIZE; - const NUM_PAGES: usize = 3; - - impl StoreConfig for Config { - type Key = u8; - - fn num_tags(&self) -> usize { - 1 - } - - fn keys(&self, entry: StoreEntry, mut add: impl FnMut(u8)) { - assert_eq!(entry.tag, 0); - if !entry.data.is_empty() { - add(entry.data[0]); - } - } - } - - fn new_buffer(storage: Box<[u8]>) -> BufferStorage { - let options = BufferOptions { - word_size: WORD_SIZE, - page_size: PAGE_SIZE, - max_word_writes: 2, - max_page_erases: 2, - strict_write: true, - }; - BufferStorage::new(storage, options) - } - - fn new_store() -> Store { - let storage = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); - Store::new(new_buffer(storage), Config).unwrap() - } - - #[test] - fn insert_ok() { - let mut store = new_store(); - assert_eq!(store.iter().count(), 0); - let tag = 0; - let key = 1; - let data = &[key, 2]; - let entry = StoreEntry { - tag, - data, - sensitive: false, - }; - store.insert(entry).unwrap(); - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_one(&key).unwrap().1, entry); - } - - #[test] - fn insert_sensitive_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let data = &[key, 4]; - let entry = StoreEntry { - tag, - data, - sensitive: true, - }; - store.insert(entry).unwrap(); - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_one(&key).unwrap().1, entry); - } - - #[test] - fn delete_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let entry = StoreEntry { - tag, - data: &[key, 2], - sensitive: false, - }; - store.insert(entry).unwrap(); - assert_eq!(store.find_all(&key).count(), 1); - let (index, _) = store.find_one(&key).unwrap(); - store.delete(index).unwrap(); - assert_eq!(store.find_all(&key).count(), 0); - assert_eq!(store.iter().count(), 0); - } - - #[test] - fn delete_sensitive_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let entry = StoreEntry { - tag, - data: &[key, 2], - sensitive: true, - }; - store.insert(entry).unwrap(); - assert_eq!(store.find_all(&key).count(), 1); - let (index, _) = store.find_one(&key).unwrap(); - store.delete(index).unwrap(); - assert_eq!(store.find_all(&key).count(), 0); - assert_eq!(store.iter().count(), 0); - assert!(store.deleted_entries_are_wiped()); - } - - #[test] - fn insert_until_full() { - let mut store = new_store(); - let tag = 0; - let mut key = 0; - while store - .insert(StoreEntry { - tag, - data: &[key, 0], - sensitive: false, - }) - .is_ok() - { - key += 1; - } - assert!(key > 0); - } - - #[test] - fn compact_ok() { - let mut store = new_store(); - let tag = 0; - let mut key = 0; - while store - .insert(StoreEntry { - tag, - data: &[key, 0], - sensitive: false, - }) - .is_ok() - { - key += 1; - } - let (index, _) = store.find_one(&0).unwrap(); - store.delete(index).unwrap(); - store - .insert(StoreEntry { - tag: 0, - data: &[key, 0], - sensitive: false, - }) - .unwrap(); - for k in 1..=key { - assert_eq!(store.find_all(&k).count(), 1); - } - } - - #[test] - fn reboot_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let data = &[key, 2]; - let entry = StoreEntry { - tag, - data, - sensitive: false, - }; - store.insert(entry).unwrap(); - - // Reboot the store. - let store = store.get_storage(); - let store = Store::new(new_buffer(store), Config).unwrap(); - - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_one(&key).unwrap().1, entry); - } - - #[test] - fn replace_atomic() { - let tag = 0; - let key = 1; - let old_entry = StoreEntry { - tag, - data: &[key, 2, 3, 4, 5, 6], - sensitive: false, - }; - let new_entry = StoreEntry { - tag, - data: &[key, 7, 8, 9], - sensitive: false, - }; - let mut delay = 0; - loop { - let mut store = new_store(); - store.insert(old_entry).unwrap(); - store.arm_snapshot(delay); - let (index, _) = store.find_one(&key).unwrap(); - store.replace(index, new_entry).unwrap(); - let (complete, store) = match store.get_snapshot() { - Err(_) => (true, store.get_storage()), - Ok(store) => (false, store), - }; - let store = Store::new(new_buffer(store), Config).unwrap(); - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_all(&key).count(), 1); - let (_, cur_entry) = store.find_one(&key).unwrap(); - assert!((cur_entry == old_entry && !complete) || cur_entry == new_entry); - if complete { - break; - } - delay += 1; - } - } - - #[test] - fn compact_atomic() { - let tag = 0; - let mut delay = 0; - loop { - let mut store = new_store(); - let mut key = 0; - while store - .insert(StoreEntry { - tag, - data: &[key, 0], - sensitive: false, - }) - .is_ok() - { - key += 1; - } - let (index, _) = store.find_one(&0).unwrap(); - store.delete(index).unwrap(); - let (index, _) = store.find_one(&1).unwrap(); - store.arm_snapshot(delay); - store - .replace( - index, - StoreEntry { - tag, - data: &[1, 1], - sensitive: false, - }, - ) - .unwrap(); - let (complete, store) = match store.get_snapshot() { - Err(_) => (true, store.get_storage()), - Ok(store) => (false, store), - }; - let store = Store::new(new_buffer(store), Config).unwrap(); - assert_eq!(store.iter().count(), key as usize - 1); - for k in 2..key { - assert_eq!(store.find_all(&k).count(), 1); - assert_eq!( - store.find_one(&k).unwrap().1, - StoreEntry { - tag, - data: &[k, 0], - sensitive: false, - } - ); - } - assert_eq!(store.find_all(&1).count(), 1); - let (_, entry) = store.find_one(&1).unwrap(); - assert_eq!(entry.tag, tag); - assert!((entry.data == [1, 0] && !complete) || entry.data == [1, 1]); - if complete { - break; - } - delay += 1; - } - } - - #[test] - fn invalid_tag() { - let mut store = new_store(); - let entry = StoreEntry { - tag: 1, - data: &[], - sensitive: false, - }; - assert_eq!(store.insert(entry), Err(StoreError::InvalidTag)); - } - - #[test] - fn invalid_length() { - let mut store = new_store(); - let entry = StoreEntry { - tag: 0, - data: &[0; PAGE_SIZE], - sensitive: false, - }; - assert_eq!(store.insert(entry), Err(StoreError::StoreFull)); - } -} diff --git a/src/embedded_flash/syscall.rs b/src/embedded_flash/syscall.rs index 5eae561..e043772 100644 --- a/src/embedded_flash/syscall.rs +++ b/src/embedded_flash/syscall.rs @@ -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 { 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 { 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(()) } diff --git a/src/lib.rs b/src/lib.rs index d6260c7..a5c4cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,3 +18,6 @@ extern crate alloc; pub mod ctap; pub mod embedded_flash; + +#[macro_use] +extern crate arrayref; diff --git a/src/main.rs b/src/main.rs index 855325e..202a1ea 100644 --- a/src/main.rs +++ b/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::::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 { diff --git a/third_party/lang-items/Cargo.toml b/third_party/lang-items/Cargo.toml index 39ffbf0..5d109f3 100644 --- a/third_party/lang-items/Cargo.toml +++ b/third_party/lang-items/Cargo.toml @@ -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 = [] diff --git a/third_party/libtock-drivers/src/crp.rs b/third_party/libtock-drivers/src/crp.rs new file mode 100644 index 0000000..3b686ca --- /dev/null +++ b/third_party/libtock-drivers/src/crp.rs @@ -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 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 { + 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(()) +} diff --git a/third_party/libtock-drivers/src/lib.rs b/third_party/libtock-drivers/src/lib.rs index 8b8983c..f014996 100644 --- a/third_party/libtock-drivers/src/lib.rs +++ b/third_party/libtock-drivers/src/lib.rs @@ -2,6 +2,7 @@ pub mod buttons; pub mod console; +pub mod crp; pub mod led; #[cfg(feature = "with_nfc")] pub mod nfc; diff --git a/tools/configure.py b/tools/configure.py new file mode 100755 index 0000000..5fa2da1 --- /dev/null +++ b/tools/configure.py @@ -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())