├── .gitignore ├── LICENSE ├── README.md ├── attestation_key_pair.py ├── build.gradle ├── decode_bufinfo.py ├── docs ├── FAQ.md ├── certs.md ├── fido_certification.md ├── implementation.md ├── installation.md ├── requirements.md └── security_model.md ├── get_install_parameters.py ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── install_attestation_cert.py ├── mds.json ├── python_tests ├── __init__.py └── ctap │ ├── __init__.py │ ├── ctap_hid_device.py │ ├── ctap_test.py │ ├── test_auth_config.py │ ├── test_cred_blob.py │ ├── test_cred_mgmt.py │ ├── test_credprotect.py │ ├── test_ctap_attestation_mode_switch.py │ ├── test_ctap_attestation_mode_switch_with_fixed_key.py │ ├── test_ctap_basic_attestation.py │ ├── test_ctap_basics.py │ ├── test_ctap_locked_self_attestation.py │ ├── test_ctap_pins.py │ ├── test_ctap_resets.py │ ├── test_extended_apdus.py │ ├── test_hmac_secret.py │ ├── test_largeblobs.py │ ├── test_long_request_buffer.py │ ├── test_malformed_input.py │ ├── test_setminpin.py │ ├── test_u2f.py │ └── test_uvm.py ├── requirements.txt ├── settings.gradle └── src ├── main └── java │ └── us │ └── q3q │ └── fido2 │ ├── BufferManager.java │ ├── CannedCBOR.java │ ├── FIDO2Applet.java │ ├── FIDOConstants.java │ ├── P256Constants.java │ ├── PinRetryCounter.java │ ├── ResidentKeyData.java │ ├── SigOpCounter.java │ └── TransientStorage.java └── test └── java └── us └── q3q └── fido2 ├── AppletBasicTest.java └── VSim.java /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /build 4 | /venv 5 | __pycache__ 6 | /bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bryan Jacobs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FIDO2 CTAP2 Javacard Applet 2 | 3 | ## Overview 4 | 5 | This repository contains sources for a feature-rich, FIDO2 CTAP2.1 6 | compatible applet targeting the Javacard Classic system, version 3.0.4. In a 7 | nutshell, this lets you take a smartcard, install an app onto it, 8 | and have it work as a FIDO2 authenticator device with a variety of 9 | features. You can generate and use OpenSSH `ecdsa-sk` type keys, including 10 | ones you carry with you on the key (`-O resident`). You can securely unlock 11 | a LUKS encrypted disk with `systemd-cryptenroll`. You can log in to a Linux 12 | system locally with [pam-u2f](https://github.com/Yubico/pam-u2f). 13 | 14 | 100% of the FIDO2 CTAP2.1 spec is covered, with the exception of features 15 | that aren't physically on an ordinary smartcard, such as biometrics or 16 | other on-board user verification. The implementation in the default configuration 17 | passes the official FIDO certification test suite version 1.7.17 in 18 | "CTAP2.1 full feature profile" mode. 19 | 20 | In order to run this outside a simulator, you will need 21 | [a compatible smartcard](docs/requirements.md). Some smartcards which 22 | describe themselves as running Javacard 3.0.1 also work - see the 23 | detailed requirements. 24 | 25 | You might be interested in [reading about the security model](docs/security_model.md). 26 | 27 | ## Environment Setup and Building the application 28 | 1. **Download JavacardKit**: Obtain a copy of [JavacardKit version 3.0.4](https://www.oracle.com/java/technologies/javacard-sdk-downloads.html) (or jckit_303 if you prefer). 29 | 2. **Set Environment Variable**: Configure the `JC_HOME` environment variable to point to your JavacardKit directory. 30 | ```bash 31 | export JC_HOME= 32 | ``` 33 | 34 | 3. **Run Gradle Build**: Execute the following command to build the JavaCard application, which will produce a `.cap` file for installation. 35 | ```bash 36 | ./gradlew buildJavaCard 37 | ``` 38 | 39 | 40 | ## Testing the Application 41 | 42 | ### Overview 43 | You have multiple options for testing the JavaCard application: 44 | 45 | 1. **Actual Smartcard**: You can test on a physical smartcard. 46 | 2. **Virtual SmartCard**: Alternatively, you can use VSmartCard and JCardSim for quicker and easier testing. 47 | 48 | ### Detailed Steps 49 | 50 | #### Option 1: Using Actual Smartcard 51 | Simply install the `.cap` file onto the smartcard and proceed with testing. 52 | 53 | #### Option 2: Using Virtual SmartCard and JCardSim 54 | 1. **VSmartCard and JCardSim**: Use these tools for a simulated environment. 55 | 2. **Third-Party Testing Suites**: Utilize tools like SoloKey's `fido2-tests` for comprehensive analysis. The `VSim` class can help you get started. 56 | 57 | #### Python Tests 58 | 1. **Python Test Suite**: Navigate to the `python_tests` directory, which contains Python-language tests. 59 | 2. **Run the Tests**: Execute the following commands to set up and run the tests. 60 | ```bash 61 | export JC_HOME= 62 | ./gradlew jar testJar 63 | python -m venv venv 64 | ./venv/bin/pip install -U -r requirements.txt 65 | ./venv/bin/python -m unittest discover -s python_tests 66 | ``` 67 | 3. **Interoperability**: These tests use the Python `python-fido2` library because there is currently no FIDO2 client library for the JVM. You can also test with `libfido2`, Python libraries, or the official FIDO Standards Tests (Javascript). 68 | 69 | #### Advanced Settings 70 | - **Fast IPC**: By default, the tests use fast interprocess communication with the JVM, bypassing PC/SC. The tests take less than 71 | fifteen seconds to run, for me, even though there are almost two hundred cases. 72 | - **Customization**: You can modify settings in `python_tests/ctap/ctap_test.py` to enable CTAP traffic logging, allow JVM remote debugging, or use a VSmartCard PC/SC connection. 73 | 74 | 75 | 76 | 77 | ## Contributing 78 | 79 | - If you wish to contribute to the project, feel free to raise a pull request or open an issue. 80 | 81 | ## Where to go Next 82 | 83 | If you just want to install the app, look at [what you can configure](docs/installation.md). 84 | 85 | I suggest [reading the FAQ](docs/FAQ.md) and perhaps [the security model](docs/security_model.md). 86 | 87 | If you're a really detail-oriented person, you might enjoy reading 88 | [about the implementation](docs/implementation.md). 89 | 90 | ## Implementation Status 91 | 92 | | Feature | Status | 93 | |------------------------------------|-------------------------------------------------------| 94 | | CTAP1/U2F | Implemented (see [install guide](docs/certs.md)) | 95 | | CTAP2.0 core | Implemented | 96 | | CTAP2.1 core | Implemented | 97 | | Resident keys | Implemented | 98 | | User Presence | User always considered present: one verification only | 99 | | ECDSA (SecP256r1) | Implemented | 100 | | Other crypto, like ed25519 | Not implemented - availability depends on hardware | 101 | | Self attestation | Implemented | 102 | | Basic attestation with ECDSA certs | Implemented (see [install guide](docs/certs.md)) | 103 | | Webauthn (NOT CTAP!) uvm extension | Implemented | 104 | | Webauthn devicePubKey extension | Not implemented | 105 | | CTAP2.1 hmac-secret extension | Implemented | 106 | | CTAP2.1 alwaysUv option | Implemented | 107 | | CTAP2.1 credProtect option | Implemented | 108 | | CTAP2.1 PIN Protocol 1 | Implemented | 109 | | CTAP2.1 PIN Protocol 2 | Implemented | 110 | | CTAP2.1 credential management | Implemented | 111 | | CTAP2.1 enterprise attestation | Implemented in code, disabled | 112 | | CTAP2.1 authenticator config | Implemented | 113 | | CTAP2.1 minPinLength extension | Implemented, default max two RPIDs can receive | 114 | | CTAP2.1 credBlob extension | Implemented, discoverable creds only | 115 | | CTAP2.1 largeBlobKey extension | Implemented | 116 | | CTAP2.1 authenticatorLargeBlobs | Implemented, default 1024 bytes storage (max 4k) | 117 | | CTAP2.1 bio-stuff | Not implemented (doesn't make sense in this context?) | 118 | | Key backups | Not implemented | 119 | | APDU chaining | Supported | 120 | | Extended APDUs | Supported | 121 | | Performance | Adequate (sub-3-second common operations) | 122 | | Resource consumption | Reasonably optimized for avoiding flash wear | 123 | | Bugs | Yes | 124 | | Code quality | No | 125 | | Security | Theoretical, but see "bugs" row above | 126 | 127 | ## Software Compatibility 128 | 129 | | Platform | Status | 130 | |---------------------------|------------------| 131 | | Android (Google Play) | CTAP1 only [1] | 132 | | Android (hwsecurity) | Working | 133 | | Android (MicroG) | Working | 134 | | Android (FIDOk) | Working | 135 | | iOS | Reported working | 136 | | Linux (libfido2) | Working | 137 | | Linux (FIDOk) | Working | 138 | | Windows 10 | Working | 139 | 140 | | Smartcard | Status | 141 | |-----------------------------------------------------------------------------------|------------------| 142 | | J3H145 (NXP JCOP3) | Working | 143 | | J3R180 (NXP JCOP4) | Working | 144 | | OMNI Ring (Infineon SLE78) | Working | 145 | | jCardSim | Working | 146 | | [Vivokey FlexSecure (NXP JCOP4)](https://dangerousthings.com/product/flexsecure/) | Working | 147 | | A40CR | Reported Working | 148 | 149 | | Application | Status | 150 | |---------------------|--------------------------------| 151 | | Chrome on Android | CTAP1 Only (Play Services [1]) | 152 | | Chrome on Linux | Working, USBHID only [2] | 153 | | Chrome on Windows | Working | 154 | | Fennec on Android | CTAP1 Only (Play Services [1]) | 155 | | WebView on Android | Working | 156 | | Firefox on Linux | Working, USBHID only [2] | 157 | | Firefox on Windows | Working | 158 | | MS Edge on Windows | Working | 159 | | Safari on iOS | Reported working | 160 | | OpenSSH | Working | 161 | | pam_u2f | Working | 162 | | systemd-cryptenroll | Working | 163 | | python-fido2 | Working | 164 | | FIDOk | Working | 165 | 166 | There are two compatibility issues in the table above: 167 | 1. Google Play Services on Android contains a complete webauthn implementation, but it appears to be 168 | hardwired to use only "passkeys". If a site explicitly requests a *non-discoverable* credential, 169 | you will be prompted to use an NFC security key, but this is only CTAP1 and not CTAP2. There's 170 | nothing fundamentally preventing this from working on Android but the current state of Chrome 171 | and Fennec are that CTAP2 doesn't, because both use the broken Play Services library. MicroG has 172 | a fully-working implementation, though! See https://github.com/microg/GmsCore/pull/2194 for PIN 173 | support. 174 | 1. Some browsers support FIDO2 in theory but only allow USB security keys - this implementation 175 | is for PC/SC, and doesn't implement USB HID, so it will only work with FIDO2 176 | implementations that can handle e.g. NFC tokens instead of being restricted to USB. 177 | In order to use a smartcard in these situations you'll need https://github.com/StarGate01/CTAP-bridge , 178 | https://github.com/BryanJacobs/fido2-hid-bridge/ , https://github.com/BryanJacobs/FIDOk/ or similar, 179 | bridging USB-HID traffic to PC/SC. 180 | -------------------------------------------------------------------------------- /attestation_key_pair.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64 4 | 5 | from cryptography.hazmat.primitives.asymmetric import ec 6 | from cryptography.hazmat.primitives._serialization import Encoding 7 | from cryptography.hazmat.primitives import serialization 8 | 9 | if __name__ == '__main__': 10 | privkey = ec.generate_private_key(ec.SECP256R1()) 11 | pubkey = privkey.public_key() 12 | 13 | private_bytes = privkey.private_numbers().private_value.to_bytes(length=32, byteorder='big') 14 | public_bytes = pubkey.public_bytes(encoding=Encoding.X962, format=serialization.PublicFormat.UncompressedPoint) 15 | 16 | print("PRIVATE key: " + str(base64.b64encode(private_bytes))) 17 | print("PUBLIC key: " + str(base64.b64encode(public_bytes))) 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { url "https://javacard.pro/maven" } 5 | } 6 | } 7 | 8 | 9 | plugins { 10 | id("java") 11 | id("com.klinec.gradle.javacard") version "1.8.0" apply false 12 | } 13 | 14 | var jcHomeSet = System.getenv("JC_HOME") != null 15 | if (!jcHomeSet) { 16 | project.logger.warn("JC_HOME environment variable not set - doing a testing/fake build with jCardSim!") 17 | project.logger.warn("YOU WILL NOT BE ABLE TO BUILD A JAVACARD APPLET THIS WAY") 18 | } 19 | 20 | group = "us.q3q" 21 | version = "1.0-SNAPSHOT" 22 | 23 | repositories { 24 | mavenCentral() 25 | } 26 | 27 | dependencies { 28 | if (jcHomeSet) { 29 | testImplementation(group: 'com.klinec', name: 'jcardsim', version: '3.0.5.11') { 30 | // Javacard will be provided by the user at runtime through the JC_HOME env var 31 | exclude(group: 'oracle.javacard', module: 'api_classic') 32 | } 33 | } else { 34 | // Perform a full-test build, since there's no javacard SDK 35 | implementation(group: 'com.klinec', name: 'jcardsim', version: '3.0.5.11') 36 | } 37 | testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.1' 38 | 39 | testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1' 40 | } 41 | 42 | test { 43 | useJUnitPlatform() 44 | } 45 | 46 | tasks.register('testJar', Jar) { 47 | archiveBaseName = project.name + '-tests' 48 | duplicatesStrategy = 'include' 49 | from sourceSets.test.output + sourceSets.test.allSource 50 | from { 51 | sourceSets.test.runtimeClasspath.filter { 52 | it.toString().indexOf("jcardsim-") != -1 53 | }.collect { 54 | zipTree(it) 55 | } 56 | } 57 | } 58 | 59 | if (jcHomeSet) { 60 | apply plugin: "com.klinec.gradle.javacard" 61 | javacard { 62 | config { 63 | cap { 64 | packageName 'us.q3q.fido2' 65 | version '0.4' 66 | aid PackageID 67 | output 'FIDO2.cap' 68 | applet { 69 | className 'us.q3q.fido2.FIDO2Applet' 70 | aid ApplicationID 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /decode_bufinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | if len(sys.argv) < 2: 6 | print("Usage: decode_bufinfo.py ") 7 | sys.exit(1) 8 | 9 | s = sys.argv[1] 10 | 11 | b = bytes.fromhex(s) 12 | 13 | def chop(x, amt): 14 | return x[amt:] 15 | 16 | def short_as(x, desc): 17 | v = int.from_bytes(x[:2], byteorder='big', signed=True) 18 | print("%s: %d" % (desc, v)) 19 | return chop(x, 2) 20 | 21 | def check_transient(x, desc): 22 | if x[0] == 0: 23 | print("%s: PERSISTENT" % desc) 24 | elif x[0] == 2: 25 | print("%s: transient" % desc) 26 | else: 27 | print("%s: UNKNOWN" % desc) 28 | return chop(x, 1) 29 | 30 | def check_type(x, desc, not_a_transient, transient_reset, transient_deselect): 31 | if x[0] == not_a_transient: 32 | print("%s: PERSISTENT" % desc) 33 | elif x[0] == transient_deselect: 34 | print("%s: transient" % desc) 35 | elif x[0] == transient_reset: 36 | print("%s: transient_RESET" % desc) 37 | else: 38 | print("%s: UNKNOWN" % desc) 39 | 40 | return chop(x, 1) 41 | 42 | if b[0:2] != b'\xFE\xFF': 43 | print("Invalid APDU result") 44 | sys.exit(1) 45 | 46 | b = chop(b, 2) 47 | b = short_as(b, "APDU Buffer Length") 48 | not_a_transient = b[0] 49 | b = chop(b, 1) 50 | transient_reset = b[0] 51 | b = chop(b, 1) 52 | transient_deselect = b[0] 53 | b = chop(b, 1) 54 | b = check_transient(b, "Authenticator Key Agreement Key") 55 | b = check_transient(b, "Credential Key") 56 | b = check_type(b, "PIN Token", not_a_transient, transient_reset, transient_deselect) 57 | b = check_type(b, "Shared Secret Verify Key", not_a_transient, transient_reset, transient_deselect) 58 | b = check_type(b, "Permissions RP ID", not_a_transient, transient_reset, transient_deselect) 59 | b = check_transient(b, "Shared Secret AES Key") 60 | b = check_transient(b, "PIN Wrapping Key") 61 | b = check_type(b, "Request/Response Buffer Memory", not_a_transient, transient_reset, transient_deselect) 62 | b = short_as(b, "Transient Scratch Buffer Size") 63 | b = short_as(b, "Persistent Memory Remaining") 64 | b = short_as(b, "Transient Reset Memory Remaining") 65 | b = short_as(b, "Transient Deselect Memory Remaining") 66 | 67 | if b[0:2] != b'\xFE\xFF': 68 | print("Invalid APDU result") 69 | sys.exit(1) 70 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## How can you have an FAQ on a newly-created repository? Surely the questions aren't frequent yet. 4 | 5 | You caught me, I'm a fraud and these are anticipatory questions. 6 | 7 | ## What's FIDO2? 8 | 9 | FIDO2 is a standard to replace passwords with secure, intercompatible devices. 10 | With a FIDO2 Authenticator - like this software implements - you can log in to 11 | a web site without choosing a username or password, and be confident that nobody 12 | without the Authenticator will be able to pretend to be you. 13 | 14 | ## What's a Javacard? 15 | 16 | Javacard (Classic) is an operating system that runs, mostly, on secure microprocessors. 17 | It lets you put little applications on things like phone SIM cards, passports, or ID badges. 18 | 19 | ## Don't you need a CBOR parser to write a CTAP2 authenticator? 20 | 21 | Apparently not. Instead of implementing a real CBOR parser I just 22 | poured more sweat into the implementation, and added a topping of 23 | non-standards-compliance. 24 | 25 | As a result of not having a proper CBOR parser, the app will often 26 | return undesirable error codes on invalid input, but it should 27 | handle most valid input acceptably. 28 | 29 | It does this by linearly scanning a byte-buffer with the CBOR object in it, 30 | and moving a read index forward the desired amount. Unknown objects get skipped. 31 | Any object declaring a length greater than two bytes long causes an error, 32 | because it's not possible to have >65535 of something in a 1,024-byte-long 33 | buffer, and the CTAP2 standard requires that CBOR be "canonical". 34 | 35 | ## Why did you write this, when someone else said they were almost done writing a better version? 36 | 37 | Well, they said that, but they hadn't published the source code and I got impatient. 38 | 39 | Two is better than zero, right? 40 | 41 | UPDATE: this repository was published in 2022, and as of 2023 (more than a year 42 | later) there are zero other open-source CTAP2 Javacard implementations available, 43 | so far as I can tell. There's a difference between talking about doing something 44 | and actually doing it. 45 | 46 | ## Why did you write this at all? 47 | 48 | I was pretty unhappy with the idea of trusting my "two factor" SSH 49 | keys' security to a closed-source hardware device, and even the 50 | existing open hardware devices didn't work the way I wanted. 51 | 52 | I wanted my password to be used in such a way that without it, the 53 | authenticator was useless - in other words, a true second factor. 54 | 55 | So I wrote a CTAP2 implementation that [had that property](security_model.md). 56 | 57 | ## The implementation says you're not "standards compliant". Why? 58 | 59 | Well, first off, this app doesn't attempt to do a full CBOR parse, so its error 60 | statuses often aren't perfect and it's generally tolerant of invalid input. 61 | 62 | Secondly, the CTAP API requires user presence detection, but there's really no 63 | way to do that on Javacard 3.0.4. We can't even use the "presence timeout" 64 | that is described in the spec for NFC devices: there's no timer! So you're 65 | always treated as being present, which is to some extent offset by the fact 66 | that anything real requires you type your PIN (if one is set)... Additionally, 67 | this app will not clear CTAP2.1 PIN token permissions on use. 68 | 69 | So set a PIN, and unplug your card when you're not using it. 70 | 71 | Finally, the CTAP2.0 and CTAP2.1 standards are actually mutually incompatible. When 72 | a getAssertion call is made with an `allowList` given, CTAP2.0 says that the 73 | authenticator should iterate through assertions generated with the matching 74 | credentials from the allowlist. CTAP2.1 says the authenticator should pick one 75 | matching credential, return an assertion generated with it, and ignore any 76 | other matches. 77 | 78 | This implementation uses the CTAP2.1 behavior. Because one or the other must be 79 | chosen, it can't be both fully CTAP2.0 compatible and CTAP2.1 compatible at the same time. 80 | 81 | Another more minor difference is that CTAP2.0 allows PINs of 64 bytes or longer. 82 | This authenticator and CTAP2.1 cap PINs at 63 bytes long. 83 | 84 | ## How does the security model handle U2F? 85 | 86 | U2F doesn't support PINs. 87 | 88 | [The security model](security_model.md) requires PINs. 89 | 90 | The compromise is that the authenticator will not make credProtect=3 credentials 91 | available over U2F, and U2F is entirely disabled when the `alwaysUv` option is 92 | enabled. This is accomplished by encrypting the U2F credentials with the "low security" 93 | wrapping key. 94 | 95 | ## Isn't PBKDF2 on a smartcard a fig leaf? 96 | 97 | Probably, yes, but it makes me feel better. 98 | 99 | You can raise the iteration count, but really there's only so much that can be 100 | done here. At least it means off-the-shelf rainbow tables probably won't work. 101 | 102 | ## I hear bcrypt or Argon2id is better than PBKDF2 103 | 104 | Good luck implementing those on a 16-bit microprocessor. I welcome you to try. 105 | 106 | ## What does this implementation store for resident keys? 107 | 108 | It will store: 109 | - the credential ID (an AES256 encrypted blob of the RP ID SHA-256 110 | hash, the credential private key, and the credProtect level) 111 | - up to 32 characters of the RP ID, again AES256 encrypted 112 | - a max 64-byte-long user ID, again AES256 encrypted 113 | - a user name, yet again AES256 encrypted 114 | - the 64-byte public key associated with the credential, unencrypted 115 | - Several 16-byte random IVs used for encrypting each field 116 | - the length of the RP ID, unencrypted 117 | - the length of the user ID, unencrypted 118 | - the length of the user name, unencrypted 119 | - a boolean set to true on the first credential from a given RP ID, used 120 | to save state when enumerating and counting on-device RPs 121 | - a two-bit credProtect level value 122 | - the length of any credBlob stored with the credential 123 | - how many distinct RPs have valid keys on the device, unencrypted 124 | - how many total RPs are on the device, unencrypted 125 | 126 | This is almost the minimum to make the credentials management API work; the 127 | user name (not ID!) is technically optional, but useful to have for credentials 128 | management. 129 | 130 | It would be possible to encrypt the length fields too, they just aren't and 131 | I didn't see it as important. It would be possible with much more work to 132 | remove the separately-stored credProtect level and use the one inside the 133 | Credential ID. 134 | 135 | ## Why is the code quality so low? 136 | 137 | You're welcome to contribute to improving it. I wrote this for a purpose and 138 | it seems to work for that purpose. 139 | 140 | Please remember that this code is written for processors that don't have an 141 | `int` type - only `short`. Most function calls are a runtime overhead, and 142 | each object allocation comes out of your at-most-2kB of RAM available. You 143 | can't practically use dynamic memory allocation at all, it's just there to tease 144 | you. 145 | 146 | The code I wrote may look ugly, and it's certainly not perfect, but it is 147 | reasonably efficient in execution on in-order processors with very limited 148 | stacks. 149 | 150 | A perfect example of this is the BufferManager class. It looks like a mess, but 151 | it makes it possible to use both sides of the APDU buffer as transient memory, 152 | avoiding flash wear on very memory-constrained devices. 153 | 154 | ## I'm getting some strange CBOR error when I try to use this 155 | 156 | Run the app in JCardSim with VSmartCard and hook up your Java debugger. 157 | See what's going on. Raise a pull request to fix it. 158 | 159 | ## Can you make this work on Javacard 2.2.1? 160 | 161 | Ahahahahahahahaahha 162 | 163 | Javacard versions before 3.0.1 don't support SHA-2 hashing. Not gonna happen. 164 | 165 | ## What does this applet mean for the flash storage lifetime of my smartcard? 166 | 167 | On every makeCred or getAssertion operation, the app will: 168 | 169 | - Increment a flash-stored counter (1-4 bytes written) 170 | - Set an elliptic curve private key object 171 | 172 | The applet will try to allocate the EC private key object in RAM, but will fall back to flash if 173 | the smartcard doesn't support RAM-backed allocations or doesn't have enough RAM. The flash-stored 174 | counter is wear-leveled across 67 bytes. 175 | 176 | On every powerup or unsuccessful PIN attempt, the app will set an elliptic curve private key object. 177 | 178 | On every PIN attempt, the app will overwrite a PIN retry counter... whether the PIN attempt is 179 | successful or not. But that's one byte written for a failure, or two bytes for a success. The PIN 180 | retry counter is wear-leveled across 64 bytes. 181 | 182 | Additionally, creating or deleting resident keys will (of course) write to flash, and there are some 183 | initial flash writes when installing or resetting the applet. 184 | 185 | Overall, this applet is pretty great at keeping everything in RAM, and you're much more likely 186 | to be given trouble by software bugs than by your flash write endurance. Flash is never used as 187 | writable buffer space if your smartcard has at least 2k of RAM, is only used for long request 188 | chaining if your smartcard has 1k of RAM, and is only used for "ordinary" requests if you're under 189 | around 200 bytes of RAM. Great care has been taken to make sure the most common operations like 190 | getPinToken and getKeyAgreement don't write to flash unnecessarily. 191 | 192 | Note that operations like writing the largeBlobStore will, of course, use flash. 193 | 194 | If you want to assess exactly what is and is not in RAM on your particular Javacard, you can install 195 | the applet and send APDUs like the following: 196 | 197 | gpp -d -a 00A4040008A0000006472F000100 -a 801000000145FF 198 | 199 | (This is an app select, followed by the CTAP2 vendor use area command `0x45`) 200 | 201 | The result that comes back can be decoded by passing it to the included `decode_bufinfo.py` script. 202 | 203 | ## Can I update the app and maintain the validity of my previously-issued credentials? 204 | 205 | No. Once you start using a certain version of the applet, you're stuck on that version if you 206 | want the issued credentials to stay valid. Have multiple authenticators, eh? 207 | 208 | ## Are there any limits to how long I can keep using this on a card? 209 | 210 | Each time you create a credential or get an assertion (regardless of whether the credential is 211 | discoverable or not) a counter is incremented. When it hits its maximum value of 2^32 you must 212 | reset the authenticator (invalidating all created credentials) to continue using the app. This counter is 213 | shared across all credentials, discoverable or otherwise. 214 | 215 | 2^32 is 4,294,967,296 - large enough that you could use the token once per second for 136 years 216 | without reaching the maximum value. The counter is incremented by up to sixteen per 217 | operation, so you could get unlucky enough to only get 8.5 years of using it once per second. 218 | 219 | That should be enough longevity. 220 | 221 | The only other limit to be aware of is the PIN retry count - if you incorrectly attempt a PIN eight 222 | times (by default) across three power-ups of the authenticator without successfully entering it once, 223 | the app will be locked and you won't be able to use it without clearing everything. 224 | 225 | ## I'm getting "operation denied" for certain requests 226 | 227 | The authenticator will, by default, refuse to create credProtect=3 non-discoverable 228 | credentials without a PIN set. This is to avoid needing to store the credProtect 229 | level in the credential ID itself. 230 | 231 | If you want to use this authenticator with those relying parties, set a PIN. 232 | -------------------------------------------------------------------------------- /docs/certs.md: -------------------------------------------------------------------------------- 1 | # Installing the Applet for Basic Attestation 2 | 3 | A default install of the FIDO2Applet will use "self attestation". This prevents any 4 | CTAP1/U2F functionality (U2F requires attestation certificates). The authenticator will 5 | have a CTAP2 AAGUID of all zeros. 6 | 7 | If you instead wish to use CTAP2 Basic Attestation and/or CTAP1, you will need to provide 8 | an AAGUID, a certificate chain, and a private key before using the applet. Only P256 9 | certificates (ECDSA) are supported for the authenticator's own certificate; any algorithm 10 | may be used for CAs further up the chain. 11 | 12 | These may be provided via a vendor CTAP command (command byte 0x46). In order 13 | to enable the vendor CTAP command, you must install the applet with parameters enabling it: 14 | see [the install guide](installation.md). 15 | 16 | The vendor CTAP command will be rejected if the authenticator already contains a certificate, 17 | or if the authenticator has been used to make any credentials since the last reset, so it must 18 | be installed FIRST. Note that the AAGUID and certificate are not cleared by resetting the 19 | authenticator; once installed, they persist until the applet is deleted, and cannot be changed. 20 | 21 | The syntax for the data to the vendor command is as follows: 22 | 23 | 1. 16 byte AAGUID 24 | 1. 32 byte ECDSA private key point (aka the S-value) 25 | 1. Two-byte total length of CBOR object following this one 26 | 1. Remaining bytes are a CBOR-encoded array of certificates, with each cert encoded as DER. The 27 | first certificate in the array must correspond to the authenticator's own key. Note that this 28 | MUST be a CBOR array even if it contains only one element! 29 | 30 | Notes on length: 31 | - You'll be using APDU chaining, so the maximum total size is a hair under 65535 bytes 32 | - Certificates will be stored directly into an on-flash byte array, so the maximum is also 33 | limited by the available flash 34 | - Keep the first certificate to a few hundred bytes or U2F/CTAP1 registration requests will fail: 35 | the device only has a 1024 byte transmission buffer 36 | 37 | Advice: keep your certificates *as short as possible*, since the longer they are, the more 38 | flash you'll use and the slower the makeCredential/register operations will be. 39 | 40 | You can install a self-signed certificate easily using the `install_attestation_cert.py` script in 41 | the repository root. 42 | -------------------------------------------------------------------------------- /docs/implementation.md: -------------------------------------------------------------------------------- 1 | ## Implementation Details 2 | 3 | This page describes some of the key architectural choices of the applet. 4 | 5 | ### Coding Style 6 | 7 | There are very few function calls in this application. Most code is explicit. This is due, in part, to 8 | performance concerns, and in part due to the relatively highly optimized memory management. There's just 9 | not much point to using a function if you couldn't safely _call_ it from any other context, or if it 10 | took twenty input parameters and had five side effects... 11 | 12 | ### CBOR 13 | 14 | Without dynamic memory allocation, parsing CBOR is difficult: it describes nested structures with varying 15 | lengths. 16 | 17 | This application doesn't parse CBOR so much as it streams it. Each byte is read in sequence, and occasionally 18 | indexes into the stream are saved. 19 | 20 | The key functions for doing this are `consumeAnyObject` (reads past the next CBOR entry), `consumeMapAndGetID` 21 | (reads a map object and, after call, puts the index and length of its `id` entry into temp variables), and 22 | `getMapEntryCount` (returns the number of entries in a map). 23 | 24 | This strategy of linearly reading only works due to the CTAP requirement of canonical CBOR: there literally 25 | is only one way to represent each input, and the fields must be in a defined order. 26 | 27 | ### Buffer Manager 28 | 29 | Use of dynamic memory isn't really allowed on a Javacard. Nonetheless, we need access to dynamic-ish 30 | structures to do things like computing hashes or signing data. 31 | 32 | The BufferManager class abstracts away the use of dynamic memory. It can place allocations into four 33 | different areas: 34 | 35 | - The lower half of the APDU, "behind" the read cursor 36 | - The upper half of the APDU, "above" where the output will reside (or in a place the output will be, 37 | if the allocation is first freed) 38 | - An in-memory temporary buffer 39 | - A flash buffer as a last resort 40 | 41 | In order to abstract away the choice of storage, allocating memory consists of three calls. The first 42 | returns a "handle". The second uses the handle to get the byte buffer in which the storage is placed. 43 | The third returns the offset into that byte buffer of the allocation. 44 | 45 | In other words, when using the buffer manager, you must always use offsets - who knows what other data 46 | are in the same buffer with your allocation? 47 | 48 | Internally, the Buffer Manager uses three bytes of the in-memory buffer to record fill levels for 49 | flash and RAM, and four bytes of the APDU - located around the 8k mark for very huge APDUs or at 50 | the end for more reasonable ones - to track how much of the APDU is full. 51 | 52 | This means individual allocations cannot be freed in arbitrary order: *all allocations must be released 53 | in the opposite order in which they were allocated*, and also *the same size must be passed to the 54 | free call as the allocate one*. The Buffer Manager doesn't track how many allocations have been made 55 | only what size they are! 56 | 57 | Handle IDs encode the buffer in which they're allocated: 58 | 59 | - Large negative numbers are in memory 60 | - Small negative numbers are in the APDU 61 | - Positive numbers are in flash 62 | 63 | Lower APDU space only becomes available when the `informAPDUBufferAvailability` call is made. Where an 64 | allocation is allowed to live can be controlled by passing a parameter to the `allocate` call - don't 65 | put something in the APDU if you want it to survive across another request! 66 | 67 | ### Status Bits 68 | 69 | The `TransientStorage` class manages runtime state - state that mostly gets cleared when another applet 70 | is selected. Sending "chained" APDU responses or receiving "chained" APDU requests is done by using the 71 | `outgoingContinuation` and `chainIncomingRead` methods in that class. Incoming reads store a two-byte 72 | offset, and outgoing continuations store both a two-byte offset and a two-byte total length. 73 | 74 | All the data to be streamed (except possibly the first payload) should be in `bufferMem` before 75 | setting up the streaming. 76 | 77 | `TransientStorage` also contains bitfields for things like whether the authenticator has been "unlocked" 78 | by a PIN since power-on, and if so what PIN protocol was used for it. 79 | 80 | ### Delivering Attestation Certificates 81 | 82 | DER-encoded X.509 certificates can be, for this type of application, extremely large - multiple kilobytes. 83 | In order to handle large certificates, a special bit in `TransientStorage` indicates that one of these is 84 | necessary after the response. If that bit is set, the outgoing stream will continue from `attestationData` 85 | after `bufferMem` is exhausted. 86 | 87 | Unfortunately, the `largeBlobKey` extension places its own data AFTER the extremely long certificate. To 88 | deal with this, a hack is used: the `largeBlobKey` is placed at the very end of `bufferMem` (in its last 89 | 32 bytes), and another bit in `TransientStorage` is set to indicate that when `attestationData` is empty, 90 | those 32 bytes should be sent to the user. 91 | 92 | ### Credentials, IVs, and Wrapping Keys 93 | 94 | Each credential this application produces is a combination of the SHA256 hash of an RPID and the 95 | credential's own private key. It also contains a bit indicating whether the credential is discoverable, 96 | a byte indicating the credential's protection level, a 16 byte IV for encryption, and a 16 byte HMAC 97 | for verification. 98 | 99 | There are also 14 random/unused bytes inside the credential to make it an even multiple of AES256's 100 | encryption block size (16 bytes). 101 | 102 | The discoverability bit is necessary because deleting a discoverable credential should invalidate it, 103 | even if it is given back to the authenticator in an allowList. 104 | 105 | Each RK gets a separate IV for each of its data structures: 106 | 107 | - Encrypted user ID 108 | - Encrypted user name 109 | - Encrypted RP name 110 | - credBlob (don't confuse this with largeBlobKey) 111 | - largeBlobKey (don't confuse this with credBlob) 112 | 113 | When a particular item has a dynamic length, the length is stored unencrypted. All objects are multiples of 32 114 | bytes long to allow easy AES256 decryption. 115 | 116 | ### Enumeration 117 | 118 | Finding whether a particular credential is an RK is done by walking the list, and doing a byte-exact 119 | comparison with the credential ID being checked. This means deleting an RK invalidates it even when it 120 | is presented to the authenticator in an `allowList`. 121 | 122 | Each RK has a bit that says whether it is a "representative" of a unique RP. This is set at the time the 123 | credential is being stored. When a credential with this bit set is deleted, all the credentials are scanned 124 | to find another credential sharing that same RP. If there is one, it is flagged as the new representative. 125 | 126 | RKs are stored in a list in order of their creation. When an RK is replaced, it is moved to the end of the list, 127 | so the stored RKs are always sorted. 128 | 129 | Enumerating assertions is done by storing, in memory, the relevant portions of the getAssertion request and 130 | the index of the last resident key returned. `getNextAssertion` goes through the resident key list 131 | in reverse, starting with the key "before" the one last returned; this ensures that creds are produced 132 | in the standard-specified "descending" order. 133 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation Parameters 2 | 3 | This applet provides a variety of install-time configurable settings. These are configured via a 4 | CBOR map provided when the applet is installed (for example, via `gpp --install --params `). 5 | 6 | To generate the parameter string, use the `get_install_parameters.py` script at the root of the repository. 7 | The help the script provides (`--help`) explains each options. 8 | 9 | The defaults - when no install parameters are provided - are for maximum FIDO standards compatibility, but 10 | won't accept an attestation certificate. So if you want CTAP1/U2F, you'll need to install the applet with 11 | parameters. 12 | 13 | If you want attestation to work, you'll also need to run `./install_attestation_cert.py` after installing the 14 | applet itself! 15 | -------------------------------------------------------------------------------- /docs/requirements.md: -------------------------------------------------------------------------------- 1 | # Runtime requirements 2 | 3 | This app requires, in theory, Javacard 3.0.4. I really, really, 4 | really wanted to support Javacard 3.0.1, which runs on cool 5 | products like the Mclear/NFCRings.com OMNI Ring (seriously nice tech!). 6 | 7 | Unfortunately, CTAP2.0 requires an EC diffie-helmann key exchange 8 | in order to support PINs or the hmac-secret extension, and it uses 9 | DH with a SHA256 hash. Javacard 3.0.1 officially supports only ECDH-SHA1. 10 | Javacard 3.0.4 doesn't support ECDH-SHA256 either... but it provides 11 | a "plain" variant that returns the raw DH output which you then 12 | hash yourself - good enough for me. 13 | 14 | So it's not possible to make this app work in a meaningful way on 15 | the original Javacard 3.0.1 or earlier **as the spec is written** (see below!). 16 | 17 | You might think you could implement ECDH yourself in software. I don't think 18 | you're right. ECDH agreement is just one, simple, elliptic-curve multiplication 19 | operation. But these processors don't even have 32-bit integers, much less the 20 | 128-bit ones we'd be using. It would just be too slow to be reasonable. It's 21 | not feasible, sorry. 22 | 23 | HOWEVER, there do exist cards that support the appropriate algorithms atop Javacard 24 | 3.0.1. Those will work. In fact, the OMNI Ring itself is one of those, and the app works 25 | there! Check [JCAlgTest](https://github.com/crocs-muni/JCAlgTest) on your target card. 26 | You need: 27 | 28 | - KeyBuilder `LENGTH_AES_256` (symmetric crypto for credential ID wrapping BUT also used 29 | for communication between the card and the platform) 30 | - Cipher `ALG_AES_BLOCK_128_CBC_NOPAD` (using symmetric crypto - note this is AES with 31 | 256-bit keys and 128-bit blocks, aka AES256, despite the 128 in the alg name) 32 | - Cipher getInstance `CIPHER_AES_CBC PAD_NOPAD` (using symmetric crypto) 33 | - KeyPair on-card generation `ALG_EC_FP LENGTH_EC_FP_256` (core ECDSA keypair generation) 34 | - KeyBuilder `TYPE_EC_FP_PRIVATE LENGTH_EC_FP_256` (core ECDSA keypair usage) 35 | - Signature `ALG_ECDSA_SHA_256` (actually signing card-produced results) 36 | - KeyAgreement `ALG_EC_SVDP_DH_PLAIN` (used in combo with SHA256 to implement secure channel 37 | between the card and the platform) 38 | - MessageDigest `ALG_SHA_256` (used to implement HMAC-SHA256 for verifying PINs, etc, and 39 | for the above-mentioned secure channel) 40 | - RandomData `ALG_SECURE_RANDOM` (used for key generation etc. Note `ALG_KEYGENERATION` is not 41 | used, that's too new) 42 | - Some memory, including ~14 bytes (yes, bytes) of `MEMORY_TYPE_TRANSIENT_RESET` and a 43 | larger amount of `MEMORY_TYPE_TRANSIENT_DESELECT` (ideally at least 134 bytes). The less RAM 44 | you have, the more flash wear will ensue. 45 | - About 200 bytes max commit capacity, used for atomically creating and updating resident 46 | keys 47 | - An amount of `MEMORY_TYPE_PERSISTENT` sufficient to hold the app and the resident keys, etc 48 | 49 | Signature `ALG_HMAC_SHA_256` is missing from the above list, because the HMAC part 50 | is implemented in software (using the OS-provided SHA256). 51 | 52 | You also **want** the following to avoid having flash storage wear each time the card is 53 | powered up, and the risk of private keys being stored there in the first place: 54 | 55 | - `TYPE_EC_FP_PRIVATE_TRANSIENT_DESELECT` (ideal) or `TYPE_EC_FP_PRIVATE_TRANSIENT_RESET` 56 | - `TYPE_AES_TRANSIENT_DESELECT` (ideal) or `TYPE_AES_TRANSIENT_RESET` 57 | 58 | So to summarize, let's discuss the full requirements on the authenticator side: 59 | 60 | - Javacard Classic 3.0.4 caveatted as above 61 | - Approximately 2kB of total RAM OR ~128 bytes plus comparatively more flash wear 62 | - A 256 byte APDU buffer (most cards have this although the standard only mandates 128 bytes) 63 | - Support for AES256-CBC 64 | - Support for ECDH-plain 65 | - Support for SHA-256 hashing 66 | - Support for EC with 256-bit keys 67 | - Approximately 30k of storage by default (very tunable) 68 | - Ideally, support for EC TRANSIENT_DESELECT keys, as otherwise you'll get flash usage 69 | every time the app is selected 70 | 71 | Examples of cards I've tested working are the NXP J3H145 (tons of RAM, nice card) 72 | and the Mclear OMNI (advertises 8k RAM but only makes 276 bytes available for the app!). 73 | Many other cards should work fine too. 74 | 75 | # Platform-side requirements 76 | 77 | On the computer side of things, you'll likely want `libfido2` compiled 78 | with support for PC/SC, which is currently experimental, and/or `libnfc`. On 79 | Arch Linux this is not the default - out of the box `libfido2` only works with 80 | USB HID tokens, which this is **not**. I have uploaded [an AUR package with 81 | the appropriate support for your convenience](https://aur.archlinux.org/packages/libfido2-full). 82 | 83 | Without either of those two options you will Have A Bad Day. 84 | 85 | If you have them, you should see the card start showing up in the output 86 | of `fido2-token -L`. You can see what gets sent to and from the card by 87 | setting `FIDO_DEBUG=1` before running your command. 88 | -------------------------------------------------------------------------------- /get_install_parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import base64 5 | 6 | if __name__ == '__main__': 7 | parser = argparse.ArgumentParser("get_install_parameters", 8 | description="Return parameters for installing FIDO2Applet with custom settings") 9 | parser.add_argument('--enable-attestation', action='store_true', default=None, 10 | help="Allows loading an attestation certificate after installing the applet") 11 | parser.add_argument('--high-security', action='store_true', default=None, 12 | help="Does not comply with the FIDO standards, but protects credentials against bugs or " 13 | "faulty authenticator hardware. Implies high-security RKs.") 14 | parser.add_argument('--force-always-uv', action='store_true', default=None, 15 | help="Requires the PIN for all operations, always") 16 | parser.add_argument('--high-security-rks', action='store_true', default=None, 17 | help="Protects discoverable credentials against bugs and faulty authenticator hardware, " 18 | "at the cost of standards compliance") 19 | parser.add_argument('--protect-against-reset', action='store_true', default=None, 20 | help="Require sending a reset command twice, across two power cycles, to truly reset " 21 | "the authenticator") 22 | parser.add_argument('--kdf-iterations', type=int, default=5, 23 | help="Number of iterations of the Key Derivation Function used. Protects against " 24 | "brute-force attacks against the PIN (when authenticator hardware is faulty), " 25 | "at the cost of performance") 26 | parser.add_argument('--max-cred-blob-len', type=int, default=32, 27 | help="Maximum length of the blob stored with every discoverable credential. Must be >=32") 28 | parser.add_argument('--large-blob-store-size', type=int, default=1024, 29 | help="Length of the large blob array in flash memory. Must be >=1024") 30 | parser.add_argument('--max-rk-rp-length', type=int, default=32, 31 | help="Number of bytes of the relying party identifier stored with each RK. Must be >=32") 32 | parser.add_argument('--max-ram-scratch', type=int, default=254, 33 | help="Number of bytes of RAM to use for working memory. Reduces flash wear.") 34 | parser.add_argument('--buffer-mem', type=int, default=1024, 35 | help="Number of bytes of RAM to use for request processing. Reduces flash wear. Must be >=1024") 36 | parser.add_argument('--flash-scratch', type=int, default=1024, 37 | help="Number of bytes of flash to use when RAM is exhausted. For low-memory situations.") 38 | parser.add_argument('--do-not-store-pin-length', action='store_false', default=None, 39 | help="Avoid storing the length of the user PIN internally. Causes setMinPin to force a PIN " 40 | "change") 41 | parser.add_argument('--cache-pin-token', action='store_false', default=None, 42 | help="Allow a PIN token to be potentially used multiple times, within its permissions") 43 | parser.add_argument('--multiple-writes-per-pin-token', action='store_false', default=None, 44 | help="In combination with --cache-pin-token, will allow many reads but only one write/use per " 45 | "time obtaining the PIN token") 46 | parser.add_argument('--certification-level', type=int, default=None, 47 | help="Obtained FIDO Alliance certification level") 48 | parser.add_argument('--attestation-private-key', 49 | help="Base64-encoded RAW (32 byte) private key for attestation certificate. Implies --enable-attestation") 50 | 51 | args = parser.parse_args() 52 | 53 | if args.buffer_mem < 1024: 54 | parser.error("CTAP standards require at least 1024 bytes of request/response buffer memory") 55 | 56 | if args.large_blob_store_size < 1024 or args.large_blob_store_size > 2048: 57 | parser.error("Large blob store size must be between 1024 and 2048 bytes") 58 | 59 | if args.max_cred_blob_len < 32 or args.max_cred_blob_len > 255: 60 | parser.error("Cred blob len must be between 32 and 255 bytes") 61 | 62 | if args.max_rk_rp_length < 32 or args.max_rk_rp_length > 255: 63 | parser.error("The RP length stored for each RK must be between 32 and 255 bytes") 64 | 65 | if args.attestation_private_key is not None: 66 | args.enable_attestation = True 67 | args.attestation_private_key = base64.b64decode(args.attestation_private_key) 68 | 69 | num_options_set = 0 70 | install_param_bytes = [] 71 | for option_number, option_string in enumerate([ 72 | 'enable_attestation', 73 | 'high_security', 74 | 'force_always_uv', 75 | 'high_security_rks', 76 | 'protect_against_reset', 77 | 'kdf_iterations', 78 | 'max_cred_blob_len', 79 | 'large_blob_store_size', 80 | 'max_rk_rp_length', 81 | 'max_ram_scratch', 82 | 'buffer_mem', 83 | 'flash_scratch', 84 | 'do_not_store_pin_length', 85 | 'cache_pin_token', 86 | 'certification_level', 87 | 'attestation_private_key', 88 | 'multiple_writes_per_pin_token' 89 | ]): 90 | val = getattr(args, option_string) 91 | if val is None: 92 | continue 93 | num_options_set += 1 94 | 95 | bytes_for_option = [] 96 | if val is True: 97 | bytes_for_option = [0xF5] 98 | elif val is False: 99 | bytes_for_option = [0xF4] 100 | elif isinstance(val, bytes): 101 | if len(val) <= 23: 102 | bytes_for_option = [0x40 + len(val)] 103 | elif len(val) <= 255: 104 | bytes_for_option = [0x58, len(val)] 105 | else: 106 | bytes_for_option = [0x59, (len(val) & 0xFF00) >> 8, len(val) & 0x00FF] 107 | bytes_for_option += [int(x) for x in val] 108 | else: 109 | if val <= 23: 110 | bytes_for_option = [val] 111 | elif val <= 255: 112 | bytes_for_option = [0x18, val] 113 | else: 114 | bytes_for_option = [0x19, (val & 0xFF00) >> 8, val & 0x00FF] 115 | 116 | install_param_bytes += [option_number] + bytes_for_option 117 | 118 | install_param_bytes = [0xA0 + num_options_set] + install_param_bytes 119 | print(bytes(install_param_bytes).hex()) 120 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | PackageID=A000000647 2 | ApplicationID=A0000006472F0001 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJacobs/FIDO2Applet/0194107d9648577379058b59843504924b546514/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /install_attestation_cert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import argparse 5 | import base64 6 | import binascii 7 | 8 | from fido2.ctap2 import Ctap2 9 | from fido2.ctap2.base import args as ctap_args 10 | from fido2.pcsc import CtapPcscDevice 11 | 12 | import secrets 13 | 14 | from cryptography.hazmat.primitives.asymmetric import ec 15 | from cryptography.hazmat.primitives._serialization import Encoding, PrivateFormat, NoEncryption 16 | from cryptography.hazmat.primitives.serialization import load_der_private_key 17 | 18 | from python_tests.ctap.ctap_test import BasicAttestationTestCase 19 | 20 | if __name__ == '__main__': 21 | parser = argparse.ArgumentParser(description='Install AAGUID and attestation certificate(s)') 22 | parser.add_argument('--name', 23 | default='FIDO2Applet', 24 | help='Common name to use for the certificate') 25 | parser.add_argument('--aaguid', 26 | default=None, 27 | help='AAGUID to use, expressed as 16 hex bytes (32-character-long string)') 28 | parser.add_argument('--ca-cert-bytes', 29 | default=None, 30 | help='CA certificate, expressed as base64-encoded DER') 31 | parser.add_argument('--ca-private-key', 32 | default=None, 33 | help='CA private key, expressed as base64-encoded unencrypted PKCS8 DER') 34 | parser.add_argument('--org', 35 | default='ACME', 36 | help='Organization name to use for certificates') 37 | parser.add_argument('--country', 38 | default='US', 39 | help='ISO country code to use for certificates') 40 | parser.add_argument('--already-loaded-public-key', 41 | help='The private key is already loaded on the card; this is the base64 DER-encoded PUBLIC key') 42 | args = parser.parse_args() 43 | 44 | if (args.ca_private_key is None) != (args.ca_cert_bytes is None): 45 | raise IllegalArgumentException("Either both or neither of CA certificate and private key must be set") 46 | 47 | aaguid = None 48 | if args.aaguid is not None: 49 | if len(args.aaguid) != 32: 50 | sys.stderr.write("Invalid AAGUID length!\n") 51 | sys.exit(1) 52 | aaguid = bytes.fromhex(args.aaguid) 53 | else: 54 | aaguid = secrets.token_bytes(16) 55 | 56 | tc = BasicAttestationTestCase() 57 | if args.ca_private_key is None: 58 | ca_privkey_and_cert = tc.get_ca_cert(org=args.org) 59 | privkey_bytes = ca_privkey_and_cert[0].private_bytes( 60 | encoding=Encoding.DER, 61 | format=PrivateFormat.PKCS8, 62 | encryption_algorithm=NoEncryption() 63 | ) 64 | print(f"Generated CA private key: {base64.b64encode(privkey_bytes)}") 65 | print(f"Generated CA cert: {base64.b64encode(ca_privkey_and_cert[1])}") 66 | else: 67 | privkey = load_der_private_key(data=base64.b64decode(args.ca_private_key), password=None) 68 | ca_privkey_and_cert = privkey, base64.b64decode(args.ca_cert_bytes) 69 | 70 | print(f"Using AAGUID: {aaguid.hex()}") 71 | 72 | get_certs_args = { 73 | "name": args.name, 74 | "ca_privkey_and_cert": ca_privkey_and_cert, 75 | "org": args.org, 76 | "country": args.country 77 | } 78 | 79 | if args.already_loaded_public_key is None: 80 | private_key = ec.generate_private_key(ec.SECP256R1()) 81 | get_certs_args['private_key'] = private_key 82 | else: 83 | private_key = None 84 | public_key = base64.b64decode(args.already_loaded_public_key) 85 | get_certs_args['public_key'] = ec.EllipticCurvePublicKey.from_encoded_point( 86 | ec.SECP256R1(), 87 | public_key 88 | ) 89 | print("Using existing public key " + str(binascii.hexlify(public_key))) 90 | cert_bytes = tc.get_x509_certs(**get_certs_args) 91 | 92 | at_bytes = tc.assemble_cbor_from_attestation_certs(private_key=private_key, 93 | cert_bytes=cert_bytes[:-1], 94 | aaguid=aaguid) 95 | 96 | print(binascii.hexlify(at_bytes)) 97 | 98 | devices = list(CtapPcscDevice.list_devices()) 99 | if len(devices) > 1: 100 | sys.stderr.write("Found multiple PC/SC devices!\n") 101 | sys.exit(1) 102 | if len(devices) == 0: 103 | sys.stderr.write("Could not find any usable FIDO PC/SC devices! Make sure your user account can read/write to pcscd...\n") 104 | sys.exit(1) 105 | device = devices[0] 106 | 107 | res = Ctap2(device).send_cbor( 108 | 0x46, 109 | ctap_args(at_bytes) 110 | ) 111 | print(f"Got response: {res} (empty is good)") 112 | -------------------------------------------------------------------------------- /mds.json: -------------------------------------------------------------------------------- 1 | { 2 | "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/", 3 | "description": "FIDO2 Javacard Applet", 4 | "aaguid": "b6a13d01-7826-af82-1b05-2b53adba1b4e", 5 | "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAZCAYAAABKM8wfAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAudEVYdFNvZnR3YXJlAFdpbmRvd3MgUGhvdG8gRWRpdG9yIDEwLjAuMTAwMTEuMTYzODQQf5EJAAAAB3RJTUUH5wgGERYLRCAwnwAAACB0RVh0Q3JlYXRpb25UaW1lADIwMjM6MDg6MDYgMTc6MjE6MjEdBQaaAAAAIXRFWHRDcmVhdGlvbiBUaW1lADIwMjM6MDg6MDYgMTc6MjE6MjGVimVcAAAM1UlEQVRYRy1Xe4wcd33/zHtnd2cft3e7e3s+b2znzDk+J9RNQiyCE6cYN4UGYvpKpTZqC0WlCCr1j4JaqaeK/kX/SYoEQkWqaKSmgQiIqgbJIjgBUhPXseNXYsd3Z5/vsbe3z5md2XlPP7NBp59ud3Ye39/3+3mN8OZ//UMCUcT3f3QZoZRBODLx7Olj2FMRYXoJnECFL0nwXB8ulyICQgwEXAkEhIkL2w14CwmyJEKRFCSJAC/0wa/I5SRoioogFJERAhQ1mdfIcHwg5B2kxMdw7ENUlMm1eQ18jgc3TJDw+jCWICJGVkv/8/y/eubx5bM/vYLY2sUDdQ2W1cfz/30bL14rQWNlQjBGvLoNZbaGcGhC4wOMvApDjpDEPhJBgCSrKKoyi2FxUgxdiVHMSNyOAD57srEgSCCDm51shgXoERSeJ4u8ng0R+FsUx5OiFInHeCCIBSRRhIgrjgMuFvzI0YXl1ZVVlOszuN7qwihkWUwAxCH+7cwKHpgScTCy8affuog/LN1GMncPdOODYtK/vG6gUlBRZmeKGQW6JkJlawu6hgI/i1LEzxKm2GldSQAxBCTe/9dFBizCD0QEEZDh7+nSuPH0t5jFi0kC14/Qt5LJOZLkWMufe+RhvH3mVfzexx6E7XswZsq4eKuN2+9fh+K14OayWJxT8NzPHayv2PjfLRFHDkyxwxwhC8qx2EiMMOQ07NiFKEiTLsVyCIcP9CM+mv9jrjGxJPC39FjAzgUp7FxOgIWLSdp1toEQ9QRCgvsTZAIiGYN3nEBG+su/+fiyrwwR7juCS+MsPrswgzPfewWH5rNwtCYEJcTJpVlsJlnc7dr469+U8aubLXz9RwOUvDVUa1ViN8SOxw2aO+g7NjsG6HyQRxj0bAHWKOZDxQmmXWJfYQejkJBKC2RhLB0yN5BCwk88boqtTCT2NyaHbFiBxSlqbIQI4T//5QvJ2tBHTpdR4O6OLOzBzPU72PvgYZz/1U1c22mhUTVg9ke4MpQx6PVxcFrGaxuAofTQrOag33sU+Xwf00UX3jiEEuSxWC+laETb1NAyLXS52Vbbx2DE3RDN+WIeeSOD2bqIe+oDQkrmtQo2TIeT8tHIl2FoKu4MdzG0XSiyhLygQ3jjO3+XdMn+bmJy9w4enluEdKuPvQXg6jDExuoKtJKEHNuz2R5iqlTCv76+QYLGcLO1STd23RICcwzB2IOY51XKIhr1PAYsctAdkPURyciOpmzPiJxIDEVVJ4Qch1SMIINSLcFnPhHBHZFknEaeRC5kcvi/lTsTaPiRgBzyELM5HY2yDsnVcGFFhTMgrlwSo5Rl11VcaQ2Q43jH3BQiH9VMAnPswXZizKsOpgwFe4wx7puP8eB8G83iAOK4i82bK+jv7iD2U/wFJFwElbLmOS677XFiJhlH1Ql38OE9PcQ72/jaNzxs2jqnNcZgSExzUl43x6Ug7KuUVBXSxx77reUQOjZsA//zzghffGgOA2cLiwf3I+6N8B/nbuD4oQqqJZ3dzcHQs3jlnS00pwzsn85CyciwvBiNmSJHOkIlL8EKBTRrKo9JKPB7Picjl+XKKyiVFDT3llAsG8iViogjD8Goiyl1jJIwxEtnqddVnbBxCZwiCV1ANlvCwmwOhbLNze99fPn6QEZp5RdYOngET91fx15VwU/ePI8fvt3Han+AkF3aO1dCbaqC2HXx3Jlb+PLJezmEPJIgpBrI8EkYiQ/vdk2YIxvmoI/x0EJMPLqug9HIQRJyMoMhxhbhx3N2tnfR71kTY0gNJJ2AwamdvUBpLJVxazuEFU9RKVTUygGsfh9iTRtxzAGerG7jmcYGeu2Xcbl1Cc1GFkvNOvp2iBd+eQdH9jdRnzbw2kobBysK9jcKyOgcFSVo2iBhNUpTKvIsKpc4BJ2NskIYuTZEFhy7I9iWBZ/q4I1t9HpdCq3LToYkZYChm2BgUq2cDvbgXVy4aMEozYHQRValVNId66W9EJ95SMPppQSj2adhLB7GucsZ6s4SKvedhDwVIJIVdMwQF98bc0gq/vnFt/HdL51Am4SKZAEmCygaOZSzClQxoQvSMIjXfGogNA+NJqFy5eQPuijThAajMYgkBL6LkvqBA24PSFp2OuUJ0YeyfQ2zRoTT9wNLjYSQ0pEpZCC89YOvJRkpj5W1Dbx1ZRViTP0k52yzj9feuYv7DjVx4cJ7ZLuJo/tn8a2//STMnQ5c5oO7JF7PGkNVJYRRgt12F2OPRdHFdgYO8SfCyGgwHZKNtYQsLfRDUEVRyYnojyin1OSuHWO9M8K+YoAZOqIZUOKYPdqZBfz7Vx/Bbt+h2VCXEwXC9775+UTXqBK04omXU5Z0Wac9ahCoCFQUmJSs03/2TeyrGHjlK7+Nte0WUKtjbbONRMnQEJgWCI2t1i7H6sAaB8S0gBkOa9pg8GGhPYu5g5tMA1TqhEOfmYPa27Vcnh9ja+Di2F6JM4yhcqp2LONqV8dXvvgRZDkxRhbIchbSE48eXbYpYw4vclwRI37u2z4f4KDNnXV5o9QSL1/dwicON/DRI3Ow6FjvXr1N7awgn9Fh2cQsNzvgNQa7urnb5f08zFMR8px9SGIKMXWYCU6gASQMPKm2WvaYk4knYajFtt9fE0muPKYYrqrTBXQsTsYoQ2TQspyI93ch/dGp+5ZHHF+dRCpkBeoqBZqdpYKhyDVbEDhiD58+vh9LeopvFRXqc7ZYwhrD0t12Z+L/GvNBwJCSppYWXXGrN0YzS8GkQaTVZVi4T4UJCJkx4eN6AWNmRH0P8H7LQZHPfvL+KmvQ07RLKHtY3R0ynzbw2L0qMoRElulRfH99nVnUgpoPyHpaj+ZByRKHeQ86l6TymERNLProN4gvT8A25cqhNJVY/JSq0Z5nWDQLQ4TNVgctKyZGNcw16iyUjchQ9Fl0jnieYibIM5HFkct0p6I7cumwCpZmmaAkFWVaNvMQVEWm7hdgd9qYoQtmix5JF0I89ug8Dh4uQ9MFRGlGVclqlSOUmQS4BCqBSiYHEi8qZzGKRGzvOMwFOzjQoAEwjnaosatbHZx/fwcvXezjyYUKvvTEIcxVqem1eVSqVTaD2SFnEF7cJCc0XcjRkolXjcwnUyRuyLNtXLm5joisN7kqjKXD4fiD7ExYZPifcZWB2ueWSLos266J9Bf6u8pkJAYcNW/GyAElkhB3+Y0RcqM7xNEPNXHm9XOYq5VIlAg3uIn1XRd5McCJxVns2t4kD0sM3vqvpzA/M435ygyPG5jLG8zdCvVXZUyNcbdH0axP4UC1SKynE+DicY8K0msTgpIMOX1Z+OOnf2N51PfIXB3jQUoAnpiynhk1ZJF+eiE3kfgicjHDBzF4d6eHb7x8fvKqtGd/HTKxeGPDxLsbA5LHwhvXtolVkq6oY3+zBolaneZ2n8SRKZttwuDqlomfr5jYHQxgWsNJE77wF6ew9NE61JDEdGXUmGcshqufXRjiIw9zIyYb8AcnF5frHI+Q4pSaKJEcAq12zM6mjDb5qegXEciMh3cYA+n/z7/8SzzwxAH849c/meo8fDpVq+fh9GcP4+lPLaJH1p9f6+GlN27h9TevYV+2AJ8y9t7dDn5yeRXfOXcT2Rkdnzq+D2fP34CeySNSqlhdu4vHj89DoEYrAq3aI1HZMIvqtTDHmoIhhJe+/ftJjgSpqZlJ+HaIUZKU5FCwTXlL2JkMPY47Qadl4oc/ZgH7Zuh0Yxw71uCLZYxLb65hx9bw2KeX8OiHi+j0x6jU8oj7fLugZZ+9tIXzb2+iWtehSxpOHW8Ano5qQ8FDp77NVyASTinioUPA33/++EQRyjUFr/7gEm5vjvBWu4Q/f/YgFqss48t/8uBymAyx2Q2g8cXRdseIeumrzQhTsUqBd2mjMrGsIKbB/O6Je/DCi+eg8a26RMv+xeUeHjv1CF549V187tkHmBVoBEMPrXWTDsb3Obpgk4VpNKMTD8+hMKPAb9l8R3NALqMVziIneJNg9Nw/nUSjyBDvOeDpqC9Mwb3dQ6jFfBFQuSES9Pmv/g51nP7fiNBeGzI+VpAxRLhOCFdg6FDoU9yxs+1j4Z4ZpqwAhbks1oYdeCMPM8UsMmYZG/IIWQqoSD0WyHjCGn1vjCAJ0MxlsDlISexi3546obGO5r28puVibE7Dbd3Ayxe6mG8exOrtDpb2KQjYvKtXfRwqdvDUiSZ+fLaD67uU+e8uP5WoJIfJdFWlLoZ0oY0dBvVKBs7QgVeSsZDX0WIX+nd8HJnNIGAo4Us3DuTzE2na7QV8zVex1nEwXc7xtWaEErOvRaeU6HCNepFOJeJ2v425ShXz3LBOFq5TSUwWLZBYg76AVy9vYYZ8KhDD1WkJo50Is4cN5ClxdodKoir4fykOSdmZudJPAAAAAElFTkSuQmCC", 6 | "alternativeDescriptions": {}, 7 | "protocolFamily": "fido2", 8 | "schema": 3, 9 | "authenticatorVersion": 1, 10 | "upv": [ 11 | { 12 | "major": 1, 13 | "minor": 1 14 | }, 15 | { 16 | "major": 1, 17 | "minor": 0 18 | } 19 | ], 20 | "authenticationAlgorithms": [ 21 | "secp256r1_ecdsa_sha256_raw" 22 | ], 23 | "publicKeyAlgAndEncodings": [ 24 | "cose" 25 | ], 26 | "attestationTypes": [ 27 | "basic_full" 28 | ], 29 | "userVerificationDetails": [ 30 | [ 31 | { 32 | "userVerificationMethod": "none" 33 | } 34 | ], 35 | [ 36 | { 37 | "userVerificationMethod": "passcode_external", 38 | "caDesc": { 39 | "base": 10, 40 | "minLength": 4, 41 | "maxRetries": 8 42 | } 43 | } 44 | ] 45 | ], 46 | "keyProtection": [ 47 | "hardware", 48 | "secure_element" 49 | ], 50 | "matcherProtection": [ 51 | "on_chip" 52 | ], 53 | "isFreshUserVerificationRequired": true, 54 | "cryptoStrength": 128, 55 | "attachmentHint": [ 56 | "external", 57 | "nfc", 58 | "wireless" 59 | ], 60 | "tcDisplay": [], 61 | "attestationRootCertificates": [ 62 | "MIIBPjCB5qADAgECAhRGVRN2Qi4Y97Q3vK0l5YB8CqNbVTAKBggqhkjOPQQDAjAgMQ0wCwYDVQQKDARBQ01FMQ8wDQYDVQQDDAZBdXRoQ0EwHhcNMjMwOTE1MTYwNDE0WhcNMzMwOTEzMTYwNDE0WjAgMQ0wCwYDVQQKDARBQ01FMQ8wDQYDVQQDDAZBdXRoQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATyn3WzUaMddA5MqLqf27YTS2izCw+HTX+vTUeImQkg9bPB67t4SswTEgT/VAQq7WgjGFRw0mk2Pu3nAf98wAjfMAoGCCqGSM49BAMCA0cAMEQCIESrgGpvqhNKAsHqZVWUYZxQRcss3nGwnFg827aZamMCAiA1vQUO0f5VAzLRccQK7/otfRBha/5wNUyK8QyF8pQcgA==" 63 | ], 64 | "authenticatorGetInfo": { 65 | "versions": [ 66 | "FIDO_2_0", 67 | "FIDO_2_1", 68 | "FIDO_2_1_PRE", 69 | "U2F_V2" 70 | ], 71 | "extensions": [ 72 | "uvm", 73 | "credBlob", 74 | "credProtect", 75 | "hmac-secret", 76 | "largeBlobKey", 77 | "minPinLength" 78 | ], 79 | "aaguid": "b6a13d017826af821b052b53adba1b4e", 80 | "options": { 81 | "rk": true, 82 | "up": false, 83 | "uvAcfg": true, 84 | "alwaysUv": false, 85 | "credMgmt": true, 86 | "authnrCfg": true, 87 | "clientPin": false, 88 | "largeBlobs": true, 89 | "pinUvAuthToken": true, 90 | "setMinPINLength": true, 91 | "makeCredUvNotRqd": true 92 | }, 93 | "pinUvAuthProtocols": [ 94 | 2, 95 | 1 96 | ], 97 | "maxCredentialCountInList": 23, 98 | "maxCredentialIdLength": 112, 99 | "algorithms": [ 100 | { 101 | "alg": -7, 102 | "type": "public-key" 103 | } 104 | ], 105 | "maxSerializedLargeBlobArray": 1024, 106 | "minPINLength": 4, 107 | "firmwareVersion": 1, 108 | "maxCredBlobLength": 32, 109 | "maxRPIDsForSetMinPINLength": 2, 110 | "uvModality": 512, 111 | "remainingDiscoverableCredentials": 100 112 | } 113 | } -------------------------------------------------------------------------------- /python_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJacobs/FIDO2Applet/0194107d9648577379058b59843504924b546514/python_tests/__init__.py -------------------------------------------------------------------------------- /python_tests/ctap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJacobs/FIDO2Applet/0194107d9648577379058b59843504924b546514/python_tests/ctap/__init__.py -------------------------------------------------------------------------------- /python_tests/ctap/test_auth_config.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import unittest 3 | from typing import Optional 4 | 5 | from fido2.ctap import CtapError 6 | from fido2.ctap2 import ClientPin, Config, PinProtocolV2 7 | 8 | from .ctap_test import CTAPTestCase 9 | 10 | 11 | class AuthenticatorConfigTestCase(CTAPTestCase): 12 | 13 | cp: ClientPin 14 | 15 | def setUp(self, install_params: Optional[bytes] = None) -> None: 16 | super().setUp(install_params=install_params) 17 | self.reset() 18 | 19 | def reset(self): 20 | super().reset() 21 | self.cp = ClientPin(self.ctap2) 22 | 23 | def test_alwaysUv_default_off(self): 24 | info = self.ctap2.get_info() 25 | self.assertFalse(info.options.get("alwaysUv")) 26 | self.assertFalse("U2F_V2" in info.versions) 27 | 28 | def test_alwaysUv_off_after_pin_set(self): 29 | self.cp.set_pin(secrets.token_hex(10)) 30 | 31 | info = self.ctap2.get_info() 32 | self.assertFalse(info.options.get("alwaysUv")) 33 | self.assertFalse("U2F_V2" in info.versions) 34 | 35 | def test_enable_alwaysUv(self): 36 | Config(self.ctap2).toggle_always_uv() 37 | 38 | info = self.ctap2.get_info() 39 | self.assertTrue(info.options.get("alwaysUv")) 40 | self.assertFalse(info.options.get("makeCredUvNotRequired")) 41 | self.assertFalse("U2F_V2" in info.versions) 42 | 43 | @unittest.skip("EP is disabled out of the box") 44 | def test_enable_enterprise_attestation(self): 45 | Config(self.ctap2).enable_enterprise_attestation() 46 | 47 | info = self.ctap2.get_info() 48 | self.assertTrue(info.options.get("ep")) 49 | 50 | def test_enterprise_attestation_accepted_when_enabled(self): 51 | Config(self.ctap2).enable_enterprise_attestation() 52 | 53 | self.basic_makecred_params["enterprise_attestation"] = 1 54 | 55 | self.ctap2.make_credential(**self.basic_makecred_params) 56 | 57 | def test_enterprise_attestation_rejects_invalid_value_when_enabled(self): 58 | Config(self.ctap2).enable_enterprise_attestation() 59 | 60 | self.basic_makecred_params["enterprise_attestation"] = 9 61 | with self.assertRaises(CtapError) as e: 62 | self.ctap2.make_credential(**self.basic_makecred_params) 63 | 64 | self.assertEqual(CtapError.ERR.INVALID_OPTION, e.exception.code) 65 | 66 | def test_enterprise_attestation_rejected_when_disabled(self): 67 | self.basic_makecred_params["enterprise_attestation"] = 9 68 | 69 | with self.assertRaises(CtapError) as e: 70 | self.ctap2.make_credential(**self.basic_makecred_params) 71 | 72 | self.assertEqual(CtapError.ERR.INVALID_PARAMETER, e.exception.code) 73 | 74 | def test_makecred_rejected_with_alwaysUv_no_pin(self): 75 | Config(self.ctap2).toggle_always_uv() 76 | 77 | with self.assertRaises(CtapError) as e: 78 | self.ctap2.make_credential(**self.basic_makecred_params) 79 | 80 | self.assertEqual(CtapError.ERR.PIN_NOT_SET, e.exception.code) 81 | 82 | def test_disable_alwaysUv_without_pin_rejected(self): 83 | Config(self.ctap2).toggle_always_uv() 84 | 85 | with self.assertRaises(CtapError) as e: 86 | Config(self.ctap2).toggle_always_uv() 87 | 88 | self.assertEqual(CtapError.ERR.PUAT_REQUIRED, e.exception.code) 89 | 90 | def test_toggling_alwaysUv_survives_soft_reset(self): 91 | Config(self.ctap2).toggle_always_uv() 92 | 93 | self.softResetCard() 94 | 95 | info = self.ctap2.get_info() 96 | self.assertTrue(info.options.get("alwaysUv")) 97 | 98 | def test_toggle_alwaysUv_without_acfg_perm(self): 99 | pin = secrets.token_hex(10) 100 | Config(self.ctap2).toggle_always_uv() 101 | self.cp.set_pin(pin) 102 | uv = self.cp.get_pin_token(pin) 103 | cfg = Config(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv) 104 | 105 | with self.assertRaises(CtapError) as e: 106 | cfg.toggle_always_uv() 107 | 108 | self.assertEqual(CtapError.ERR.PIN_AUTH_INVALID, e.exception.code) 109 | 110 | def test_toggle_alwaysUv_with_pin(self): 111 | pin = secrets.token_hex(10) 112 | Config(self.ctap2).toggle_always_uv() 113 | self.cp.set_pin(pin) 114 | uv = self.cp.get_pin_token(pin, 115 | permissions=ClientPin.PERMISSION.AUTHENTICATOR_CFG) 116 | cfg = Config(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv) 117 | cfg.toggle_always_uv() 118 | 119 | self.test_alwaysUv_default_off() 120 | -------------------------------------------------------------------------------- /python_tests/ctap/test_cred_blob.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | from .ctap_test import CTAPTestCase 4 | 5 | 6 | class CredBlobTestCase(CTAPTestCase): 7 | 8 | def store_cred_blob(self, blob: bytes): 9 | return self.ctap2.make_credential(**self.basic_makecred_params, 10 | options={ 11 | "rk": True 12 | }, 13 | extensions={ 14 | "credBlob": blob 15 | }) 16 | 17 | def test_cred_blob_fails_on_non_rk(self): 18 | blob = secrets.token_bytes(32) 19 | 20 | res = self.ctap2.make_credential(**self.basic_makecred_params, 21 | extensions={ 22 | "credBlob": blob 23 | }) 24 | self.assertEqual({ 25 | "credBlob": False 26 | }, res.auth_data.extensions) 27 | 28 | def test_cred_blob_fails_on_long_blob(self): 29 | blob = secrets.token_bytes(33) 30 | 31 | res = self.store_cred_blob(blob) 32 | 33 | self.assertEqual({ 34 | "credBlob": False 35 | }, res.auth_data.extensions) 36 | 37 | def test_cred_blob_make_credential(self): 38 | blob = secrets.token_bytes(32) 39 | 40 | res = self.store_cred_blob(blob) 41 | 42 | pubkey = res.auth_data.credential_data.public_key 43 | pubkey.verify(res.auth_data + self.client_data, res.att_stmt['sig']) 44 | self.assertEqual({ 45 | "credBlob": True 46 | }, res.auth_data.extensions) 47 | 48 | def test_cred_blob_retrieval(self): 49 | blob = secrets.token_bytes(32) 50 | self.store_cred_blob(blob) 51 | 52 | self.softResetCard() 53 | 54 | res = self.get_assertion_from_cred(cred=None, rp_id=self.rp_id, extensions={"credBlob": True}) 55 | 56 | self.assertEqual({ 57 | "credBlob": blob 58 | }, res.auth_data.extensions) 59 | 60 | def test_retrieving_short_cred(self): 61 | blob = secrets.token_bytes(3) 62 | self.store_cred_blob(blob) 63 | 64 | res = self.get_assertion_from_cred(cred=None, rp_id=self.rp_id, extensions={"credBlob": True}) 65 | 66 | self.assertEqual({ 67 | "credBlob": blob 68 | }, res.auth_data.extensions) 69 | -------------------------------------------------------------------------------- /python_tests/ctap/test_cred_mgmt.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | from fido2.ctap2.extensions import CredProtectExtension 4 | from fido2.webauthn import ResidentKeyRequirement, PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor 5 | from parameterized import parameterized 6 | 7 | from ctap.ctap_test import CredManagementBaseTestCase, FixedPinUserInteraction 8 | 9 | 10 | class CredManagementTestCase(CredManagementBaseTestCase): 11 | @parameterized.expand([ 12 | ("low", CredProtectExtension.POLICY.OPTIONAL), 13 | ("medium", CredProtectExtension.POLICY.OPTIONAL_WITH_LIST), 14 | ("high", CredProtectExtension.POLICY.REQUIRED), 15 | ]) 16 | def test_deleting_rk(self, _, policy): 17 | client = self.get_high_level_client(extensions=[CredProtectExtension], 18 | user_interaction=FixedPinUserInteraction(self.pin)) 19 | resident_key = ResidentKeyRequirement.REQUIRED 20 | 21 | dcs_before = self.ctap2.get_info().remaining_disc_creds 22 | res = client.make_credential(options=self.get_high_level_make_cred_options( 23 | resident_key, 24 | { 25 | "credentialProtectionPolicy": policy 26 | } 27 | )) 28 | dcs_after_creation = self.ctap2.get_info().remaining_disc_creds 29 | cm = self.get_credential_management() 30 | cm.delete_cred(self.get_descriptor_from_cred_id( 31 | res.attestation_object.auth_data.credential_data.credential_id 32 | )) 33 | dcs_after_deletion = self.ctap2.get_info().remaining_disc_creds 34 | 35 | self.assertLessEqual(dcs_before - 1, dcs_after_creation) 36 | self.assertEqual(dcs_before, dcs_after_deletion) 37 | 38 | def test_deleting_one_rk_for_rp(self): 39 | rp_id = 'a' 40 | client = self.get_high_level_client(extensions=[CredProtectExtension], 41 | user_interaction=FixedPinUserInteraction(self.pin), 42 | origin = 'https://' + rp_id) 43 | resident_key = ResidentKeyRequirement.REQUIRED 44 | 45 | cm = self.get_credential_management() 46 | 47 | rps = cm.enumerate_rps() 48 | self.assertEqual(0, len(rps)) 49 | user_id_1 = b'abc' 50 | user_id_2 = b'def' 51 | 52 | cred1 = client.make_credential(options=self.get_high_level_make_cred_options( 53 | resident_key, 54 | { 55 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 56 | }, 57 | rp_id=rp_id, 58 | user_id=user_id_1 59 | )) 60 | cred2 = client.make_credential(options=self.get_high_level_make_cred_options( 61 | resident_key, 62 | { 63 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 64 | }, 65 | rp_id=rp_id, 66 | user_id=user_id_2 67 | )) 68 | self.assertNotEqual( 69 | cred1.attestation_object.auth_data.credential_data.credential_id, 70 | cred2.attestation_object.auth_data.credential_data.credential_id, 71 | ) 72 | cm = self.get_credential_management() 73 | rps = cm.enumerate_rps() 74 | self.assertEqual(1, len(rps)) 75 | 76 | for rp in rps: 77 | self.assertEqual(rp_id, rp[3]['id']) 78 | self.assertEqual(self.rp_id_hash(rp_id).hex(), rp[4].hex()) 79 | 80 | creds = cm.enumerate_creds(rp_id_hash=self.rp_id_hash(rp_id)) 81 | self.assertEqual(2, len(creds)) 82 | 83 | cm.delete_cred(self.get_descriptor_from_cred_id( 84 | cred1.attestation_object.auth_data.credential_data.credential_id 85 | )) 86 | 87 | cm = self.get_credential_management() 88 | rps = cm.enumerate_rps() 89 | self.assertEqual(1, len(rps)) 90 | 91 | def test_creating_many_rks(self): 92 | client = self.get_high_level_client(extensions=[CredProtectExtension], 93 | user_interaction=FixedPinUserInteraction(self.pin)) 94 | client._verify_rp_id = lambda x: True 95 | resident_key = ResidentKeyRequirement.REQUIRED 96 | first_cred = client.make_credential(options=self.get_high_level_make_cred_options( 97 | resident_key 98 | )) 99 | for x in range(100): 100 | rp_id = secrets.token_hex(20) 101 | client.make_credential(options=self.get_high_level_make_cred_options( 102 | resident_key, rp_id=rp_id 103 | )) 104 | 105 | res = client.get_assertion(self.get_high_level_assertion_opts_from_cred(cred=None, rp_id=self.rp_id)) 106 | assertions = res.get_assertions() 107 | self.assertEqual(1, len(assertions)) 108 | self.assertEqual(res.get_response(0).credential_id, 109 | first_cred.attestation_object.auth_data.credential_data.credential_id) 110 | 111 | def test_enumerating_mixed_security_creds(self): 112 | pin_client = self.get_high_level_client(extensions=[CredProtectExtension], 113 | user_interaction=FixedPinUserInteraction(self.pin)) 114 | resident_key = ResidentKeyRequirement.REQUIRED 115 | hs_cred = pin_client.make_credential(options=self.get_high_level_make_cred_options( 116 | resident_key, 117 | { 118 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 119 | } 120 | )) 121 | other_rp = secrets.token_hex(18) 122 | pin_client_other_suffix = self.get_high_level_client(extensions=[CredProtectExtension], 123 | user_interaction=FixedPinUserInteraction(self.pin), 124 | origin='https://' + other_rp) 125 | other_hs_cred = pin_client_other_suffix.make_credential(options=self.get_high_level_make_cred_options( 126 | resident_key, 127 | { 128 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 129 | }, 130 | rp_id=other_rp 131 | )) 132 | self.softResetCard() 133 | self.basic_makecred_params['options'] = {'rk': True} 134 | self.basic_makecred_params['user']['id'] = secrets.token_bytes(20) 135 | ls_cred = self.ctap2.make_credential(**self.basic_makecred_params) 136 | 137 | rps = self.get_credential_management().enumerate_rps() 138 | self.assertEqual(2, len(rps)) 139 | 140 | creds = self.get_credential_management().enumerate_creds(rp_id_hash=self.rp_id_hash(self.rp_id)) 141 | other_creds = self.get_credential_management().enumerate_creds(rp_id_hash=self.rp_id_hash(other_rp)) 142 | 143 | self.assertEqual(1, len(other_creds)) 144 | self.assertEqual(2, len(creds)) 145 | 146 | cred_levels = [x[10] for x in creds] 147 | cred_ids = [x[7]['id'] for x in creds] 148 | self.assertEqual([1, 3], sorted(cred_levels)) 149 | self.assertEqual(sorted([ls_cred.auth_data.credential_data.credential_id, 150 | hs_cred.attestation_object.auth_data.credential_data.credential_id]), 151 | sorted(cred_ids)) 152 | 153 | self.assertEqual(3, other_creds[0][10]) 154 | self.assertEqual(other_hs_cred.attestation_object.auth_data.credential_data.credential_id, 155 | other_creds[0][7]['id']) 156 | 157 | def test_updating_user(self): 158 | pin_client = self.get_high_level_client(user_interaction=FixedPinUserInteraction(self.pin)) 159 | cred = pin_client.make_credential(options=self.get_high_level_make_cred_options( 160 | ResidentKeyRequirement.REQUIRED 161 | )) 162 | cm = self.get_credential_management() 163 | new_id = secrets.token_bytes(64) 164 | new_name = "Frooby Bobble" 165 | 166 | cm.update_user_info(cred_id=PublicKeyCredentialDescriptor( 167 | type='public-key', 168 | id=cred.attestation_object.auth_data.credential_data.credential_id 169 | ), user_info=PublicKeyCredentialUserEntity( 170 | id=new_id, 171 | name=new_name, 172 | display_name='Some very long stuff that makes this inconvenient to work with' 173 | )) 174 | 175 | cm = self.get_credential_management() 176 | after_cred = cm.enumerate_creds(rp_id_hash=self.rp_id_hash(self.rp_id))[0] 177 | self.assertEqual(new_id, after_cred[6].get('id')) 178 | self.assertEqual(new_name, after_cred[6].get('name')) 179 | -------------------------------------------------------------------------------- /python_tests/ctap/test_ctap_attestation_mode_switch.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | 4 | from fido2.ctap2.base import args 5 | from fido2.webauthn import Aaguid 6 | from parameterized import parameterized 7 | 8 | from ctap.ctap_test import BasicAttestationTestCase 9 | 10 | 11 | class AttestationModeSwitchTestCase(BasicAttestationTestCase): 12 | def setUp(self, install_params: Optional[bytes] = None) -> None: 13 | super().setUp(bytes([0xA1, 0x00, 0xF5])) 14 | 15 | @parameterized.expand([ 16 | ("tiny", 3), 17 | ("very short", 140), 18 | ("short", 300), 19 | ("medium", 900), 20 | ("long", 2000), 21 | ("very long", 8000), 22 | ]) 23 | def test_applying_cert_len(self, _, length): 24 | info_before = self.ctap2.get_info() 25 | self.assertEqual(Aaguid.NONE, info_before.aaguid) 26 | 27 | cert_bytes = secrets.token_bytes(length) 28 | cert = self.gen_attestation_cert([cert_bytes]) 29 | 30 | self.ctap2.send_cbor( 31 | self.VENDOR_COMMAND_SWITCH_ATT, 32 | args(cert) 33 | ) 34 | 35 | info_after = self.ctap2.get_info() 36 | self.assertEqual(self.aaguid, info_after.aaguid) 37 | 38 | cred_res = self.ctap2.make_credential(**self.basic_makecred_params) 39 | self.assertEqual(self.cert, cred_res.att_stmt['x5c'][0]) 40 | self.assertIsNone(cred_res.large_blob_key) 41 | 42 | @parameterized.expand([ 43 | ("tiny", 3), 44 | ("very short", 140), 45 | ("short", 300), 46 | ("medium", 900), 47 | ("long", 2000), 48 | ("very long", 8000), 49 | ]) 50 | def test_applying_cert_len_with_large_blob(self, _, length): 51 | info_before = self.ctap2.get_info() 52 | self.assertEqual(Aaguid.NONE, info_before.aaguid) 53 | 54 | cert_bytes = secrets.token_bytes(length) 55 | cert = self.gen_attestation_cert([cert_bytes]) 56 | 57 | self.ctap2.send_cbor( 58 | self.VENDOR_COMMAND_SWITCH_ATT, 59 | args(cert) 60 | ) 61 | 62 | info_after = self.ctap2.get_info() 63 | self.assertEqual(self.aaguid, info_after.aaguid) 64 | 65 | self.basic_makecred_params['options'] = {'rk': True} 66 | self.basic_makecred_params['extensions'] = {'largeBlobKey': True} 67 | cred_res = self.ctap2.make_credential(**self.basic_makecred_params) 68 | self.assertEqual(self.cert, cred_res.att_stmt['x5c'][0]) 69 | self.assertIsNotNone(cred_res.large_blob_key) 70 | self.assertEqual(32, len(cred_res.large_blob_key)) 71 | 72 | def test_u2f_supported_after_switch(self): 73 | info_before = self.ctap2.get_info() 74 | 75 | cert_bytes = secrets.token_bytes(100) 76 | cert = self.gen_attestation_cert([cert_bytes]) 77 | self.ctap2.send_cbor( 78 | self.VENDOR_COMMAND_SWITCH_ATT, 79 | args(cert) 80 | ) 81 | 82 | info_after = self.ctap2.get_info() 83 | self.assertFalse("U2F_V2" in info_before.versions) 84 | self.assertTrue("U2F_V2" in info_after.versions) 85 | 86 | def test_switching_survives_soft_reset(self): 87 | cert_bytes = secrets.token_bytes(100) 88 | cert = self.gen_attestation_cert([cert_bytes]) 89 | self.ctap2.send_cbor( 90 | self.VENDOR_COMMAND_SWITCH_ATT, 91 | args(cert) 92 | ) 93 | 94 | self.softResetCard() 95 | 96 | info = self.ctap2.get_info() 97 | self.assertTrue("U2F_V2" in info.versions) 98 | -------------------------------------------------------------------------------- /python_tests/ctap/test_ctap_attestation_mode_switch_with_fixed_key.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | from cryptography.hazmat.primitives.asymmetric import ec 4 | 5 | from fido2.ctap2.base import args 6 | 7 | from ctap.ctap_test import BasicAttestationTestCase 8 | 9 | 10 | class AttestationModeSwitchWithFixedKeyTestCase(BasicAttestationTestCase): 11 | def setUp(self, install_params: Optional[bytes] = None) -> None: 12 | 13 | privkey = ec.generate_private_key(ec.SECP256R1()) 14 | self.public_key = privkey.public_key() 15 | private_bytes = privkey.private_numbers().private_value.to_bytes(length=32, byteorder='big') 16 | 17 | super().setUp(bytes([0xA2, 0x00, 0xF5, 0x0F, 0x58, 0x20]) + private_bytes) 18 | 19 | def test_u2f_supported_after_switch(self): 20 | info_before = self.ctap2.get_info() 21 | 22 | cert_bytes = secrets.token_bytes(100) 23 | cert = self.gen_attestation_cert(cert_bytes=[cert_bytes], public_key=self.public_key) 24 | self.ctap2.send_cbor( 25 | self.VENDOR_COMMAND_SWITCH_ATT, 26 | args(cert) 27 | ) 28 | 29 | info_after = self.ctap2.get_info() 30 | self.assertFalse("U2F_V2" in info_before.versions) 31 | self.assertTrue("U2F_V2" in info_after.versions) 32 | -------------------------------------------------------------------------------- /python_tests/ctap/test_ctap_basic_attestation.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from cryptography import x509 4 | from cryptography.exceptions import InvalidSignature 5 | from cryptography.hazmat.primitives.asymmetric.ec import ECDSA 6 | from cryptography.hazmat.primitives.hashes import SHA256 7 | from fido2.cose import ES256 8 | 9 | from .ctap_test import BasicAttestationTestCase 10 | 11 | 12 | class CTAPBasicAttestationTestCase(BasicAttestationTestCase): 13 | 14 | def setUp(self, install_params: Optional[bytes] = None) -> None: 15 | super().setUp(install_params) 16 | self.install_attestation_cert() 17 | 18 | def test_aaguid_visible_in_info(self): 19 | info = self.ctap2.get_info() 20 | self.assertEqual(self.aaguid, info.aaguid) 21 | 22 | def test_cert_attestation(self): 23 | cred_res = self.ctap2.make_credential(**self.basic_makecred_params) 24 | self.assertEqual("packed", cred_res.fmt) 25 | self.assertIsNotNone(cred_res.att_stmt) 26 | self.assertEqual(self.aaguid, cred_res.auth_data.credential_data.aaguid) 27 | x5c = cred_res.att_stmt['x5c'] 28 | self.assertEqual(2, len(x5c)) 29 | auth_cert, ca_cert = x5c 30 | self.assertEqual(self.cert, auth_cert) 31 | self.assertEqual(ES256.ALGORITHM, cred_res.att_stmt['alg']) 32 | sig = cred_res.att_stmt.get('sig') 33 | self.assertIsNotNone(sig) 34 | 35 | cred_pubkey = cred_res.auth_data.credential_data.public_key 36 | with self.assertRaises(InvalidSignature): 37 | cred_pubkey.verify(cred_res.auth_data + self.client_data, sig) 38 | self.public_key.verify(sig, 39 | cred_res.auth_data + self.client_data, 40 | ECDSA(SHA256())) 41 | auth_cert_der = x509.load_der_x509_certificate(x5c[0]) 42 | ca_cert_der = x509.load_der_x509_certificate(x5c[1]) 43 | auth_cert_der.verify_directly_issued_by(ca_cert_der) 44 | ca_cert_der.verify_directly_issued_by(ca_cert_der) 45 | self.assertEqual(self.public_key, auth_cert_der.public_key()) 46 | self.assertEqual(self.ca_public_key, ca_cert_der.public_key()) 47 | 48 | assert_client_data = self.get_random_client_data() 49 | 50 | assert_res = self.get_assertion_from_cred(cred_res, client_data=assert_client_data) 51 | self.assertIsNone(assert_res.number_of_credentials) 52 | self.assertIsNone(assert_res.user) 53 | self.assertEqual(cred_res.auth_data.credential_data.credential_id, 54 | assert_res.credential['id']) 55 | 56 | cred_res.auth_data.credential_data.public_key.verify( 57 | assert_res.auth_data + assert_client_data, assert_res.signature 58 | ) 59 | 60 | with self.assertRaises(InvalidSignature): 61 | self.public_key.verify(assert_res.signature, 62 | assert_res.auth_data + assert_client_data, 63 | ECDSA(SHA256())) 64 | with self.assertRaises(InvalidSignature): 65 | self.ca_public_key.verify(assert_res.signature, 66 | assert_res.auth_data + assert_client_data, 67 | ECDSA(SHA256())) 68 | -------------------------------------------------------------------------------- /python_tests/ctap/test_ctap_locked_self_attestation.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | 4 | from fido2.ctap import CtapError 5 | from fido2.ctap2.base import args 6 | from fido2.webauthn import Aaguid 7 | 8 | from ctap.ctap_test import CTAPTestCase 9 | 10 | 11 | class LockedSelfAttestationTestCase(CTAPTestCase): 12 | def setUp(self, install_params: Optional[bytes] = None) -> None: 13 | super().setUp(bytes()) 14 | 15 | def test_switching_attestation_modes_disallowed(self): 16 | info = self.ctap2.get_info() 17 | self.assertEqual(Aaguid.NONE, info.aaguid) 18 | with self.assertRaises(CtapError) as e: 19 | self.ctap2.send_cbor( 20 | 0x46, 21 | args(secrets.token_bytes(122)) 22 | ) 23 | self.assertEqual(CtapError.ERR.NOT_ALLOWED, e.exception.code) 24 | 25 | -------------------------------------------------------------------------------- /python_tests/ctap/test_ctap_pins.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | 4 | from fido2.client import ClientError, PinRequiredError 5 | from fido2.ctap import CtapError 6 | from fido2.ctap2 import ClientPin, PinProtocolV1, PinProtocolV2 7 | from fido2.webauthn import UserVerificationRequirement 8 | from parameterized import parameterized 9 | 10 | from .ctap_test import CTAPTestCase, FixedPinUserInteraction 11 | 12 | 13 | class CTAPPINTestCase(CTAPTestCase): 14 | 15 | cp: ClientPin 16 | 17 | def setUp(self, install_params: Optional[bytes] = None) -> None: 18 | super().setUp(install_params=install_params) 19 | self.reset() 20 | 21 | def reset(self): 22 | super().reset() 23 | self.cp = ClientPin(self.ctap2) 24 | 25 | def test_pin_change(self): 26 | first_pin = secrets.token_hex(16) 27 | second_pin = secrets.token_hex(16) 28 | old_pin_client = self.get_high_level_client(user_interaction=FixedPinUserInteraction(first_pin)) 29 | new_pin_client = self.get_high_level_client(user_interaction=FixedPinUserInteraction(second_pin)) 30 | 31 | info_before_switch = self.ctap2.get_info() 32 | self.cp.set_pin(first_pin) 33 | info_after_switch = self.ctap2.get_info() 34 | self.cp.change_pin(first_pin, second_pin) 35 | with self.assertRaises(ClientError) as e: 36 | # Old PIN is now wrong 37 | old_pin_client.make_credential(self.get_high_level_make_cred_options( 38 | user_verification=UserVerificationRequirement.REQUIRED 39 | )) 40 | 41 | # New PIN is correct 42 | new_pin_client.make_credential(self.get_high_level_make_cred_options( 43 | user_verification=UserVerificationRequirement.REQUIRED 44 | )) 45 | 46 | self.assertFalse(info_before_switch.options['clientPin']) 47 | self.assertTrue(info_after_switch.options['clientPin']) 48 | self.assertEqual([2, 1], info_before_switch.pin_uv_protocols) 49 | self.assertEqual([2, 1], info_after_switch.pin_uv_protocols) 50 | 51 | def test_pin_cleared_by_reset(self): 52 | first_pin = secrets.token_hex(16) 53 | 54 | self.cp.set_pin(first_pin) 55 | self.reset() 56 | info_after_reset = self.ctap2.get_info() 57 | with self.assertRaises(CtapError) as e: 58 | self.cp.change_pin(old_pin=first_pin, new_pin=secrets.token_hex(10)) 59 | 60 | self.assertFalse(info_after_reset.options['clientPin']) 61 | self.assertEqual(CtapError.ERR.PIN_NOT_SET, e.exception.code) 62 | 63 | def test_cannot_set_pin_twice(self): 64 | first_pin = secrets.token_hex(16) 65 | self.cp.set_pin(first_pin) 66 | 67 | with self.assertRaises(CtapError) as e: 68 | self.cp.set_pin(first_pin) 69 | 70 | self.assertEqual(CtapError.ERR.PUAT_REQUIRED, e.exception.code) 71 | 72 | def test_pin_change_providing_incorrect_old_pin(self): 73 | first_pin = secrets.token_hex(16) 74 | second_pin = secrets.token_hex(16) 75 | self.cp.set_pin(first_pin) 76 | 77 | with self.assertRaises(CtapError) as e: 78 | self.cp.change_pin("12345", second_pin) 79 | 80 | self.assertEqual(CtapError.ERR.PIN_INVALID, e.exception.code) 81 | 82 | def test_eight_retries_reported(self): 83 | info = self.cp.get_pin_retries() 84 | 85 | self.assertEqual((8, False), info) 86 | 87 | def test_wrong_pin_decrements_retry_count_across_soft_reset(self): 88 | first_pin = secrets.token_hex(16) 89 | second_pin = secrets.token_hex(16) 90 | self.cp.set_pin(first_pin) 91 | with self.assertRaises(CtapError) as e: 92 | self.cp.change_pin("12345", second_pin) 93 | self.assertEqual(CtapError.ERR.PIN_INVALID, e.exception.code) 94 | 95 | before_reset = self.cp.get_pin_retries() 96 | self.softResetCard() 97 | after_reset = self.cp.get_pin_retries() 98 | 99 | self.assertEqual((7, False), before_reset) 100 | self.assertEqual((7, False), after_reset) 101 | 102 | def test_uv_not_supported(self): 103 | pin = secrets.token_hex(10) 104 | self.cp.set_pin(pin) 105 | 106 | with self.assertRaises(CtapError) as e: 107 | self.cp.get_uv_token() 108 | 109 | self.assertEqual(CtapError.ERR.NOT_ALLOWED, e.exception.code) 110 | 111 | @parameterized.expand([ 112 | ("short", 2, 1, False), 113 | ("minimal", 2, 2, True), 114 | ("reasonable", 2, 8, True), 115 | ("reasonable", 1, 8, True), 116 | ("maximal", 2, 31, True), 117 | ("overlong", 2, 32, False), 118 | ("huge", 2, 40, False), 119 | ("huge", 1, 40, False), 120 | ]) 121 | def test_pin_lengths(self, _, protocol, length, valid): 122 | pin = secrets.token_hex(length) 123 | 124 | def do_client_pin(): 125 | pin_as_bytes = pin.encode() 126 | while len(pin_as_bytes) < 64: 127 | pin_as_bytes += b'\0' 128 | 129 | if protocol == 2: 130 | cp = ClientPin(self.ctap2, PinProtocolV2()) 131 | else: 132 | cp = ClientPin(self.ctap2, PinProtocolV1()) 133 | ka, ss = cp._get_shared_secret() 134 | enc = cp.protocol.encrypt(ss, pin_as_bytes) 135 | puv = cp.protocol.authenticate(ss, enc) 136 | self.ctap2.client_pin( 137 | protocol, 138 | ClientPin.CMD.SET_PIN, 139 | key_agreement=ka, 140 | new_pin_enc=enc, 141 | pin_uv_param=puv 142 | ) 143 | 144 | if valid: 145 | do_client_pin() 146 | else: 147 | with self.assertRaises(CtapError) as e: 148 | do_client_pin() 149 | 150 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 151 | 152 | def test_pin_set_and_not_provided_library_level(self): 153 | pin = secrets.token_hex(30) 154 | ClientPin(self.ctap2).set_pin(pin) 155 | client = self.get_high_level_client() 156 | 157 | with self.assertRaises(PinRequiredError): 158 | client.make_credential(options=self.get_high_level_make_cred_options( 159 | user_verification=UserVerificationRequirement.REQUIRED 160 | )) 161 | -------------------------------------------------------------------------------- /python_tests/ctap/test_ctap_resets.py: -------------------------------------------------------------------------------- 1 | from fido2.client import ClientError 2 | from fido2.ctap import CtapError 3 | from fido2.webauthn import ResidentKeyRequirement 4 | from parameterized import parameterized 5 | 6 | from ctap.ctap_test import CTAPTestCase 7 | 8 | 9 | class ResetTestCase(CTAPTestCase): 10 | 11 | @parameterized.expand([ 12 | ("resident", True), 13 | ("nonresident", True), 14 | ]) 15 | def test_reset_invalidates_creds(self, _, resident: bool): 16 | resident_key = ResidentKeyRequirement.REQUIRED if resident else ResidentKeyRequirement.DISCOURAGED 17 | client = self.get_high_level_client() 18 | 19 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 20 | resident_key 21 | )) 22 | 23 | self.reset() 24 | 25 | with self.assertRaises(ClientError) as e: 26 | opts = self.get_high_level_assertion_opts_from_cred(None if resident else cred) 27 | client.get_assertion(options=opts) 28 | 29 | self.assertEqual(CtapError.ERR.NO_CREDENTIALS, e.exception.cause.code) 30 | 31 | @parameterized.expand([ 32 | ("resident", True), 33 | ("nonresident", True), 34 | ]) 35 | def creds_intact_after_power_cycle(self, _, resident: bool): 36 | self.softResetCard() 37 | 38 | resident_key = ResidentKeyRequirement.REQUIRED if resident else ResidentKeyRequirement.DISCOURAGED 39 | client = self.get_high_level_client() 40 | 41 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 42 | resident_key 43 | )) 44 | 45 | self.softResetCard() 46 | 47 | opts = self.get_high_level_assertion_opts_from_cred(None if resident else cred) 48 | client.get_assertion(options=opts) 49 | -------------------------------------------------------------------------------- /python_tests/ctap/test_extended_apdus.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | 4 | from fido2.ctap2.base import args 5 | from parameterized import parameterized 6 | 7 | from ctap.ctap_test import BasicAttestationTestCase 8 | 9 | 10 | class ExtendedAPDUTestCase(BasicAttestationTestCase): 11 | 12 | @classmethod 13 | def setUpClass(cls) -> None: 14 | cls.USE_EXT_APDU = True 15 | super().setUpClass() 16 | 17 | def setUp(self, install_params: Optional[bytes] = None) -> None: 18 | super().setUp(bytes([0xA1, 0x00, 0xF5])) 19 | 20 | def test_get_info(self): 21 | info = self.ctap2.get_info() 22 | self.assertEqual(bytes([0] * 16), info.aaguid) 23 | 24 | def test_makecred(self): 25 | res = self.ctap2.make_credential(**self.basic_makecred_params) 26 | self.assertTrue(res.auth_data.counter < 16) 27 | self.assertTrue(res.auth_data.counter > 0) 28 | 29 | def test_extreme_makecred_input(self): 30 | self.basic_makecred_params['user'] = { 31 | 'id': secrets.token_bytes(32), 32 | 'name': secrets.token_hex(10), 33 | 'display_name': secrets.token_hex(100), 34 | 'icon': secrets.token_hex(120) 35 | } 36 | self.basic_makecred_params['options'] = { 37 | 'rk': True 38 | } 39 | self.ctap2.make_credential(**self.basic_makecred_params) 40 | 41 | def test_chained_assertions(self): 42 | self.basic_makecred_params['options'] = {'rk': True} 43 | num_creds = 5 44 | 45 | users = set() 46 | 47 | for x in range(num_creds): 48 | user = secrets.token_bytes(32) 49 | users.add(user) 50 | self.basic_makecred_params['user']['id'] = user 51 | self.ctap2.make_credential(**self.basic_makecred_params) 52 | 53 | asserts = self.get_assertion(rp_id=self.rp_id) 54 | self.assertEqual(num_creds, asserts.number_of_credentials) 55 | 56 | next_cred = self.ctap2.get_next_assertion() 57 | self.assertTrue(next_cred.user.get('id') in users) 58 | 59 | @parameterized.expand([ 60 | ("short", 100), 61 | ("medium", 220), 62 | ("long", 900), 63 | ("xlong", 5000) 64 | ]) 65 | def test_basic_auth(self, _, length): 66 | cert_bytes = secrets.token_bytes(length) 67 | cert = self.gen_attestation_cert([cert_bytes]) 68 | self.ctap2.send_cbor( 69 | self.VENDOR_COMMAND_SWITCH_ATT, 70 | args(cert) 71 | ) 72 | cred = self.ctap2.make_credential(**self.basic_makecred_params) 73 | self.assertEqual(cert_bytes, cred.att_stmt.get("x5c")[0]) 74 | -------------------------------------------------------------------------------- /python_tests/ctap/test_hmac_secret.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | from parameterized import parameterized 4 | from fido2.client import UserInteraction 5 | from fido2.ctap2 import ClientPin 6 | from fido2.ctap2.extensions import HmacSecretExtension, CredBlobExtension 7 | from fido2.webauthn import ResidentKeyRequirement, AuthenticatorAssertionResponse, UserVerificationRequirement 8 | 9 | from .ctap_test import CTAPTestCase, FixedPinUserInteraction 10 | 11 | 12 | class HMACSecretTestCase(CTAPTestCase): 13 | 14 | def test_hmac_secret_make_credential(self): 15 | res = self.ctap2.make_credential(**self.basic_makecred_params, 16 | extensions={ 17 | "hmac-secret": True 18 | }) 19 | 20 | pubkey = res.auth_data.credential_data.public_key 21 | pubkey.verify(res.auth_data + self.client_data, res.att_stmt['sig']) 22 | self.assertEqual({ 23 | "hmac-secret": True 24 | }, res.auth_data.extensions) 25 | 26 | def get_hmacs_from_result(self, assertion: AuthenticatorAssertionResponse) -> tuple[str, str]: 27 | return (assertion.extension_results['hmacGetSecret'].get('output1'), 28 | assertion.extension_results['hmacGetSecret'].get('output2')) 29 | 30 | def test_hmac_and_credblob_together(self): 31 | blob = secrets.token_bytes(32) 32 | client = self.get_high_level_client(extensions=[HmacSecretExtension, CredBlobExtension]) 33 | hmac_only_client = self.get_high_level_client(extensions=[HmacSecretExtension]) 34 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 35 | resident_key=ResidentKeyRequirement.REQUIRED, 36 | extensions={ 37 | "hmacCreateSecret": True, 38 | "credBlob": blob 39 | } 40 | )) 41 | salt1 = secrets.token_bytes(32) 42 | salt2 = secrets.token_bytes(32) 43 | hmac_alone_assertion = hmac_only_client.get_assertion( 44 | self.get_high_level_assertion_opts_from_cred(cred, 45 | extensions={ 46 | "hmacGetSecret": { 47 | "salt1": salt1, 48 | "salt2": salt2, 49 | } 50 | }) 51 | ) 52 | self.assertIsNone(hmac_alone_assertion.get_response(0).authenticator_data.extensions.get("credBlob")) 53 | correct_hm1, correct_hm2 = self.get_hmacs_from_result(hmac_alone_assertion.get_response(0)) 54 | 55 | assertion = client.get_assertion( 56 | self.get_high_level_assertion_opts_from_cred(cred, 57 | extensions={ 58 | "hmacGetSecret": { 59 | "salt1": salt1, 60 | "salt2": salt2, 61 | }, 62 | "getCredBlob": True 63 | }) 64 | ) 65 | 66 | hm1, hm2 = self.get_hmacs_from_result(assertion.get_response(0)) 67 | self.assertEqual(correct_hm1, hm1) 68 | self.assertEqual(correct_hm2, hm2) 69 | self.assertEqual(blob, assertion.get_response(0).authenticator_data.extensions.get("credBlob")) 70 | 71 | def test_uv_and_non_uv_yield_different_values(self): 72 | no_pin_client = self.get_high_level_client(extensions=[HmacSecretExtension]) 73 | cred = no_pin_client.make_credential(options=self.get_high_level_make_cred_options( 74 | extensions={ 75 | "hmacCreateSecret": True 76 | } 77 | )) 78 | salt1 = secrets.token_bytes(32) 79 | salt2 = secrets.token_bytes(32) 80 | 81 | assertion_before = no_pin_client.get_assertion( 82 | self.get_high_level_assertion_opts_from_cred(cred, 83 | extensions={ 84 | "hmacGetSecret": { 85 | "salt1": salt1, 86 | "salt2": salt2, 87 | } 88 | }) 89 | ) 90 | 91 | pin = secrets.token_hex(30) 92 | ClientPin(self.ctap2).set_pin(pin) 93 | pin_client = self.get_high_level_client(extensions=[HmacSecretExtension], 94 | user_interaction=FixedPinUserInteraction(pin)) 95 | assertion_after = pin_client.get_assertion( 96 | self.get_high_level_assertion_opts_from_cred(cred, 97 | user_verification=UserVerificationRequirement.REQUIRED, 98 | extensions={ 99 | "hmacGetSecret": { 100 | "salt1": salt1, 101 | "salt2": salt2, 102 | } 103 | }) 104 | ) 105 | 106 | before1, before2 = self.get_hmacs_from_result(assertion_before.get_response(0)) 107 | after1, after2 = self.get_hmacs_from_result(assertion_after.get_response(0)) 108 | 109 | self.assertNotEqual(before1, after1) 110 | self.assertNotEqual(before1, before2) 111 | self.assertNotEqual(before2, after2) 112 | self.assertNotEqual(after1, after2) 113 | 114 | @parameterized.expand([ 115 | ("nonresident+nopin", False, False), 116 | ("resident+nopin", True, False), 117 | ("nonresident+pin", False, True), 118 | ("resident+pin", True, True), 119 | ]) 120 | def test_hmac_secret_usage(self, _, resident, pin_set): 121 | resident_key = ResidentKeyRequirement.REQUIRED if resident else ResidentKeyRequirement.DISCOURAGED 122 | 123 | user_interaction = UserInteraction() 124 | if pin_set: 125 | pin = secrets.token_hex(30) 126 | user_interaction = FixedPinUserInteraction(pin) 127 | ClientPin(self.ctap2).set_pin(pin) 128 | 129 | client = self.get_high_level_client(extensions=[HmacSecretExtension], 130 | user_interaction=user_interaction) 131 | 132 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 133 | resident_key, 134 | { 135 | "hmacCreateSecret": True 136 | } 137 | )) 138 | self.assertEqual({"hmacCreateSecret": True}, cred.extension_results) 139 | 140 | assert_client_data = self.get_random_client_data() 141 | 142 | def get_assertion(given_salt): 143 | opts = self.get_high_level_assertion_opts_from_cred(None if resident else cred, 144 | client_data=assert_client_data, rp_id=self.rp_id, 145 | extensions={ 146 | "hmacGetSecret": { 147 | "salt1": given_salt 148 | } 149 | }) 150 | assertions = client.get_assertion(options=opts) 151 | # TODO: verify 152 | # cred_public_key = cred.attestation_object.auth_data.credential_data.public_key 153 | # cred_public_key.verify(assertion.authenticator_data + assert_client_data, 154 | # assertion.signature) 155 | self.assertEqual(1, len(assertions.get_assertions())) 156 | assertion = assertions.get_response(0) 157 | hmac = assertion.extension_results['hmacGetSecret']['output1'] 158 | self.assertEqual(32, len(hmac)) 159 | return hmac 160 | 161 | salt1 = b"x" * 32 162 | salt2 = b"y" * 32 163 | 164 | # If hmac-secret is working properly, it should give the same result 165 | # ... when provided with the same salt, and different results otherwise 166 | hmac_secret_result = get_assertion(salt1) 167 | hmac_secret_result_2 = get_assertion(salt1) 168 | hmac_secret_result_3 = get_assertion(salt2) 169 | 170 | self.assertEqual(hmac_secret_result, hmac_secret_result_2) 171 | self.assertNotEqual(hmac_secret_result, hmac_secret_result_3) 172 | -------------------------------------------------------------------------------- /python_tests/ctap/test_largeblobs.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import secrets 3 | 4 | from cryptography.hazmat.primitives import hashes 5 | from fido2.client import ClientError 6 | from fido2.ctap import CtapError 7 | from fido2.ctap2 import AttestationResponse, LargeBlobs, ClientPin, PinProtocolV2 8 | from fido2.ctap2.extensions import LargeBlobExtension 9 | from fido2.webauthn import ResidentKeyRequirement 10 | from parameterized import parameterized 11 | 12 | from .ctap_test import BasicAttestationTestCase 13 | 14 | 15 | class LargeBlobsTestCase(BasicAttestationTestCase): 16 | 17 | def test_info_shows_largeblobkey(self): 18 | info = self.ctap2.get_info() 19 | self.assertTrue(info.options.get("largeBlobs")) 20 | 21 | def test_makecred_largeblob_rejected_on_false(self): 22 | self.basic_makecred_params['extensions'] = { 23 | 'largeBlobKey': False 24 | } 25 | self.basic_makecred_params['options'] = { 26 | 'rk': True 27 | } 28 | 29 | with self.assertRaises(CtapError) as e: 30 | self.ctap2.make_credential(**self.basic_makecred_params) 31 | 32 | self.assertEqual(CtapError.ERR.INVALID_OPTION, e.exception.code) 33 | 34 | def make_large_blob_key(self) -> AttestationResponse: 35 | params = copy.copy(self.basic_makecred_params) 36 | params['extensions'] = { 37 | 'largeBlobKey': True 38 | } 39 | params['options'] = { 40 | 'rk': True 41 | } 42 | return self.ctap2.make_credential(**params) 43 | 44 | def test_makecred_with_largeblob(self): 45 | cred = self.make_large_blob_key() 46 | 47 | self.assertIsNotNone(cred.large_blob_key) 48 | self.assertEqual(32, len(cred.large_blob_key)) 49 | 50 | def test_discover_largeblobkey(self): 51 | cred = self.make_large_blob_key() 52 | 53 | assertion = self.get_assertion(rp_id=self.rp_id, extensions={"largeBlobKey": True}) 54 | 55 | self.assertEqual(cred.large_blob_key, assertion.large_blob_key) 56 | 57 | def test_get_does_not_require_pin(self): 58 | ClientPin(self.ctap2).set_pin("12345") 59 | 60 | res = LargeBlobs(self.ctap2).read_blob_array() 61 | 62 | self.assertEqual([], res) 63 | 64 | def test_set_requires_pin(self): 65 | ClientPin(self.ctap2).set_pin("12345") 66 | 67 | with self.assertRaises(CtapError) as e: 68 | LargeBlobs(self.ctap2).write_blob_array([1, 2, 3]) 69 | 70 | self.assertEqual(CtapError.ERR.PUAT_REQUIRED, e.exception.code) 71 | 72 | def test_set_with_pin_no_permissions(self): 73 | pin = secrets.token_hex(10) 74 | cp = ClientPin(self.ctap2) 75 | cp.set_pin(pin) 76 | uv = cp.get_pin_token(pin) 77 | lb = LargeBlobs(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv) 78 | 79 | with self.assertRaises(CtapError) as e: 80 | lb.write_blob_array([1, 2, 3]) 81 | 82 | self.assertEqual(CtapError.ERR.PIN_AUTH_INVALID, e.exception.code) 83 | 84 | def test_set_with_pin_with_permission(self): 85 | pin = secrets.token_hex(10) 86 | cp = ClientPin(self.ctap2) 87 | cp.set_pin(pin) 88 | uv = cp.get_pin_token(pin, permissions=ClientPin.PERMISSION.LARGE_BLOB_WRITE) 89 | 90 | lb = LargeBlobs(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv) 91 | lb.write_blob_array([1, 2, 3]) 92 | 93 | def test_set_with_pin_without_permission(self): 94 | pin = secrets.token_hex(10) 95 | cp = ClientPin(self.ctap2) 96 | cp.set_pin(pin) 97 | uv = cp.get_pin_token(pin, permissions=ClientPin.PERMISSION.CREDENTIAL_MGMT) 98 | lb = LargeBlobs(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv) 99 | 100 | with self.assertRaises(CtapError) as e: 101 | lb.write_blob_array([1, 2, 3]) 102 | 103 | self.assertEqual(CtapError.ERR.PIN_AUTH_INVALID, e.exception.code) 104 | 105 | def test_discover_largeblobkey_with_basic_cert(self): 106 | self.install_attestation_cert() 107 | 108 | cred = self.make_large_blob_key() 109 | 110 | assertion = self.get_assertion(rp_id=self.rp_id, extensions={"largeBlobKey": True}) 111 | 112 | self.assertEqual(cred.large_blob_key, assertion.large_blob_key) 113 | 114 | def test_get_largeblobkey(self): 115 | cred = self.make_large_blob_key() 116 | 117 | assertion = self.get_assertion_from_cred(cred=cred, extensions={"largeBlobKey": True}) 118 | 119 | self.assertEqual(cred.large_blob_key, assertion.large_blob_key) 120 | 121 | def test_iterative_largeblobkeys(self): 122 | creds = [] 123 | for i in range(10): 124 | self.basic_makecred_params['user']['id'] = secrets.token_bytes(64) 125 | creds.append(self.make_large_blob_key()) 126 | 127 | self.softResetCard() 128 | 129 | base_assertion = self.get_assertion(rp_id=self.rp_id, extensions={"largeBlobKey": True}) 130 | self.assertEqual(len(creds), base_assertion.number_of_credentials) 131 | self.assertEqual(creds[-1].large_blob_key, base_assertion.large_blob_key) 132 | 133 | for i in range(len(creds) - 2, 0, -1): 134 | assertion = self.ctap2.get_next_assertion() 135 | self.assertEqual(creds[i].large_blob_key, assertion.large_blob_key) 136 | 137 | @parameterized.expand([ 138 | ("mid", 100, 30, 40), 139 | ("start", 100, 0, 30), 140 | ("end", 100, 50, 50), 141 | ("long_mid", 800, 400, 150), 142 | ("long_nearend", 800, 600, 190), 143 | ("long_nearstart", 800, 20, 600), 144 | ]) 145 | def test_ll_offset_read_of_largeblob(self, _, length, offset, read_len): 146 | blob_array = secrets.token_bytes(length) 147 | h = hashes.Hash(hashes.SHA256()) 148 | h.update(blob_array) 149 | blob_array += h.finalize()[:16] 150 | 151 | self.ctap2.large_blobs(offset=0, set=blob_array, length=len(blob_array)) 152 | res = self.ctap2.large_blobs(offset=offset, get=read_len) 153 | 154 | self.assertEqual(blob_array[offset:offset+read_len], res[1]) 155 | 156 | def test_get_empty_largeblob_arr(self): 157 | arr = LargeBlobs(self.ctap2).read_blob_array() 158 | self.assertEqual(0, len(arr)) 159 | 160 | @parameterized.expand([ 161 | ("empty", 0), 162 | ("short", 10), 163 | ("short2", 78), 164 | ("onepacket", 200), 165 | ("onepacket2", 240), 166 | ("medium", 100), 167 | ("long", 600), 168 | ("chained", 950), 169 | ("maximal", 1004), 170 | ]) 171 | def test_set_and_get_large_blobs(self, _, num_bytes): 172 | blob_array = [secrets.token_bytes(num_bytes)] 173 | LargeBlobs(self.ctap2).write_blob_array(blob_array) 174 | 175 | self.softResetCard() 176 | 177 | res = LargeBlobs(self.ctap2).read_blob_array() 178 | self.assertEqual(blob_array, res) 179 | 180 | def test_set_fragmented_low_level(self): 181 | blob_array = secrets.token_bytes(170) 182 | h = hashes.Hash(hashes.SHA256()) 183 | h.update(blob_array) 184 | blob_array += h.finalize()[:16] 185 | 186 | self.ctap2.large_blobs(offset=0, set=blob_array[:10], length=len(blob_array)) 187 | self.ctap2.large_blobs(offset=10, set=blob_array[10:20]) 188 | self.ctap2.large_blobs(offset=20, set=blob_array[20:]) 189 | res = self.ctap2.large_blobs(offset=0, get=200) 190 | 191 | self.assertEqual(blob_array, res[1]) 192 | 193 | def test_set_fragmented_incomplete_low_level(self): 194 | blob_array = secrets.token_bytes(170) 195 | h = hashes.Hash(hashes.SHA256()) 196 | h.update(blob_array) 197 | blob_array += h.finalize()[:16] 198 | 199 | self.ctap2.large_blobs(offset=0, set=blob_array[:10], length=len(blob_array)) 200 | self.ctap2.large_blobs(offset=10, set=blob_array[10:20]) 201 | res = self.ctap2.large_blobs(offset=0, get=200) 202 | 203 | self.assertEqual(17, len(res[1])) 204 | 205 | def test_get_beyond_end(self): 206 | blob_array = secrets.token_bytes(62) 207 | h = hashes.Hash(hashes.SHA256()) 208 | h.update(blob_array) 209 | blob_array += h.finalize()[:16] 210 | 211 | self.ctap2.large_blobs(offset=0, set=blob_array, length=len(blob_array)) 212 | 213 | res = self.ctap2.large_blobs(offset=0, get=200) 214 | 215 | self.assertEqual(blob_array, res[1]) 216 | 217 | def test_rejects_with_invalid_hash(self): 218 | blob_array = secrets.token_bytes(62) 219 | h = hashes.Hash(hashes.SHA256()) 220 | h.update(blob_array) 221 | blob_array += h.finalize()[:16] 222 | blob_array = blob_array[:-1] + bytes([blob_array[-1] + 1]) 223 | 224 | with self.assertRaises(CtapError) as e: 225 | self.ctap2.large_blobs(offset=0, set=blob_array, length=len(blob_array)) 226 | 227 | self.assertEqual(CtapError.ERR.INTEGRITY_FAILURE, e.exception.code) 228 | 229 | def test_set_and_get_large_blobs_high_level(self): 230 | cred = self.make_large_blob_key() 231 | 232 | data = secrets.token_bytes(99) 233 | 234 | LargeBlobs(self.ctap2).put_blob(cred.large_blob_key, data=data) 235 | 236 | self.softResetCard() 237 | 238 | res = LargeBlobs(self.ctap2).get_blob(cred.large_blob_key) 239 | self.assertEqual(data, res) 240 | 241 | def test_mixing_blobs_from_different_keys(self): 242 | cred1 = self.make_large_blob_key() 243 | cred2 = self.make_large_blob_key() 244 | data1 = secrets.token_bytes(99) 245 | data2 = secrets.token_bytes(50) 246 | LargeBlobs(self.ctap2).put_blob(cred1.large_blob_key, data=data1) 247 | LargeBlobs(self.ctap2).put_blob(cred2.large_blob_key, data=data2) 248 | 249 | self.softResetCard() 250 | 251 | res2 = LargeBlobs(self.ctap2).get_blob(cred2.large_blob_key) 252 | res1 = LargeBlobs(self.ctap2).get_blob(cred1.large_blob_key) 253 | 254 | self.assertEqual(data1, res1) 255 | self.assertEqual(data2, res2) 256 | 257 | def test_overly_long_array_rejected(self): 258 | blob_array = [secrets.token_bytes(1005)] 259 | 260 | with self.assertRaises(CtapError) as e: 261 | LargeBlobs(self.ctap2).write_blob_array(blob_array) 262 | 263 | self.assertEqual(CtapError.ERR.LARGE_BLOB_STORAGE_FULL, e.exception.code) 264 | 265 | def test_largeblob_rejected_on_non_discoverable(self): 266 | client = self.get_high_level_client(extensions=[LargeBlobExtension]) 267 | with self.assertRaises(ClientError) as e: 268 | client.make_credential(self.get_high_level_make_cred_options( 269 | extensions={ 270 | "largeBlob": { 271 | "support": "required" 272 | } 273 | } 274 | )) 275 | 276 | self.assertEqual(CtapError.ERR.INVALID_OPTION, e.exception.cause.code) 277 | 278 | def test_largeblob_ignored_when_not_requested(self): 279 | client = self.get_high_level_client(extensions=[LargeBlobExtension]) 280 | 281 | cred = client.make_credential(self.get_high_level_make_cred_options( 282 | resident_key=ResidentKeyRequirement.REQUIRED 283 | )) 284 | 285 | self.assertEqual({}, cred.extension_results) 286 | 287 | 288 | -------------------------------------------------------------------------------- /python_tests/ctap/test_long_request_buffer.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import random 3 | 4 | from typing import Optional 5 | 6 | from .ctap_test import CTAPTestCase 7 | 8 | 9 | class CTAPLongRequestBufferTestCase(CTAPTestCase): 10 | 11 | def setUp(self, install_params: Optional[bytes] = None) -> None: 12 | super().setUp(bytes([0xA1, 0x0A, 0x19, 0x08, 0x00])) 13 | 14 | def test_info_shows_long_message_size(self): 15 | info = self.ctap2.get_info() 16 | self.assertEqual(2048, info.max_msg_size) 17 | 18 | def test_long_request_handled(self): 19 | cred = self.ctap2.make_credential(**self.basic_makecred_params) 20 | allow_list = [] 21 | total_len = 0 22 | while total_len < 1100: 23 | cred_len = random.randint(1, 112) 24 | rando_data = secrets.token_bytes(cred_len) 25 | allow_list.append({ 26 | "type": "public-key", 27 | "id": rando_data 28 | }) 29 | total_len += cred_len + 20 30 | allow_list.append({ 31 | "type": "public-key", 32 | "id": cred.auth_data.credential_data.credential_id 33 | }) 34 | self.ctap2.get_assertion( 35 | rp_id=self.rp_id, 36 | client_data_hash=self.client_data, 37 | allow_list=allow_list 38 | ) 39 | -------------------------------------------------------------------------------- /python_tests/ctap/test_malformed_input.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | from fido2.ctap import CtapError 4 | from fido2.ctap2 import ClientPin 5 | 6 | from .ctap_test import CTAPTestCase 7 | 8 | 9 | class CTAPMalformedInputTestCase(CTAPTestCase): 10 | 11 | def test_notices_invalid_keyparams_later_in_array(self): 12 | self.basic_makecred_params['key_params'].append({}) 13 | with self.assertRaises(CtapError) as e: 14 | self.ctap2.make_credential(**self.basic_makecred_params) 15 | self.assertEqual(CtapError.ERR.MISSING_PARAMETER, e.exception.code) 16 | 17 | def test_options_not_a_map(self): 18 | self.basic_makecred_params['options'] = [] 19 | with self.assertRaises(CtapError) as e: 20 | self.ctap2.make_credential(**self.basic_makecred_params) 21 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 22 | 23 | def test_extensions_not_a_map(self): 24 | self.basic_makecred_params['extensions'] = "Good and Loud" 25 | with self.assertRaises(CtapError) as e: 26 | self.ctap2.make_credential(**self.basic_makecred_params) 27 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 28 | 29 | def test_too_many_extensions(self): 30 | d = {} 31 | for x in range(30): 32 | d[str(x)] = True 33 | self.basic_makecred_params['extensions'] = d 34 | with self.assertRaises(CtapError) as e: 35 | self.ctap2.make_credential(**self.basic_makecred_params) 36 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 37 | 38 | def test_rp_icon_not_text(self): 39 | self.basic_makecred_params['rp']['icon'] = 454 40 | with self.assertRaises(CtapError) as e: 41 | self.ctap2.make_credential(**self.basic_makecred_params) 42 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 43 | 44 | def test_user_icon_not_text(self): 45 | self.basic_makecred_params['user']['icon'] = 454 46 | with self.assertRaises(CtapError) as e: 47 | self.ctap2.make_credential(**self.basic_makecred_params) 48 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 49 | 50 | def test_bogus_makecred_options(self): 51 | self.basic_makecred_params['options'] = {'frobble': 23} 52 | self.ctap2.make_credential(**self.basic_makecred_params) 53 | 54 | def test_bogus_getassert_options(self): 55 | cred = self.ctap2.make_credential(**self.basic_makecred_params) 56 | self.get_assertion_from_cred(cred, options={'bogus': 123}) 57 | 58 | def test_bogus_exclude_list(self): 59 | self.basic_makecred_params['exclude_list'] = 12345 60 | 61 | with self.assertRaises(CtapError) as e: 62 | self.ctap2.make_credential(**self.basic_makecred_params) 63 | 64 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 65 | 66 | def test_bogus_exclude_list_entry(self): 67 | self.basic_makecred_params['exclude_list'] = [12345] 68 | 69 | with self.assertRaises(CtapError) as e: 70 | self.ctap2.make_credential(**self.basic_makecred_params) 71 | 72 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 73 | 74 | def test_exclude_list_not_array_raw(self): 75 | res = self.ctap2.device.call(0x10, bytes.fromhex("01a501582087e85f1f2eb615568b93d528574858f21d4cdbd6c0389fffe8d83977fc52d11102a26269646f73717565616d6973686669672e666d646e616d657829546865204578616d706c6520436f72706f726174696f6e20776974682066616b6520646f6d61696e2103a3626964582049aad568d2165c8d1ae1f9feb29bdbeef0e7305ec7e0dd5c811e67a0654d820a646e616d657818616c656370616c617a7a6f40657874656e6479616d2e63666b646973706c61794e616d656c416c65632050616c617a7a6f0481a263616c672664747970656a7075626c69632d6b65790574616c444346775f3976626f6b336b56462d497348")) 76 | 77 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, res[0]) 78 | 79 | def test_empty_pinuvauth_with_create(self): 80 | res = self.ctap2.device.call(0x10, bytes.fromhex("01A5015820FDA53DBE83484DC63BE566B024696E84FE0E24B219535EB98514D883481C5E5402A1626964781834316165343262633166623338343132386561333566646403A3626964583DBDEA92D4DAFD424B45B3DCE17BF6B8F65632D20D6CAF844BA92FE5CA44EFEDCF6A25059BF7AFB20AB2EBA99FF8D35A8EF0460D7233918505CA4998CA06646E616D656E38656536313763656133636464366C646973706C61795F6E616D65782632336362626239363038326665316439623738623734306235653735326133306334633130640481A263616C672664747970656A7075626C69632D6B65790840")) 81 | 82 | self.assertEqual(CtapError.ERR.PIN_NOT_SET, res[0]) 83 | 84 | def test_empty_pinuvauth_with_assert(self): 85 | res = self.ctap2.device.call(0x10, bytes.fromhex("02A4017820373766633733386666343537623033333337626266336538646264326237376202582001CAA8FE3488889C87E083EF22561A38C724D558646DEFB9B3C8D923F959D54C0381A26269645870EDEFB6C58327538F3B4F7E4B9AF30C70DDEE6C548FA0BEA66C59567CE8CD048258C27FB46EECCB66A23E208AF23A4D3C2E9E78B1CB53BE87419E1D0DAD2C584F84513502486B9791E3949BE75CA0F05789B37758300BD66E933317595AFFEDDB2C1762C0DC7504743B95371411787F0164747970656A7075626C69632D6B65790640")) 86 | 87 | self.assertEqual(CtapError.ERR.PIN_NOT_SET, res[0]) 88 | 89 | def test_empty_pinuvauth_with_create_pin(self): 90 | cp = ClientPin(self.ctap2) 91 | cp.set_pin('aaaaaa') 92 | res = self.ctap2.device.call(0x10, bytes.fromhex("01A5015820FDA53DBE83484DC63BE566B024696E84FE0E24B219535EB98514D883481C5E5402A1626964781834316165343262633166623338343132386561333566646403A3626964583DBDEA92D4DAFD424B45B3DCE17BF6B8F65632D20D6CAF844BA92FE5CA44EFEDCF6A25059BF7AFB20AB2EBA99FF8D35A8EF0460D7233918505CA4998CA06646E616D656E38656536313763656133636464366C646973706C61795F6E616D65782632336362626239363038326665316439623738623734306235653735326133306334633130640481A263616C672664747970656A7075626C69632D6B65790840")) 93 | 94 | self.assertEqual(CtapError.ERR.PIN_INVALID, res[0]) 95 | 96 | def test_empty_pinuvauth_with_assert_pin(self): 97 | cp = ClientPin(self.ctap2) 98 | cp.set_pin('aaaaaa') 99 | 100 | res = self.ctap2.device.call(0x10, bytes.fromhex("02A4017820373766633733386666343537623033333337626266336538646264326237376202582001CAA8FE3488889C87E083EF22561A38C724D558646DEFB9B3C8D923F959D54C0381A26269645870EDEFB6C58327538F3B4F7E4B9AF30C70DDEE6C548FA0BEA66C59567CE8CD048258C27FB46EECCB66A23E208AF23A4D3C2E9E78B1CB53BE87419E1D0DAD2C584F84513502486B9791E3949BE75CA0F05789B37758300BD66E933317595AFFEDDB2C1762C0DC7504743B95371411787F0164747970656A7075626C69632D6B65790640")) 101 | 102 | self.assertEqual(CtapError.ERR.PIN_INVALID, res[0]) 103 | 104 | def test_options_not_map_raw(self): 105 | res = self.ctap2.device.call(0x10, bytes.fromhex("01a501582078983526ab67de0cb8bca9996d14a83b248ddcfb586f18fd815ba953d19f618a02a26269646e73617473756d61686f6f6b2e6172646e616d657829546865204578616d706c6520436f72706f726174696f6e20776974682066616b6520646f6d61696e2103a362696458209d95b802bd88ef691b1b8f6cc00c258dc56be2ca80f3027b449415499e498e2c646e616d65782a636872697374656e61796f7368696d75726140696e6475737472696f7573626c75652d657965642e70746b646973706c61794e616d6573436872697374656e6120596f7368696d7572610481a263616c672664747970656a7075626c69632d6b65790680")) 106 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, res[0]) 107 | 108 | def test_invalid_pubkeycredparams_seq_raw(self): 109 | res = self.ctap2.device.call(0x10, bytes.fromhex("01a401582088ebea80f87453c4981821b9ef5a2017da4dee5c09454b5be5ef6bb0b5e59bab02a26269647673746172667275697477686973706572696e672e7669646e616d657829546865204578616d706c6520436f72706f726174696f6e20776974682066616b6520646f6d61696e2103a36269645820c3d2bd8894cfadb71278ac4ed0fe5c1c46ae949495b7261b3d685cab52eaf090646e616d65781d746172656e2e67617465776f6f644072656a6f696365777261702e706d6b646973706c61794e616d656e546172656e2047617465776f6f640482a263616c672664747970656a7075626c69632d6b65797450755a5f6b6e4a584269646d4b796970344c7552")) 110 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, res[0]) 111 | 112 | def test_bogus_exclude_list_entry_after_valid(self): 113 | cred = self.ctap2.make_credential(**self.basic_makecred_params) 114 | 115 | with self.assertRaises(CtapError) as e: 116 | self.ctap2.make_credential(**self.basic_makecred_params, exclude_list=[ 117 | self.get_descriptor_from_ll_cred(cred), 118 | 12334 119 | ]) 120 | 121 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 122 | 123 | def test_user_icon_not_text(self): 124 | self.basic_makecred_params['user']['icon'] = secrets.token_bytes(16) 125 | 126 | with self.assertRaises(CtapError) as e: 127 | self.ctap2.make_credential(**self.basic_makecred_params) 128 | 129 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 130 | 131 | def test_user_id_only(self): 132 | self.basic_makecred_params['user'] = {"id": self.basic_makecred_params['user']["id"]} 133 | 134 | cred = self.ctap2.make_credential(**self.basic_makecred_params) 135 | 136 | self.assertIsNotNone(cred) 137 | 138 | def test_rp_icon_not_text(self): 139 | self.basic_makecred_params['rp']['icon'] = secrets.token_bytes(16) 140 | 141 | with self.assertRaises(CtapError) as e: 142 | self.ctap2.make_credential(**self.basic_makecred_params) 143 | 144 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 145 | 146 | def test_rejects_non_string_type_in_array(self): 147 | self.basic_makecred_params['key_params'].append({ 148 | "type": 3949, 149 | "alg": 12 150 | }) 151 | with self.assertRaises(CtapError) as e: 152 | self.ctap2.make_credential(**self.basic_makecred_params) 153 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 154 | 155 | def test_rejects_es256_with_non_string_type(self): 156 | self.basic_makecred_params['key_params'].append({ 157 | "type": False, 158 | "alg": -7 159 | }) 160 | with self.assertRaises(CtapError) as e: 161 | self.ctap2.make_credential(**self.basic_makecred_params) 162 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 163 | 164 | def test_rejects_non_integer_alg_in_array(self): 165 | self.basic_makecred_params['key_params'].append({ 166 | "type": "public-key", 167 | "alg": "foo" 168 | }) 169 | with self.assertRaises(CtapError) as e: 170 | self.ctap2.make_credential(**self.basic_makecred_params) 171 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 172 | 173 | def test_rejects_non_integer_alg_at_start_of_array(self): 174 | self.basic_makecred_params['key_params'].insert(0, { 175 | "type": "public-key", 176 | "alg": "foo" 177 | }) 178 | with self.assertRaises(CtapError) as e: 179 | self.ctap2.make_credential(**self.basic_makecred_params) 180 | self.assertEqual(CtapError.ERR.CBOR_UNEXPECTED_TYPE, e.exception.code) 181 | -------------------------------------------------------------------------------- /python_tests/ctap/test_setminpin.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | 4 | from fido2.ctap import CtapError 5 | from fido2.ctap2 import Config, ClientPin, PinProtocolV2 6 | from fido2.ctap2.extensions import MinPinLengthExtension 7 | from parameterized import parameterized 8 | 9 | from .ctap_test import CTAPTestCase 10 | 11 | 12 | class SetMinPinTestCase(CTAPTestCase): 13 | 14 | cp: ClientPin 15 | pin: Optional[str] = None 16 | 17 | def setUp(self, install_params: Optional[bytes] = None) -> None: 18 | super().setUp(install_params=install_params) 19 | self.cp = ClientPin(self.ctap2) 20 | 21 | def get_cfg(self) -> Config: 22 | if self.pin is None: 23 | return Config(self.ctap2) 24 | uv = self.cp.get_pin_token(self.pin, 25 | permissions=ClientPin.PERMISSION.AUTHENTICATOR_CFG) 26 | return Config(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv) 27 | 28 | def test_info_min_pin_length(self): 29 | info = self.ctap2.get_info() 30 | self.assertEqual(4, info.min_pin_length) 31 | 32 | def test_info_max_rps_for_setminpin(self): 33 | info = self.ctap2.get_info() 34 | self.assertEqual(2, info.max_rpids_for_min_pin) 35 | 36 | def test_setminpin_option(self): 37 | info = self.ctap2.get_info() 38 | self.assertEqual(True, info.options.get("setMinPINLength")) 39 | 40 | @parameterized.expand([ 41 | ("authenticated", True), 42 | ("unauthenticated", False), 43 | ]) 44 | def test_setminpin_visible_in_info(self, _, setpin: bool): 45 | if setpin: 46 | self.pin = secrets.token_hex(10) 47 | self.cp.set_pin(self.pin) 48 | 49 | self.get_cfg().set_min_pin_length(min_pin_length=8) 50 | 51 | info = self.ctap2.get_info() 52 | self.assertEqual(8, info.min_pin_length) 53 | 54 | def test_setminpin_requires_pin_when_set(self): 55 | self.pin = secrets.token_hex(10) 56 | self.cp.set_pin(self.pin) 57 | 58 | with self.assertRaises(CtapError) as e: 59 | Config(self.ctap2).set_min_pin_length(min_pin_length=2) 60 | 61 | self.assertEqual(CtapError.ERR.PUAT_REQUIRED, e.exception.code) 62 | 63 | @parameterized.expand([ 64 | (4, 3), 65 | (8, 7), 66 | (30, 4), 67 | ]) 68 | def test_setminpin_cannot_go_down(self, original_value, new_value): 69 | self.get_cfg().set_min_pin_length(min_pin_length=original_value) 70 | 71 | with self.assertRaises(CtapError) as e: 72 | self.get_cfg().set_min_pin_length(min_pin_length=new_value) 73 | 74 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 75 | 76 | def test_setminpin_overlong(self): 77 | with self.assertRaises(CtapError) as e: 78 | self.get_cfg().set_min_pin_length(min_pin_length=70) 79 | 80 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 81 | 82 | def test_setminpin_rpids(self): 83 | self.get_cfg().set_min_pin_length(rp_ids=[self.rp_id], min_pin_length=6) 84 | client = self.get_high_level_client([MinPinLengthExtension]) 85 | new_rpid = secrets.token_hex(10) 86 | new_rpid_client = self.get_high_level_client([MinPinLengthExtension], origin='https://' + new_rpid) 87 | 88 | self.softResetCard() 89 | 90 | matching_cred = client.make_credential(self.get_high_level_make_cred_options(extensions={ 91 | "minPinLength": True 92 | })) 93 | nonmatching_cred = new_rpid_client.make_credential(self.get_high_level_make_cred_options(extensions={ 94 | "minPinLength": True 95 | }, rp_id=new_rpid)) 96 | 97 | self.assertEqual(6, matching_cred.attestation_object.auth_data.extensions.get('minPinLength')) 98 | self.assertIsNone(nonmatching_cred.attestation_object.auth_data.extensions) 99 | 100 | def test_set_too_many_rpids(self): 101 | ids = ['foo'] * 10 102 | 103 | with self.assertRaises(CtapError) as e: 104 | self.get_cfg().set_min_pin_length(rp_ids=ids) 105 | 106 | self.assertEqual(CtapError.ERR.KEY_STORE_FULL, e.exception.code) 107 | 108 | def test_four_ascii_chars(self): 109 | self.cp.set_pin("aaaa") 110 | 111 | def test_four_ascii_chars_rejected_when_length_increased(self): 112 | self.get_cfg().set_min_pin_length(min_pin_length=5) 113 | 114 | with self.assertRaises(CtapError) as e: 115 | self.cp.set_pin("aaaa") 116 | 117 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 118 | 119 | def test_four_multibyte_chars_rejected_when_length_increased(self): 120 | self.get_cfg().set_min_pin_length(min_pin_length=5) 121 | 122 | with self.assertRaises(CtapError) as e: 123 | self.cp.set_pin("✈✈✈✈") 124 | 125 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 126 | 127 | def test_five_multibyte_chars_accepted_when_length_increased(self): 128 | self.get_cfg().set_min_pin_length(min_pin_length=5) 129 | 130 | self.cp.set_pin("✈✈ä✈✈") 131 | 132 | def test_four_multibyte_chars_accepted_normally(self): 133 | self.cp.set_pin("✈✈✈✈") 134 | 135 | def test_change_without_pin_does_not_force_change(self): 136 | self.get_cfg().set_min_pin_length(min_pin_length=10) 137 | info = self.ctap2.get_info() 138 | 139 | self.assertEqual(False, info.force_pin_change) 140 | 141 | def test_change_with_pin_does_not_force_change_when_length_adequate(self): 142 | self.pin = secrets.token_hex(10) 143 | self.cp.set_pin(self.pin) 144 | 145 | self.get_cfg().set_min_pin_length(min_pin_length=10) 146 | info = self.ctap2.get_info() 147 | 148 | self.assertEqual(False, info.force_pin_change) 149 | self.assertEqual(10, info.min_pin_length) 150 | 151 | def test_change_with_pin_does_force_change_when_length_inadequate(self): 152 | self.pin = "AAAAAAAAA" 153 | self.cp.set_pin(self.pin) 154 | 155 | self.get_cfg().set_min_pin_length(min_pin_length=10) 156 | info = self.ctap2.get_info() 157 | 158 | self.assertEqual(True, info.force_pin_change) 159 | self.assertEqual(10, info.min_pin_length) 160 | 161 | def test_length_adequacy_is_in_codepoints(self): 162 | self.get_cfg().set_min_pin_length(min_pin_length=10) 163 | self.pin = "✈✈✈✈" # 12 bytes, 4 codepoints 164 | 165 | with self.assertRaises(CtapError) as e: 166 | self.cp.set_pin(self.pin) 167 | 168 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 169 | 170 | def test_change_with_pin_to_same_length_does_not_force_change(self): 171 | self.pin = secrets.token_hex(10) 172 | self.cp.set_pin(self.pin) 173 | 174 | self.get_cfg().set_min_pin_length(min_pin_length=4) 175 | info = self.ctap2.get_info() 176 | 177 | self.assertEqual(False, info.force_pin_change) 178 | 179 | def test_cannot_get_uv_when_change_forced(self): 180 | self.pin = secrets.token_hex(10) 181 | self.cp.set_pin(self.pin) 182 | self.get_cfg().set_min_pin_length(force_change_pin=True) 183 | 184 | with self.assertRaises(CtapError) as e: 185 | self.cp.get_pin_token(self.pin) 186 | 187 | self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code) 188 | 189 | def test_cannot_force_change_without_pin(self): 190 | with self.assertRaises(CtapError) as e: 191 | self.get_cfg().set_min_pin_length(force_change_pin=True) 192 | 193 | self.assertEqual(CtapError.ERR.PIN_NOT_SET, e.exception.code) 194 | 195 | def test_accepts_extension_on_makecred(self): 196 | client = self.get_high_level_client([MinPinLengthExtension]) 197 | cred = client.make_credential(self.get_high_level_make_cred_options(extensions={ 198 | "minPinLength": True 199 | })) 200 | self.assertIsNone(cred.extension_results.get("minPinLength")) 201 | 202 | def test_rejects_false_extension_on_makecred(self): 203 | self.basic_makecred_params['extensions'] = { 204 | 'minPinLength': False 205 | } 206 | 207 | with self.assertRaises(CtapError) as e: 208 | self.ctap2.make_credential(**self.basic_makecred_params) 209 | 210 | self.assertEqual(CtapError.ERR.INVALID_OPTION, e.exception.code) 211 | -------------------------------------------------------------------------------- /python_tests/ctap/test_u2f.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional 3 | 4 | from fido2.ctap1 import ApduError 5 | from fido2.ctap2 import ClientPin 6 | from fido2.ctap2.extensions import CredProtectExtension 7 | from fido2.webauthn import ResidentKeyRequirement 8 | 9 | from .ctap_test import BasicAttestationTestCase, FixedPinUserInteraction 10 | 11 | 12 | class U2FTestCase(BasicAttestationTestCase): 13 | 14 | rp_hash: bytes 15 | 16 | def setUp(self, install_params: Optional[bytes] = None) -> None: 17 | super().setUp(install_params) 18 | self.install_attestation_cert() 19 | self.rp_hash = self.rp_id_hash(self.rp_id) 20 | 21 | def test_u2f_version(self): 22 | res = self.ctap1.get_version() 23 | self.assertEqual("U2F_V2", res) 24 | 25 | def test_u2f_register(self): 26 | res = self.ctap1.register(self.client_data, self.rp_hash) 27 | self.assertEqual(self.cert, res.certificate) 28 | res.verify(app_param=self.rp_hash, client_param=self.client_data) 29 | 30 | def test_u2f_authenticate(self): 31 | cred = self.ctap1.register(self.client_data, self.rp_hash) 32 | 33 | res = self.ctap1.authenticate(client_param=self.client_data, 34 | app_param=self.rp_hash, 35 | key_handle=cred.key_handle) 36 | res.verify(app_param=self.rp_hash, client_param=self.client_data, 37 | public_key=cred.public_key) 38 | 39 | def test_u2f_authenticate_rejects_mismatching_rpid(self): 40 | cred = self.ctap1.register(self.client_data, self.rp_hash) 41 | 42 | with self.assertRaises(ApduError) as e: 43 | res = self.ctap1.authenticate(client_param=self.client_data, 44 | app_param=secrets.token_bytes(32), 45 | key_handle=cred.key_handle) 46 | self.assertEqual(27264, e.exception.code) 47 | 48 | def test_authenticate_increases_counter(self): 49 | cred = self.ctap1.register(self.client_data, self.rp_hash) 50 | 51 | res_1 = self.ctap1.authenticate(client_param=self.client_data, 52 | app_param=self.rp_hash, 53 | key_handle=cred.key_handle) 54 | res_2 = self.ctap1.authenticate(client_param=self.client_data, 55 | app_param=self.rp_hash, 56 | key_handle=cred.key_handle) 57 | self.assertTrue(res_1.counter < res_2.counter) 58 | 59 | def test_u2f_credential_usable_over_ctap2(self): 60 | res = self.ctap1.register(self.client_data, self.rp_hash) 61 | self.ctap2.get_assertion(rp_id=self.rp_id, 62 | allow_list=[ 63 | { 64 | "type": "public-key", 65 | "id": res.key_handle 66 | } 67 | ], 68 | client_data_hash=secrets.token_bytes(32)) 69 | 70 | def test_ctap2_credential_usable_over_u2f(self): 71 | cred = self.ctap2.make_credential(**self.basic_makecred_params) 72 | res = self.ctap1.authenticate(client_param=secrets.token_bytes(32), 73 | app_param=self.rp_hash, 74 | key_handle=cred.auth_data.credential_data.credential_id) 75 | 76 | def test_cred_protect_low_usable_over_u2f(self): 77 | pin = secrets.token_hex(8) 78 | ClientPin(self.ctap2).set_pin(pin) 79 | client = self.get_high_level_client(extensions=[CredProtectExtension], 80 | user_interaction=FixedPinUserInteraction(pin)) 81 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 82 | extensions={ 83 | "credentialProtectionPolicy": CredProtectExtension.POLICY.OPTIONAL_WITH_LIST 84 | } 85 | )) 86 | self.ctap1.authenticate(client_param=secrets.token_bytes(32), 87 | app_param=self.rp_hash, 88 | key_handle=cred.attestation_object.auth_data.credential_data.credential_id) 89 | 90 | def test_cred_protect_high_not_usable_over_u2f(self): 91 | pin = secrets.token_hex(8) 92 | ClientPin(self.ctap2).set_pin(pin) 93 | client = self.get_high_level_client(extensions=[CredProtectExtension], 94 | user_interaction=FixedPinUserInteraction(pin)) 95 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 96 | extensions={ 97 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 98 | } 99 | )) 100 | with self.assertRaises(ApduError) as e: 101 | self.ctap1.authenticate(client_param=secrets.token_bytes(32), 102 | app_param=self.rp_hash, 103 | key_handle=cred.attestation_object.auth_data.credential_data.credential_id) 104 | self.assertEqual(27264, e.exception.code) 105 | 106 | def test_discoverable_low_security_usable_over_u2f(self): 107 | client = self.get_high_level_client() 108 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 109 | resident_key=ResidentKeyRequirement.REQUIRED 110 | )) 111 | self.ctap1.authenticate(client_param=secrets.token_bytes(32), 112 | app_param=self.rp_hash, 113 | key_handle=cred.attestation_object.auth_data.credential_data.credential_id) 114 | 115 | def test_discoverable_med_security_usable_over_u2f(self): 116 | client = self.get_high_level_client() 117 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 118 | resident_key=ResidentKeyRequirement.REQUIRED, 119 | extensions={ 120 | "credentialProtectionPolicy": CredProtectExtension.POLICY.OPTIONAL_WITH_LIST 121 | } 122 | )) 123 | self.ctap1.authenticate(client_param=secrets.token_bytes(32), 124 | app_param=self.rp_hash, 125 | key_handle=cred.attestation_object.auth_data.credential_data.credential_id) 126 | 127 | def test_discoverable_high_security_not_usable_over_u2f(self): 128 | client = self.get_high_level_client(extensions=[CredProtectExtension]) 129 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 130 | resident_key=ResidentKeyRequirement.REQUIRED, 131 | extensions={ 132 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 133 | } 134 | )) 135 | with self.assertRaises(ApduError) as e: 136 | self.ctap1.authenticate(client_param=secrets.token_bytes(32), 137 | app_param=self.rp_hash, 138 | key_handle=cred.attestation_object.auth_data.credential_data.credential_id) 139 | self.assertEqual(27264, e.exception.code) 140 | 141 | def test_discoverable_high_security_not_usable_over_u2f_with_pin(self): 142 | pin = secrets.token_hex(8) 143 | ClientPin(self.ctap2).set_pin(pin) 144 | client = self.get_high_level_client(user_interaction=FixedPinUserInteraction(pin), 145 | extensions=[CredProtectExtension]) 146 | cred = client.make_credential(options=self.get_high_level_make_cred_options( 147 | resident_key=ResidentKeyRequirement.REQUIRED, 148 | extensions={ 149 | "credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED 150 | } 151 | )) 152 | with self.assertRaises(ApduError) as e: 153 | self.ctap1.authenticate(client_param=secrets.token_bytes(32), 154 | app_param=self.rp_hash, 155 | key_handle=cred.attestation_object.auth_data.credential_data.credential_id) 156 | self.assertEqual(27264, e.exception.code) 157 | -------------------------------------------------------------------------------- /python_tests/ctap/test_uvm.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any, Dict 2 | 3 | from fido2.ctap2 import ClientPin, AssertionResponse, AttestationResponse 4 | from fido2.ctap2.extensions import Ctap2Extension 5 | from fido2.ctap2.pin import PinProtocol 6 | from fido2.webauthn import UserVerificationRequirement 7 | 8 | from .ctap_test import CTAPTestCase, FixedPinUserInteraction 9 | 10 | 11 | class UVMExtension(Ctap2Extension): 12 | 13 | NAME = 'uvm' 14 | 15 | def is_supported(self) -> bool: 16 | return True 17 | 18 | def process_create_input(self, inputs: Dict[str, Any]) -> Any: 19 | return True 20 | 21 | def process_create_output( 22 | self, 23 | attestation_response: AttestationResponse, 24 | token: Optional[str], 25 | pin_protocol: Optional[PinProtocol], 26 | ) -> Optional[Dict[str, Any]]: 27 | return { 28 | "uvm": attestation_response.auth_data.extensions.get(self.NAME) 29 | } 30 | 31 | def process_get_input(self, inputs: Dict[str, Any]) -> Any: 32 | return True 33 | 34 | def process_get_output( 35 | self, 36 | assertion_response: AssertionResponse, 37 | token: Optional[str], 38 | pin_protocol: Optional[PinProtocol], 39 | ) -> Optional[Dict[str, Any]]: 40 | return { 41 | "uvm": assertion_response.auth_data.extensions.get(self.NAME) 42 | } 43 | 44 | 45 | class UVMTestCase(CTAPTestCase): 46 | 47 | def test_uvm_no_pin_on_makecred(self): 48 | res = self.ctap2.make_credential(**self.basic_makecred_params, extensions={ 49 | "uvm": True 50 | }) 51 | self.assertEqual([[1, 10, 4]], res.auth_data.extensions['uvm']) 52 | 53 | def test_uvm_with_pin_on_makecred(self): 54 | pin = "12345" 55 | ClientPin(self.ctap2).set_pin(pin) 56 | 57 | client = self.get_high_level_client(extensions=[UVMExtension], 58 | user_interaction=FixedPinUserInteraction(pin)) 59 | cred = client.make_credential( 60 | self.get_high_level_make_cred_options( 61 | user_verification=UserVerificationRequirement.REQUIRED 62 | ) 63 | ) 64 | 65 | self.assertEqual([[2048, 10, 4]], cred.extension_results['uvm']) 66 | 67 | def test_uvm_with_pin_on_get_assertion(self): 68 | cred = self.get_high_level_client().make_credential(self.get_high_level_make_cred_options()) 69 | 70 | pin = "12345" 71 | ClientPin(self.ctap2).set_pin(pin) 72 | 73 | client = self.get_high_level_client(extensions=[UVMExtension], 74 | user_interaction=FixedPinUserInteraction(pin)) 75 | 76 | assertion = client.get_assertion(self.get_high_level_assertion_opts_from_cred( 77 | cred, 78 | user_verification=UserVerificationRequirement.REQUIRED 79 | )) 80 | 81 | self.assertEqual([[2048, 10, 4]], 82 | assertion.get_assertions()[0].auth_data.extensions['uvm']) 83 | 84 | def test_uvm_without_pin_on_get_assertion(self): 85 | cred = self.get_high_level_client().make_credential(self.get_high_level_make_cred_options()) 86 | 87 | client = self.get_high_level_client(extensions=[UVMExtension]) 88 | 89 | assertion = client.get_assertion(self.get_high_level_assertion_opts_from_cred( 90 | cred 91 | )) 92 | 93 | self.assertEqual([[1, 10, 4]], 94 | assertion.get_assertions()[0].auth_data.extensions['uvm']) 95 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fido2[pcsc]==1.1.2 2 | JPype1==1.5.0 3 | parameterized==0.9.0 4 | uhid==0.0.1 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "fido2applet" 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/us/q3q/fido2/BufferManager.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | import javacard.framework.*; 4 | 5 | /** 6 | * A class for managing the awkwardness of preferring RAM to flash on very resource-constrained devices. 7 | * 8 | * Will allocate space from, in descending order of priority: 9 | * - The upper reaches of the APDU buffer, beyond the incoming request length 10 | * - The lower bounds of the APDU buffer, behind the current read index 11 | * - A TRANSIENT_DESELECT buffer 12 | * - Flash storage 13 | * 14 | * Allocations return opaque handles that need to be decoded to byte array references and offsets. 15 | * Memory can only be freed in the reverse order it is acquired - no fragmentation allowed and only a few 16 | * bytes of management overhead. 17 | */ 18 | public final class BufferManager { 19 | 20 | /** 21 | * Location in in-memory buffer of byte describing how much of the in-memory buffer is used 22 | */ 23 | private static final byte OFFSET_MEMBUF_USED_SPACE = 0; // 2 bytes 24 | /** 25 | * Location in in-memory buffer of short describing how much flash is used 26 | */ 27 | private static final byte OFFSET_FLASH_USED_SPACE = 2; // 2 bytes 28 | /** 29 | * Total in-memory buffer overhead of state keeping variables 30 | */ 31 | private static final byte STATE_KEEPING_OVERHEAD = 4; 32 | 33 | /** 34 | * In-RAM buffer which is OUTSIDE the APDU buffer. Great to have for minimizing flash wear. 35 | */ 36 | private final byte[] inMemoryBuffer; 37 | /** 38 | * Flash scratch buffer. A last resort for when there just isn't enough memory elsewhere. 39 | */ 40 | private final byte[] flashBuffer; 41 | 42 | /** 43 | * Allow allocations in the portion of the APDU buffer behind the known read cursor (growing upwards) 44 | */ 45 | public static final byte LOWER_APDU = (byte) 0x01; 46 | /** 47 | * Allow allocations in the portion of the APDU buffer beyond the end of the incoming request (growing downwards) 48 | */ 49 | public static final byte UPPER_APDU = (byte) 0x02; 50 | /** 51 | * Allow allocations in the non-APDU buffer in memory 52 | */ 53 | public static final byte MEMORY_BUFFER = (byte) 0x04; 54 | /** 55 | * Allow allocations in flash 56 | */ 57 | public static final byte FLASH = (byte) 0x08; 58 | /** 59 | * Allow allocations in any location 60 | */ 61 | public static final byte ANYWHERE = (byte) 0xFF; 62 | /** 63 | * Allow allocations anywhere that will not be clobbered by APDU writes 64 | */ 65 | public static final byte NOT_APDU_BUFFER = (byte)(MEMORY_BUFFER | FLASH); 66 | /** 67 | * Allow allocations anywhere EXCEPT in the lower reaches of the APDU. This ensures that large APDU buffers with 68 | * small writes won't get clobbered. 69 | */ 70 | public static final byte NOT_LOWER_APDU = (byte)(UPPER_APDU | MEMORY_BUFFER | FLASH); 71 | 72 | public BufferManager(short transientLen, short persistentLen) { 73 | inMemoryBuffer = JCSystem.makeTransientByteArray(transientLen, JCSystem.CLEAR_ON_DESELECT); 74 | flashBuffer = new byte[persistentLen]; 75 | clear(); 76 | } 77 | 78 | /** 79 | * Report on in-memory buffer sizing 80 | * 81 | * @return The number of bytes allocated to the in-memory non-APDU buffer 82 | */ 83 | public short getTransientBufferSize() { 84 | return (short) inMemoryBuffer.length; 85 | } 86 | 87 | private short encodeLowerAPDUOffset(short offset) { 88 | // Lower APDU offsets are given in the range [-1,-257] 89 | return (short)(-1 * offset - 1); 90 | } 91 | 92 | private short encodeUpperAPDUOffset(short offset) { 93 | // Upper offsets are given in the range [-258,-12287] 94 | return (short)(-1 * offset - 257); 95 | } 96 | 97 | private short encodeMemoryBufferOffset(short offset) { 98 | // Memory offsets are given in the range [-12288, -infinity) 99 | return (short)(-1 * offset - 12288); 100 | } 101 | 102 | private short encodeFlashOffset(short offset) { 103 | // Flash buffer offsets are given raw 104 | return offset; 105 | } 106 | 107 | /** 108 | * Allows the state manager to use APDU lower byte ranges by telling it where the read cursor is. 109 | * Note that the write cursor may not be moved backwards: once the lower APDU buffer is expanded, it cannot shrink 110 | * until the APDU is entirely cleared and the buffer manager reset. 111 | * 112 | * @param apdu Request/response object 113 | * @param amt The position of the read cursor - all bytes below this will be made available for scratch memory 114 | */ 115 | public void informAPDUBufferAvailability(APDU apdu, short amt) { 116 | if (amt > 0xFF) { 117 | amt = 0xFF; 118 | } 119 | final byte[] apduBuf = apdu.getBuffer(); 120 | final short apduBufLen = getAPDUBufferLength(apduBuf); 121 | short apLowerSpace = (short)(0xFF & apduBuf[(short)(apduBufLen - 4)]); 122 | if (amt > apLowerSpace) { 123 | apduBuf[(short)(apduBufLen - 4)] = (byte) amt; 124 | } 125 | } 126 | 127 | private short getAPDUBufferLength(byte[] apduBuf) { 128 | final short apduBufferLength = (short) apduBuf.length; 129 | if (apduBufferLength < 0 || apduBufferLength > 8096) { 130 | // We can't really make use of huge buffers, and our offsets only work if the upper APDU buffer is 131 | // positioned at a reasonably small offset 132 | return 8095; 133 | } 134 | return apduBufferLength; 135 | } 136 | 137 | /** 138 | * Sets up the state manager to use upper+lower APDU ranges. Must be called prior to APDU memory allocations. 139 | * 140 | * @param apdu Request/response object 141 | */ 142 | public void initializeAPDU(APDU apdu) { 143 | final byte[] apduBuf = apdu.getBuffer(); 144 | final short apduBufferLength = getAPDUBufferLength(apduBuf); 145 | 146 | if (apdu.getIncomingLength() < (short)(apduBufferLength - 2)) { 147 | Util.setShort(apduBuf, (short)(apduBufferLength - 2), (short) 4); // the four state-keeping bytes 148 | } 149 | apduBuf[(short)(apduBufferLength - 3)] = 0x00; 150 | apduBuf[(short)(apduBufferLength - 4)] = 0x00; 151 | } 152 | 153 | /** 154 | * Gets an opaque memory allocation handle. Throws an exception if sufficient space is not available. 155 | * 156 | * @param apdu Request/response object 157 | * @param amt Number of bytes to allocate in a contiguous region 158 | * @param allowedLocations Bitfield representing where the memory may be placed 159 | * 160 | * @return Opaque handle which may be passed to other functions to get useful information or free 161 | */ 162 | public short allocate(APDU apdu, short amt, byte allowedLocations) { 163 | final short lc = apdu.getIncomingLength(); 164 | final byte[] apduBuf = apdu.getBuffer(); 165 | final short apduBufLen = getAPDUBufferLength(apduBuf); 166 | short upperAPDUUsed = 0; 167 | if (lc < (short)(apduBufLen - 4)) { 168 | if ((allowedLocations & UPPER_APDU) != 0) { 169 | // Upper APDU buffer available potentially 170 | upperAPDUUsed = Util.getShort(apduBuf, (short) (apduBufLen - 2)); 171 | short totalUpper = (short) (apduBufLen - lc); 172 | if ((short) (totalUpper - upperAPDUUsed) > amt) { 173 | // We fit in the upper APDU buffer! 174 | short offset = (short) (apduBufLen - upperAPDUUsed - amt - 1); 175 | Util.setShort(apduBuf, (short) (apduBufLen - 2), (short) (upperAPDUUsed + amt)); 176 | return encodeUpperAPDUOffset(offset); 177 | } 178 | } 179 | 180 | if ((allowedLocations & LOWER_APDU) != 0) { 181 | short apLowerUsed = (short) (0xFF & apduBuf[(short) (apduBufLen - 3)]); 182 | short apLowerSpace = (short) (0xFF & apduBuf[(short) (apduBufLen - 4)]); 183 | if (amt <= (short) (apLowerSpace - apLowerUsed)) { 184 | // Lower APDU buffer has room 185 | if ((short) (apLowerUsed + amt) <= (short) (apduBufLen - upperAPDUUsed)) { 186 | // ... and it doesn't overlap the already-allocated part of the upper APDU buffer 187 | apduBuf[(short) (apduBufLen - 3)] += amt; 188 | return encodeLowerAPDUOffset(apLowerUsed); 189 | } 190 | } 191 | } 192 | } 193 | 194 | if ((allowedLocations & MEMORY_BUFFER) != 0) { 195 | short mbUsed = Util.getShort(inMemoryBuffer, OFFSET_MEMBUF_USED_SPACE); 196 | if (amt <= (short) (inMemoryBuffer.length - mbUsed)) { 197 | // Memory buffer has room 198 | Util.setShort(inMemoryBuffer, OFFSET_MEMBUF_USED_SPACE, 199 | (short) (mbUsed + amt)); 200 | return encodeMemoryBufferOffset(mbUsed); 201 | } 202 | } 203 | 204 | if ((allowedLocations & FLASH) != 0) { 205 | short apos = Util.getShort(inMemoryBuffer, OFFSET_FLASH_USED_SPACE); 206 | if (amt <= (short) (flashBuffer.length - apos)) { 207 | // Flash it is... 208 | Util.setShort(inMemoryBuffer, OFFSET_FLASH_USED_SPACE, 209 | (short) (apos + amt)); 210 | return encodeFlashOffset(apos); 211 | } 212 | } 213 | 214 | // No room anywhere... 215 | ISOException.throwIt(ISO7816.SW_FILE_FULL); 216 | return 0; // unreachable, but javac doesn't realize that... 217 | } 218 | 219 | /** 220 | * Release a previous allocation 221 | * 222 | * @param apdu Request/response object 223 | * @param handle Handle returned from allocate call 224 | * @param amt Size of allocation in bytes - must match what was passed to allocate call 225 | */ 226 | public void release(APDU apdu, short handle, short amt) { 227 | if (handle < 0) { 228 | if (handle <= -12288) { 229 | Util.setShort(inMemoryBuffer, OFFSET_MEMBUF_USED_SPACE, 230 | (short)(Util.getShort(inMemoryBuffer, OFFSET_MEMBUF_USED_SPACE) - amt)); 231 | return; 232 | } 233 | final byte[] apduBuf = apdu.getBuffer(); 234 | final short apduBufLen = getAPDUBufferLength(apduBuf); 235 | if (handle <= -256) { 236 | final short curUsed = Util.getShort(apduBuf, (short)(apduBufLen - 2)); 237 | Util.setShort(apduBuf, (short)(apduBufLen - 2), (short)(curUsed - amt)); 238 | return; 239 | } 240 | apduBuf[(short)(apduBufLen - 3)] -= amt; 241 | return; 242 | } 243 | Util.setShort(inMemoryBuffer, OFFSET_FLASH_USED_SPACE, 244 | (short)(Util.getShort(inMemoryBuffer, OFFSET_FLASH_USED_SPACE) - amt)); 245 | } 246 | 247 | /** 248 | * Gets the buffer which contains the memory for a particular allocation handle 249 | * 250 | * @param apdu Request/response object 251 | * @param handle Result of a previous allocate call 252 | * 253 | * @return Byte array housing the allocated region 254 | */ 255 | public byte[] getBufferForHandle(APDU apdu, short handle) { 256 | if (handle < 0) { 257 | if (handle <= -12288) { 258 | return inMemoryBuffer; 259 | } 260 | // Both upper and lower APDU handles map here 261 | return apdu.getBuffer(); 262 | } 263 | 264 | return flashBuffer; 265 | } 266 | 267 | /** 268 | * Gets the offset within a buffer for a particular allocation handle 269 | * 270 | * @param apdu Request/response object 271 | * @param handle Result of a previous allocate call 272 | * 273 | * @return Offset within the byte array returned by getBufferForHandle at which the allocated memory begins 274 | */ 275 | public short getOffsetForHandle(short handle) { 276 | if (handle < 0) { 277 | if (handle <= -12288) { 278 | return (short)(handle * -1 - 12288); 279 | } 280 | if (handle < -256) { 281 | return (short)(-1 * handle - 257); 282 | } 283 | return (short)(-1 * handle - 1); 284 | } 285 | return handle; 286 | } 287 | 288 | /** 289 | * Wipes internal state of the buffer manager, releasing all non-APDU objects. It's assumed the APDU will be cleared 290 | * between when this call is made and the next memory allocation is requested. 291 | */ 292 | public void clear() { 293 | // We still keep our state variables in memory, so don't reset the used amount to zero... 294 | Util.setShort(inMemoryBuffer, OFFSET_MEMBUF_USED_SPACE, STATE_KEEPING_OVERHEAD); 295 | Util.setShort(inMemoryBuffer, OFFSET_FLASH_USED_SPACE, (short) 0); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/main/java/us/q3q/fido2/CannedCBOR.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | /** 4 | * Pre-packed CBOR objects for convenience and speed 5 | */ 6 | public abstract class CannedCBOR { 7 | // Parameters for canned responses 8 | static final byte[] U2F_V2_RESPONSE = { 9 | 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32 10 | // U 2 F _ V 2 11 | }; 12 | static final byte[] FIDO_2_RESPONSE = { 13 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30 14 | // F I D O _ 2 _ 0 15 | }; 16 | 17 | static final byte[] UVM_EXTENSION_ID = { 18 | 0x75, 0x76, 0x6D, // uvm 19 | }; 20 | 21 | static final byte[] HMAC_SECRET_EXTENSION_ID = { 22 | 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // hmac-secret 23 | }; 24 | 25 | static final byte[] MIN_PIN_LENGTH = { 26 | 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, // minPinLength 27 | }; 28 | 29 | static final byte[] CRED_PROTECT_EXTENSION_ID = { 30 | 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // credProtect 31 | }; 32 | 33 | static final byte[] CRED_BLOB_EXTENSION_ID = { 34 | 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6f, 0x62, // credBlob 35 | }; 36 | 37 | static final byte[] LARGE_BLOB_EXTENSION_ID = { 38 | 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65, 0x79, // largeBlobKey 39 | }; 40 | 41 | static final byte[] VERSIONS_WITH_U2F = { 42 | (byte) 0x84, // array - four items 43 | 0x68, // string - eight bytes long 44 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // FIDO_2_0 45 | 0x68, // string - eight bytes long 46 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // FIDO_2_1 47 | 0x6C, // string - twelve bytes long 48 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, // FIDO_2_1_PRE 49 | 0x66, // string - six bytes long 50 | 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, // U2F_V2 51 | }; 52 | 53 | static final byte[] VERSIONS_WITHOUT_U2F = { 54 | (byte) 0x83, // array - three items 55 | 0x68, // string - eight bytes long 56 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // FIDO_2_0 57 | 0x68, // string - eight bytes long 58 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // FIDO_2_1 59 | 0x6C, // string - twelve bytes long 60 | 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, // FIDO_2_1_PRE 61 | }; 62 | 63 | static final byte[] AUTH_INFO_START = { 64 | 0x02, // map key: extensions 65 | (byte) 0x86, // array - six items 66 | 0x63, // string - three bytes long 67 | 0x75, 0x76, 0x6D, // uvm 68 | 0x68, // string - eight bytes long 69 | 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6f, 0x62, // credBlob 70 | 0x6B, // string - eleven bytes long 71 | 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // credProtect 72 | 0x6B, // string - eleven bytes long 73 | 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // hmac-secret 74 | 0x6C, // string - twelve bytes long 75 | 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65, 0x79, // largeBlobKey 76 | 0x6C, // string - twelve bytes long 77 | 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, // minPinLength 78 | 0x03, // map key: aaguid 79 | 0x50, // byte string, 16 bytes long 80 | }; 81 | 82 | static final byte[] AUTH_INFO_SECOND = { 83 | 0x62, // string: two bytes long 84 | 0x72, 0x6B, // rk 85 | (byte) 0xF5, // true 86 | 0x62, // string: two bytes long 87 | 0x75, 0x70, // up 88 | (byte) 0xF4, // false 89 | 0x66, // string: six bytes long 90 | 0x75, 0x76, 0x41, 0x63, 0x66, 0x67, // uvAcfg 91 | (byte) 0xF5, // true 92 | 0x68, // string - eight bytes long 93 | 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x76, // alwaysUv 94 | }; 95 | 96 | static final byte[] AUTH_INFO_THIRD = { 97 | 0x68, // string - eight bytes long 98 | 0x63, 0x72, 0x65, 0x64, 0x4d, 0x67, 0x6d, 0x74, // credMgmt 99 | (byte) 0xF5, // true 100 | 0x69, // string: eight bytes long 101 | 0x61, 0x75, 0x74, 0x68, 0x6E, 0x72, 0x43, 0x66, 0x67, // authnrCfg 102 | (byte) 0xF5, // true 103 | 0x69, // string: nine bytes long 104 | 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x69, 0x6e, // clientPin 105 | }; 106 | 107 | static final byte[] MAKE_CRED_UV_NOT_REQD = { 108 | 0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71, 0x64 109 | }; 110 | 111 | static final byte[] SET_MIN_PIN_LENGTH = { 112 | 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, // setMinPINLength 113 | }; 114 | 115 | static final byte[] LARGE_BLOBS = { 116 | 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x73, // largeBlobs 117 | }; 118 | 119 | static final byte[] PIN_UV_AUTH_TOKEN = { 120 | 0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65, 0x6E 121 | }; 122 | static final byte[] MAKE_CREDENTIAL_RESPONSE_PREAMBLE = { 123 | 0x01, // map key: fmt 124 | 0x66, // string - six bytes long 125 | 0x70, 0x61, 0x63, 0x6B, 0x65, 0x64, // packed 126 | 0x02, // map key: authData 127 | }; 128 | static final byte[] PUBLIC_KEY_ALG_PREAMBLE = { 129 | (byte) 0xA5, // map - five entries 130 | 0x01, // map key: kty 131 | 0x02, // integer (2) - means EC2, a two-point elliptic curve key 132 | 0x03, // map key: alg 133 | 0x26, // integer (-7) - means ES256 algorithm 134 | 0x20, // map key: crv 135 | 0x01, // integer (1) - means P256 curve 136 | 0x21, // map key: x-point 137 | 0x58, // byte string with one-byte length next 138 | 0x20 // 32 bytes long 139 | }; 140 | static final byte[] PUBLIC_KEY_DH_ALG_PREAMBLE = { 141 | 0x01, // map key: kty 142 | 0x02, // integer (2) - means EC2, a two-point elliptic curve key 143 | 0x03, // map key: alg 144 | 0x38, 0x18, // integer (-25) - means ECDH with SHA256 hash algorithm 145 | 0x20, // map key: crv 146 | 0x01, // integer (1) - means P256 curve 147 | 0x21, // map key: x-point 148 | 0x58, // byte string with one-byte length next 149 | 0x20 // 32 bytes long 150 | }; 151 | static final byte[] SELF_ATTESTATION_STATEMENT_PREAMBLE = { 152 | 0x03, // map key: attestation statement 153 | (byte) 0xA2, // map - two entries 154 | 0x63, // string - three bytes long 155 | 0x61, 0x6C, 0x67, // alg 156 | 0x26, // integer (-7) - means ES256 algorithm 157 | 0x63, // string: three characters 158 | 0x73, 0x69, 0x67, // sig 159 | }; 160 | 161 | static final byte[] BASIC_ATTESTATION_STATEMENT_PREAMBLE = { 162 | 0x03, // map key: attestation statement 163 | (byte) 0xA3, // map - three entries 164 | 0x63, // string - three bytes long 165 | 0x61, 0x6C, 0x67, // alg 166 | 0x26, // integer (-7) - means ES256 algorithm 167 | 0x63, // string: three characters 168 | 0x73, 0x69, 0x67, // sig 169 | }; 170 | 171 | static final byte[] FIDO_CERT_LEVEL = { 172 | (byte) 0xA1, // map with one entry 173 | 0x64, // string - four bytes long 174 | 0x46, 0x49, 0x44, 0x4F, // FIDO 175 | }; 176 | 177 | static final byte[] X5C = { 178 | 0x63, // string: three characters 179 | 0x78, 0x35, 0x63, // x5c 180 | }; 181 | 182 | static final byte[] SINGLE_ID_MAP_PREAMBLE = { 183 | (byte) 0xA1, // map: one entry 184 | 0x62, // string - two bytes long 185 | 0x69, 0x64, // id 186 | }; 187 | 188 | static final byte[] ID_AND_NAME_MAP_PREAMBLE = { 189 | (byte) 0xA2, // map: two entries 190 | 0x62, // string - two bytes long 191 | 0x69, 0x64, // id 192 | }; 193 | 194 | static final byte[] NAME = { 195 | 0x64, // string: four bytes 196 | 0x6E, 0x61, 0x6D, 0x65 // name 197 | }; 198 | 199 | static final byte[] PUBLIC_KEY_TYPE = { 200 | 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79 201 | // p u b l i c - k e y 202 | }; 203 | 204 | static final byte[] ES256_ALG_TYPE = { 205 | (byte) 0x81, // array - one item 206 | (byte) 0xA2, // map - two entries 207 | 0x63, // string - three bytes long 208 | 0x61, 0x6C, 0x67, // alg 209 | 0x26, // -7 (alg ID for ES256) 210 | 0x64, // string - four bytes long 211 | 0x74, 0x79, 0x70, 0x65, // type 212 | 0x6A, // string - ten bytes long 213 | 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key 214 | }; 215 | 216 | static final byte[] INITIAL_LARGE_BLOB_ARRAY = { // magic hashed encoded empty CBOR array 217 | (byte) 0x80, 0x76, (byte) 0xBE, (byte) 0x8B, 218 | 0x52, (byte) 0x8D, 0x00, 0x75, (byte) 0xF7, 219 | (byte) 0xAA, (byte) 0xE9, (byte) 0x8D, 0x6F, 220 | (byte) 0xA5, 0x7A, 0x6D, 0x3C 221 | }; 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/us/q3q/fido2/FIDOConstants.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | /** 4 | * Constants defined by the FIDO specifications 5 | */ 6 | public abstract class FIDOConstants { 7 | // Command byte values 8 | public static final byte CMD_MAKE_CREDENTIAL = 0x01; 9 | public static final byte CMD_GET_ASSERTION = 0x02; 10 | public static final byte CMD_GET_INFO = 0x04; 11 | public static final byte CMD_CLIENT_PIN = 0x06; 12 | public static final byte CMD_RESET = 0x07; 13 | public static final byte CMD_GET_NEXT_ASSERTION = 0x08; 14 | public static final byte CMD_CREDENTIAL_MANAGEMENT = 0x0A; 15 | public static final byte CMD_AUTHENTICATOR_SELECTION = 0x0B; 16 | public static final byte CMD_LARGE_BLOBS = 0x0C; 17 | public static final byte CMD_AUTHENTICATOR_CONFIG = 0x0D; 18 | public static final byte CMD_CREDENTIAL_MANAGEMENT_PREVIEW = 0x41; 19 | 20 | /** 21 | * "Vendor" command, non-FIDO-standard: install attestation certificates 22 | */ 23 | public static final byte CMD_INSTALL_CERTS = 0x46; 24 | 25 | 26 | // Client pin subCommands 27 | public static final byte CLIENT_PIN_GET_RETRIES = 0x01; 28 | public static final byte CLIENT_PIN_GET_KEY_AGREEMENT = 0x02; 29 | public static final byte CLIENT_PIN_SET_PIN = 0x03; 30 | public static final byte CLIENT_PIN_CHANGE_PIN = 0x04; 31 | public static final byte CLIENT_PIN_GET_PIN_TOKEN = 0x05; 32 | public static final byte CLIENT_PIN_GET_PIN_TOKEN_USING_UV_WITH_PERMISSIONS = 0x06; 33 | public static final byte CLIENT_PIN_GET_PIN_TOKEN_USING_PIN_WITH_PERMISSIONS = 0x09; 34 | 35 | // Credential management subcommands 36 | public static final byte CRED_MGMT_GET_CREDS_META = 0x01; 37 | public static final byte CRED_MGMT_ENUMERATE_RPS_BEGIN = 0x02; 38 | public static final byte CRED_MGMT_ENUMERATE_RPS_NEXT = 0x03; 39 | public static final byte CRED_MGMT_ENUMERATE_CREDS_BEGIN = 0x04; 40 | public static final byte CRED_MGMT_ENUMERATE_CREDS_NEXT = 0x05; 41 | public static final byte CRED_MGMT_DELETE_CRED = 0x06; 42 | public static final byte CRED_MGMT_UPDATE_USER_INFO = 0x07; 43 | 44 | // Authenticator config subcommands 45 | public static final byte AUTH_CONFIG_ENABLE_ENTERPRISE_ATTESTATION = 0x01; 46 | public static final byte AUTH_CONFIG_TOGGLE_ALWAYS_UV = 0x02; 47 | public static final byte AUTH_CONFIG_SET_MIN_PIN_LENGTH = 0x03; 48 | 49 | // PIN token permissions 50 | public static final byte PERM_MAKE_CREDENTIAL = 0x01; 51 | public static final byte PERM_GET_ASSERTION = 0x02; 52 | public static final byte PERM_CRED_MANAGEMENT = 0x04; 53 | public static final byte PERM_BIO_ENROLLMENT = 0x08; 54 | public static final byte PERM_LARGE_BLOB_WRITE = 0x10; 55 | public static final byte PERM_AUTH_CONFIG = 0x20; 56 | 57 | // Error (and OK) responses 58 | public static final byte CTAP2_OK = 0x00; // Indicates successful response. 59 | public static final byte CTAP1_ERR_INVALID_COMMAND = 0x01; // The command is not a valid CTAP command. 60 | public static final byte CTAP1_ERR_INVALID_PARAMETER = 0x02; // The command included an invalid parameter. 61 | public static final byte CTAP1_ERR_INVALID_LENGTH = 0x03; // Invalid message or item length. 62 | public static final byte CTAP1_ERR_INVALID_SEQ = 0x04; // Invalid message sequencing. 63 | public static final byte CTAP1_ERR_TIMEOUT = 0x05; // Message timed out. 64 | public static final byte CTAP1_ERR_CHANNEL_BUSY = 0x06; // Channel busy. 65 | public static final byte CTAP1_ERR_LOCK_REQUIRED = 0x0A; // Command requires channel lock. 66 | public static final byte CTAP1_ERR_INVALID_CHANNEL = 0x0B; // Command not allowed on this cid. 67 | public static final byte CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11; // Invalid/unexpected CBOR error. 68 | public static final byte CTAP2_ERR_INVALID_CBOR = 0x12; // Error when parsing CBOR. 69 | public static final byte CTAP2_ERR_MISSING_PARAMETER = 0x14; // Missing non-optional parameter. 70 | public static final byte CTAP2_ERR_LIMIT_EXCEEDED = 0x15; // Limit for number of items exceeded. 71 | public static final byte CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18; // Large blob storage is full 72 | public static final byte CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19; // Valid credential found in the exclude list. 73 | public static final byte CTAP2_ERR_PROCESSING = 0x21; // Processing (Lengthy operation is in progress). 74 | public static final byte CTAP2_ERR_INVALID_CREDENTIAL = 0x22; // Credential not valid for the authenticator. 75 | public static final byte CTAP2_ERR_USER_ACTION_PENDING = 0x23; // Authentication is waiting for user interaction. 76 | public static final byte CTAP2_ERR_OPERATION_PENDING = 0x24; // Processing, lengthy operation is in progress. 77 | public static final byte CTAP2_ERR_NO_OPERATIONS = 0x25; // No request is pending. 78 | public static final byte CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26; // Authenticator does not support requested algorithm. 79 | public static final byte CTAP2_ERR_OPERATION_DENIED = 0x27; // Not authorized for requested operation. 80 | public static final byte CTAP2_ERR_KEY_STORE_FULL = 0x28; // Internal key storage is full. 81 | public static final byte CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B; // Unsupported option. 82 | public static final byte CTAP2_ERR_INVALID_OPTION = 0x2C; // Not a valid option for current operation. 83 | public static final byte CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D; // Pending keep alive was cancelled. 84 | public static final byte CTAP2_ERR_NO_CREDENTIALS = 0x2E; // No valid credentials provided. 85 | public static final byte CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F; // Timeout waiting for user interaction. 86 | public static final byte CTAP2_ERR_NOT_ALLOWED = 0x30; // Continuation command, such as, authenticatorGetNextAssertion not allowed. 87 | public static final byte CTAP2_ERR_PIN_INVALID = 0x31; // PIN Invalid. 88 | public static final byte CTAP2_ERR_PIN_BLOCKED = 0x32; // PIN Blocked. 89 | public static final byte CTAP2_ERR_PIN_AUTH_INVALID = 0x33; // PIN authentication,pinAuth, verification failed. 90 | public static final byte CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34; // PIN authentication,pinAuth, blocked. Requires power recycle to reset. 91 | public static final byte CTAP2_ERR_PIN_NOT_SET = 0x35; // No PIN has been set. 92 | public static final byte CTAP2_ERR_PIN_REQUIRED = 0x36; // PIN is required for the selected operation. 93 | public static final byte CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37; // PIN policy violation. Currently only enforces minimum length. 94 | // 0x38 now RFU 95 | public static final byte CTAP2_ERR_REQUEST_TOO_LARGE = 0x39; // Authenticator cannot handle this request due to memory constraints. 96 | public static final byte CTAP2_ERR_ACTION_TIMEOUT = 0x3A; // The current operation has timed out. 97 | public static final byte CTAP2_ERR_UP_REQUIRED = 0x3B; // User presence is required for the requested operation. 98 | public static final byte CTAP2_ERR_INTEGRITY_FAILURE = 0x3D; // A checksum did not match. 99 | public static final byte CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E; // The requested subcommand is either invalid or not implemented. 100 | public static final byte CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40; // The permissions parameter contains an unauthorized permission. 101 | public static final byte CTAP1_ERR_OTHER = 0x7F; // Other unspecified error. 102 | /** 103 | * HKDF "info" for PIN protocol two HMAC key 104 | */ 105 | static final byte[] CTAP2_HMAC_KEY_INFO = { 106 | 0x43, 0x54, 0x41, 0x50, 0x32, 0x20, 0x48, 0x4D, 0x41, 0x43, 0x20, 0x6B, 0x65, 0x79, 0x01 107 | // C T A P 2 H M A C k e y 108 | }; 109 | /** 110 | * HKDF "info" for PIN protocol two AES key 111 | */ 112 | static final byte[] CTAP2_AES_KEY_INFO = { 113 | 0x43, 0x54, 0x41, 0x50, 0x32, 0x20, 0x41, 0x45, 0x53, 0x20, 0x6B, 0x65, 0x79, 0x01 114 | // C T A P 2 A E S k e y 115 | }; 116 | /** 117 | * HKDF salt - 32 zeros 118 | */ 119 | static final byte[] ZERO_SALT = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/us/q3q/fido2/P256Constants.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | import javacard.security.ECKey; 4 | 5 | /** 6 | * Elliptic curve parameters for curve P256/secp256r1, mandatory for CTAP2 (and U2F) authenticators 7 | */ 8 | public abstract class P256Constants { 9 | private static final byte[] P = { 10 | (byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00, 11 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff, 12 | (byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff 13 | }; 14 | private static final byte[] A = { 15 | (byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00, 16 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff, 17 | (byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xfc 18 | }; 19 | private static final byte[] B = { 20 | 0x5a,(byte)0xc6,0x35,(byte)0xd8,(byte)0xaa,0x3a,(byte)0x93,(byte)0xe7,(byte)0xb3,(byte)0xeb,(byte)0xbd,0x55, 21 | 0x76,(byte)0x98,(byte)0x86,(byte)0xbc,0x65,0x1d,0x06,(byte)0xb0,(byte)0xcc,0x53,(byte)0xb0,(byte)0xf6, 22 | 0x3b,(byte)0xce,0x3c,0x3e,0x27,(byte)0xd2,0x60,0x4b 23 | }; 24 | private static final byte[] G = { 25 | 0x04, 26 | 0x6b,0x17,(byte)0xd1,(byte)0xf2,(byte)0xe1,0x2c,0x42,0x47,(byte)0xf8,(byte)0xbc,(byte)0xe6, 27 | (byte)0xe5,0x63,(byte)0xa4,0x40,(byte)0xf2,0x77,0x03,0x7d,(byte)0x81,0x2d,(byte)0xeb,0x33, 28 | (byte)0xa0,(byte)0xf4,(byte)0xa1,0x39,0x45,(byte)0xd8,(byte)0x98,(byte)0xc2, 29 | (byte)0x96, 30 | 0x4f,(byte)0xe3,0x42,(byte)0xe2,(byte)0xfe,0x1a,0x7f,(byte)0x9b,(byte)0x8e,(byte)0xe7,(byte)0xeb,0x4a, 31 | 0x7c,0x0f,(byte)0x9e,0x16,0x2b,(byte)0xce,0x33,0x57,0x6b,0x31,0x5e,(byte)0xce, 32 | (byte)0xcb,(byte)0xb6,0x40,0x68,0x37,(byte)0xbf,0x51,(byte)0xf5 33 | }; 34 | private static final byte[] N = { 35 | (byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,0x00,0x00,0x00,0x00,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff, 36 | (byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xbc,(byte)0xe6,(byte)0xfa,(byte)0xad,(byte)0xa7,0x17,(byte)0x9e,(byte)0x84, 37 | (byte)0xf3,(byte)0xb9,(byte)0xca,(byte)0xc2,(byte)0xfc,0x63,0x25,0x51 38 | }; 39 | private static final byte H = 0x01; 40 | 41 | static void setCurve(ECKey key) { 42 | key.setFieldFP(P, (short) 0, (short) P.length); 43 | key.setA(A, (short) 0, (short) A.length); 44 | key.setB(B, (short) 0, (short) B.length); 45 | key.setG(G, (short) 0, (short) G.length); 46 | key.setR(N, (short) 0, (short) N.length); 47 | key.setK(H); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/us/q3q/fido2/PinRetryCounter.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | /** 4 | * Flash-stored counter for managing PIN retries. Can only be decremented, read, or reset. 5 | * 6 | * Attempts to avoid writing the same counter every time by rotating around an array. 7 | * This will be counterproductive (no write amplification, but runtime overhead) on a system with effective 8 | * wear leveling, but will greatly (up to 32x) extend the lifetime on systems without it. 9 | */ 10 | public final class PinRetryCounter { 11 | private final byte[] counters; 12 | private final byte defaultValue; 13 | 14 | public PinRetryCounter(byte defaultValue) { 15 | counters = new byte[64]; 16 | this.defaultValue = defaultValue; 17 | counters[0] = defaultValue; 18 | for (short i = 1; i < counters.length; i++) { 19 | counters[i] = (byte)(defaultValue + 1); 20 | } 21 | } 22 | 23 | /** 24 | * Finds which counter is currently valid 25 | * 26 | * @return The index of the first "valid" (not set to defaultValue+1) counter in the array 27 | */ 28 | private short getValidCounterIdx() { 29 | // Scan along array until we get to a counter that's valid 30 | short counterIdx = 0; 31 | for (counterIdx = 0; counterIdx < counters.length; counterIdx++) { 32 | if (counters[counterIdx] != (byte)(defaultValue + 1)) { 33 | break; 34 | } 35 | } 36 | return counterIdx; 37 | } 38 | 39 | /** 40 | * Prepare to decrement or fetch the retry counter 41 | * 42 | * @return A value that can be used to fetch, reset, or decrement the counter 43 | */ 44 | public short prepareIndex() { 45 | return getValidCounterIdx(); 46 | } 47 | 48 | public byte getRetryCount(short prepareResult) { 49 | return counters[prepareResult]; 50 | } 51 | 52 | /** 53 | * Decrement the retry counter 54 | * 55 | * @param prepareResult The return value of the prepareToDecrement call 56 | */ 57 | public void decrement(short prepareResult) { 58 | counters[prepareResult]--; 59 | } 60 | 61 | /** 62 | * Resets the counter back to its default value 63 | */ 64 | public void reset(short prepareResult) { 65 | short nextCounterIdx = (short)(prepareResult + 1); 66 | if (nextCounterIdx >= counters.length) { 67 | // Wraparound 68 | nextCounterIdx = 0; 69 | } 70 | 71 | // Ordering is important: set up the new counter value, THEN clear out the newly-obsoleted one 72 | counters[nextCounterIdx] = defaultValue; 73 | counters[prepareResult] = (byte)(defaultValue + 1); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/us/q3q/fido2/SigOpCounter.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | import javacard.framework.JCSystem; 4 | 5 | /** 6 | * 32-bit always-increasing counter that avoids writing to the same flash location too often 7 | * 8 | * Uses 67 bytes of flash to provide a 32-bit counter where the most-written byte is written 256 9 | * times in a row, but only takes 1/64 of the overall write load. 10 | */ 11 | public final class SigOpCounter { 12 | private final byte[] firstBytes; 13 | private final byte[] lastBytes; 14 | 15 | public SigOpCounter() { 16 | firstBytes = new byte[3]; 17 | lastBytes = new byte[64]; 18 | } 19 | 20 | /** 21 | * Atomically increase the counter value 22 | * 23 | * @param amt Amount by which to increase the counter 24 | * @return false if the counter has hit max - true otherwise 25 | */ 26 | public boolean increment(short amt) { 27 | boolean ok = false; 28 | 29 | JCSystem.beginTransaction(); 30 | short lbIdx = (short)(0x3F & firstBytes[2]); 31 | try { 32 | short curLast = (short)(lastBytes[lbIdx] & 0xFF); 33 | short amountLeftInLast = (short)(0x00FF - curLast); 34 | if (amountLeftInLast < amt) { 35 | // Increase the higher-order bytes and move to next lower-order byte slot 36 | if (firstBytes[2] == (byte) 0xFF) { 37 | if (firstBytes[1] == (byte) 0xFF) { 38 | if (firstBytes[0] == (byte) 0xFF) { 39 | // Completely full up. 40 | return false; 41 | } 42 | firstBytes[0]++; 43 | firstBytes[1] = 0; 44 | } else { 45 | firstBytes[1]++; 46 | } 47 | firstBytes[2] = 0; 48 | } else { 49 | firstBytes[2]++; 50 | } 51 | lastBytes[(short)(0x3F & firstBytes[2])] = (byte)(amt - amountLeftInLast); 52 | } else { 53 | // Straightforward increase of lower-order byte 54 | lastBytes[lbIdx] = (byte)(lastBytes[lbIdx] + amt); 55 | } 56 | ok = true; 57 | } finally { 58 | if (ok) { 59 | JCSystem.commitTransaction(); 60 | } else { 61 | JCSystem.abortTransaction(); 62 | } 63 | } 64 | 65 | return true; 66 | } 67 | 68 | /** 69 | * Packs counter as a 32-bit (4 byte) integer into the output 70 | * 71 | * @param outBuf Buffer into which to encode counter 72 | * @param outOffset Offset at which to start writing counter 73 | */ 74 | public void pack(byte[] outBuf, short outOffset) { 75 | outBuf[outOffset++] = firstBytes[0]; 76 | outBuf[outOffset++] = firstBytes[1]; 77 | outBuf[outOffset++] = firstBytes[2]; 78 | outBuf[outOffset++] = lastBytes[(short)(0x3F & firstBytes[2])]; 79 | } 80 | 81 | /** 82 | * Returns true if the counter still has its initial value 83 | * 84 | * @return true if the counter is zero 85 | */ 86 | public boolean isZero() { 87 | return (firstBytes[0] == 0 && firstBytes[1] == 0 88 | && firstBytes[2] == 0 && lastBytes[0] == 0); 89 | } 90 | 91 | /** 92 | * Resets counter for new use. Does not start its own transaction - use within an existing one! 93 | */ 94 | public void clear() { 95 | firstBytes[0] = 0; 96 | firstBytes[1] = 0; 97 | firstBytes[2] = 0; 98 | lastBytes[0] = 0; // other lastByte entries will be zeroed again when we reach them in the natural course 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/us/q3q/fido2/AppletBasicTest.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | import com.licel.jcardsim.smartcardio.CardSimulator; 4 | import com.licel.jcardsim.utils.AIDUtil; 5 | 6 | import javacard.framework.AID; 7 | import javacard.framework.ISO7816; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import javax.smartcardio.CommandAPDU; 13 | import javax.smartcardio.ResponseAPDU; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | 19 | /** 20 | * Example (only example) unit tests with jcardsim 21 | */ 22 | public class AppletBasicTest { 23 | 24 | CardSimulator simulator; 25 | AID appletAID = AIDUtil.create("A0000006472F0001"); 26 | AID randoAID = AIDUtil.create("F100900001"); 27 | AID randoLongAID = AIDUtil.create("F100900001AAAAAAAAAAAA"); 28 | 29 | @BeforeEach 30 | public void setupApplet() { 31 | simulator = new CardSimulator(); 32 | 33 | simulator.installApplet(appletAID, FIDO2Applet.class); 34 | 35 | simulator.selectApplet(appletAID); 36 | } 37 | 38 | private ResponseAPDU sendCTAP(String hexCommand) { 39 | int[] bparams = new int[hexCommand.length() / 2]; 40 | for (int i = 0; i < bparams.length; i++) { 41 | bparams[i] = ((Character.digit(hexCommand.charAt(i*2), 16) << 4) 42 | + Character.digit(hexCommand.charAt(i*2+1), 16)); 43 | } 44 | return sendCTAP(bparams); 45 | } 46 | 47 | private ResponseAPDU send(byte[] bparams) { 48 | CommandAPDU commandAPDU = new CommandAPDU(bparams); 49 | ResponseAPDU response = simulator.transmitCommand(commandAPDU); 50 | 51 | ArrayList prevResponses = new ArrayList<>(); 52 | int totalResponseLen = response.getNr(); 53 | prevResponses.add(response); 54 | while (response.getSW() >= ISO7816.SW_BYTES_REMAINING_00 && response.getSW() < ISO7816.SW_BYTES_REMAINING_00 + 256 && totalResponseLen < 65537) { 55 | // Chaining... chained response... 56 | CommandAPDU nextADPU = new CommandAPDU(new byte[] {0x00, (byte) 0xC0, 0x00, 0x00}); 57 | response = simulator.transmitCommand(nextADPU); 58 | prevResponses.add(response); 59 | totalResponseLen += response.getData().length; 60 | } 61 | 62 | byte[] combinedBB = new byte[totalResponseLen + 2]; 63 | ResponseAPDU lastResponse = prevResponses.get(prevResponses.size() - 1); 64 | 65 | int off = 0; 66 | for (int i = 0; i < prevResponses.size(); i++) { 67 | byte[] b = prevResponses.get(i).getData(); 68 | for (int j = 0; j < b.length; j++) { 69 | combinedBB[off++] = b[j]; 70 | } 71 | } 72 | 73 | combinedBB[off++] = (byte) lastResponse.getSW1(); 74 | combinedBB[off++] = (byte) lastResponse.getSW2(); 75 | 76 | return new ResponseAPDU(combinedBB); 77 | } 78 | 79 | private ResponseAPDU send(int... params) { 80 | byte[] bparams = new byte[params.length]; 81 | for (int i = 0; i < params.length; i++) { 82 | bparams[i] = (byte) params[i]; 83 | } 84 | 85 | return send(bparams); 86 | } 87 | 88 | private ResponseAPDU sendCTAP(int... vals) { 89 | boolean shortLen = vals.length <= 255; 90 | int[] framedVals = new int[vals.length + (shortLen ? 6 : 7)]; // Hmm, why isn't this 8 for extended length? 91 | framedVals[0] = 0x80; 92 | framedVals[1] = 0x10; 93 | framedVals[2] = 0x00; 94 | framedVals[3] = 0x00; 95 | if (shortLen) { 96 | framedVals[4] = (byte) vals.length; 97 | } else { 98 | framedVals[4] = 0x00; 99 | framedVals[5] = (vals.length & 0xFF00) >> 8; 100 | framedVals[6] = vals.length & 0x00FF; 101 | } 102 | System.arraycopy(vals, 0, framedVals, shortLen ? 5 : 7, vals.length); 103 | framedVals[framedVals.length - 1] = 0x00; 104 | return send(framedVals); 105 | } 106 | 107 | @Test 108 | public void checkIncorrectCLA() { 109 | ResponseAPDU response = send(0x00, 0x09, 0x00, 0x00); 110 | 111 | assertEquals(ISO7816.SW_CLA_NOT_SUPPORTED, response.getSW()); 112 | } 113 | 114 | @Test 115 | public void checkIncorrectINS() { 116 | ResponseAPDU response = send(0x80, 0x01, 0x00, 0x00); 117 | 118 | assertEquals(ISO7816.SW_INS_NOT_SUPPORTED, response.getSW()); 119 | } 120 | 121 | @Test 122 | public void checkIncorrectP1() { 123 | ResponseAPDU response = send(0x80, 0x10, 0x01, 0x00); 124 | 125 | assertEquals(ISO7816.SW_INCORRECT_P1P2, response.getSW()); 126 | } 127 | 128 | @Test 129 | public void checkIncorrectP2() { 130 | ResponseAPDU response = send(0x80, 0x10, 0x00, 0x01); 131 | 132 | assertEquals(ISO7816.SW_INCORRECT_P1P2, response.getSW()); 133 | } 134 | 135 | @Test 136 | public void checkUnknownCTAPCommand() { 137 | ResponseAPDU response = sendCTAP(0x99); 138 | 139 | assertEquals(ISO7816.SW_NO_ERROR, (short) response.getSW()); 140 | 141 | byte[] data = response.getData(); 142 | assertEquals(1, data.length); 143 | assertEquals(FIDOConstants.CTAP1_ERR_INVALID_COMMAND, data[0]); 144 | } 145 | 146 | @Test 147 | public void checkVersionInSelectionResponse() { 148 | byte[] resp = simulator.selectAppletWithResult(appletAID); 149 | 150 | short recvdStatus = (short) (resp[resp.length - 2] * 256 + resp[resp.length - 1]); 151 | 152 | assertEquals(ISO7816.SW_NO_ERROR, recvdStatus); 153 | 154 | byte[] respWithoutStatus = new byte[resp.length-2]; 155 | System.arraycopy(resp, 0, respWithoutStatus, 0, resp.length-2); 156 | assertEquals("FIDO_2_0", new String(respWithoutStatus)); 157 | } 158 | 159 | @Test 160 | public void checkIgnoreSelectingIncorrectAID() { 161 | byte[] resp = simulator.selectAppletWithResult(appletAID); 162 | short recvdStatus = (short) (resp[resp.length - 2] * 256 + resp[resp.length - 1]); 163 | 164 | assertEquals(ISO7816.SW_NO_ERROR, recvdStatus); 165 | 166 | ResponseAPDU responseAPDU = send(AIDUtil.select(randoAID)); 167 | assertEquals(ISO7816.SW_FILE_NOT_FOUND, responseAPDU.getSW()); 168 | } 169 | 170 | @Test 171 | public void checkIgnoreSelectingIncorrectLongAID() { 172 | byte[] resp = simulator.selectAppletWithResult(appletAID); 173 | short recvdStatus = (short) (resp[resp.length - 2] * 256 + resp[resp.length - 1]); 174 | 175 | assertEquals(ISO7816.SW_NO_ERROR, recvdStatus); 176 | 177 | ResponseAPDU responseAPDU = send(AIDUtil.select(randoLongAID)); 178 | assertEquals(ISO7816.SW_FILE_NOT_FOUND, responseAPDU.getSW()); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/test/java/us/q3q/fido2/VSim.java: -------------------------------------------------------------------------------- 1 | package us.q3q.fido2; 2 | 3 | import com.licel.jcardsim.base.Simulator; 4 | import com.licel.jcardsim.remote.VSmartCard; 5 | import com.licel.jcardsim.utils.AIDUtil; 6 | import javacard.framework.AID; 7 | 8 | import java.lang.reflect.Field; 9 | 10 | /** 11 | * Launches jcardsim with VSmartCard connectivity 12 | */ 13 | public class VSim { 14 | 15 | static final AID appletAID = AIDUtil.create("A0000006472F0001"); 16 | static final int PORT = 35963; 17 | 18 | public static Simulator startBackgroundSimulator() throws Exception { 19 | System.setProperty("com.licel.jcardsim.vsmartcard.reloader.port", "" + PORT); 20 | System.setProperty("com.licel.jcardsim.vsmartcard.reloader.delay", "1000"); 21 | 22 | VSmartCard sc = new VSmartCard("127.0.0.1", PORT); 23 | 24 | // The JCardSim VSmartCard class doesn't natively support loading applets at startup... 25 | // ... and it also doesn't provide access to the Simulator class necessary to do that! 26 | // To avoid needing to patch VCardSim, we'll violate Java member visibility rules 27 | // and reach directly into the class to install our applet. 28 | Field f = sc.getClass().getDeclaredField("sim"); 29 | f.setAccessible(true); 30 | return (Simulator) f.get(sc); 31 | } 32 | 33 | public static synchronized void installApplet(Simulator sim, byte[] params) { 34 | if (params.length > 255) { 35 | throw new IllegalArgumentException("Install parameters too long!"); 36 | } 37 | sim.installApplet(appletAID, FIDO2Applet.class, params, (short) 0, (byte) params.length); 38 | sim.selectApplet(appletAID); 39 | } 40 | 41 | public static Simulator startForegroundSimulator() { 42 | return new Simulator(); 43 | } 44 | 45 | public static synchronized byte[] transmitCommand(Simulator sim, byte[] command) { 46 | return sim.transmitCommand(command); 47 | } 48 | 49 | public static synchronized void softReset(Simulator sim) { 50 | sim.reset(); 51 | sim.selectApplet(appletAID); 52 | } 53 | 54 | public static void main(String[] args) throws Exception { 55 | Simulator sim = startBackgroundSimulator(); 56 | 57 | installApplet(sim, new byte[0]); 58 | } 59 | 60 | } 61 | --------------------------------------------------------------------------------