diff --git a/.editorconfig b/.editorconfig index 66effa89..f34250c1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -35,8 +35,7 @@ indent_size = 4 indent_style = space tab_width = 4 guidelines = 100, 120 -guidelines_style = -2.5px solid 40ff0000 +guidelines_style = 2.5px solid 40ff0000 # New line preferences end_of_line = crlf diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.yml b/.github/ISSUE_TEMPLATE/2-bug-report.yml index 19d38f7c..3fb2fc7b 100644 --- a/.github/ISSUE_TEMPLATE/2-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/2-bug-report.yml @@ -44,7 +44,7 @@ body: attributes: label: Version description: What version of our SDK are you using? - placeholder: 1.10.0 + placeholder: 1.11.0 validations: required: true - type: input @@ -52,7 +52,7 @@ body: attributes: label: Version description: What version of our firmware are you running? - placeholder: 5.4.3 + placeholder: 5.7.0 validations: required: true - type: textarea diff --git a/.github/workflows/verify-code-style.yml b/.github/workflows/verify-code-style.yml index ba3f7474..65e1288f 100644 --- a/.github/workflows/verify-code-style.yml +++ b/.github/workflows/verify-code-style.yml @@ -1,71 +1,71 @@ -# Copyright 2021 Yubico AB -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# # Copyright 2021 Yubico AB +# # +# # Licensed under the Apache License, Version 2.0 (the "License"); +# # you may not use this file except in compliance with the License. +# # You may obtain a copy of the License at +# # +# # http://www.apache.org/licenses/LICENSE-2.0 +# # +# # Unless required by applicable law or agreed to in writing, software +# # distributed under the License is distributed on an "AS IS" BASIS, +# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# # See the License for the specific language governing permissions and +# # limitations under the License. -name: Check code formatting +# name: Verify code style -on: - pull_request: - branches: - - 'main' - - 'develop**' - - 'release/**' - paths: - - '**.h' - - '**.c' - - '**.cs' - - '**.csproj' - - '**.sln' - - '.github/workflows/check-code-formatting.yml' +# on: +# pull_request: +# branches: +# - 'main' +# - 'develop**' +# - 'release/**' +# paths: +# - '**.h' +# - '**.c' +# - '**.cs' +# - '**.csproj' +# - '**.sln' +# - '.github/workflows/check-code-formatting.yml' -jobs: - verify-code-style: - name: "Verify code style" - runs-on: windows-latest +# jobs: +# verify-code-style: +# name: "Verify code style" +# runs-on: windows-latest - steps: - - uses: actions/checkout@v4 +# steps: +# - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - global-json-file: global.json +# - uses: actions/setup-dotnet@v4 +# with: +# global-json-file: global.json - - name: Add local NuGet repository - run: dotnet nuget add source --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/Yubico/index.json" +# - name: Add local NuGet repository +# run: dotnet nuget add source --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/Yubico/index.json" - - name: Build Yubico.NET.SDK.sln - run: dotnet build --nologo --verbosity normal Yubico.NET.SDK.sln +# #- name: Build Yubico.NET.SDK.sln +# # run: dotnet build --nologo --verbosity normal Yubico.NET.SDK.sln - - name: "Add DOTNET to path explicitly to address bug where it cannot be found" - shell: bash - run: | - DOTNET_PATH=$(which dotnet) - if [ -z "$DOTNET_PATH" ]; then - echo "dotnet not found via which, checking /usr/share/dotnet" - # Finding all executables named dotnet and picking the first one - DOTNET_PATH=$(find /usr/share/dotnet -type f -name "dotnet" -executable | head -n 1) - fi +# - name: "Add DOTNET to path explicitly to address bug where it cannot be found" +# shell: bash +# run: | +# DOTNET_PATH=$(which dotnet) +# if [ -z "$DOTNET_PATH" ]; then +# echo "dotnet not found via which, checking /usr/share/dotnet" +# # Finding all executables named dotnet and picking the first one +# DOTNET_PATH=$(find /usr/share/dotnet -type f -name "dotnet" -executable | head -n 1) +# fi - if [ -z "$DOTNET_PATH" ]; then - echo "dotnet executable not found." - exit 1 - else - echo "Using dotnet at $DOTNET_PATH" - DOTNET_DIR=$(dirname $(readlink -f $DOTNET_PATH)) - echo "$DOTNET_DIR" >> $GITHUB_PATH - echo "Added $DOTNET_DIR to GITHUB_PATH" - fi +# if [ -z "$DOTNET_PATH" ]; then +# echo "dotnet executable not found." +# exit 1 +# else +# echo "Using dotnet at $DOTNET_PATH" +# DOTNET_DIR=$(dirname $(readlink -f $DOTNET_PATH)) +# echo "$DOTNET_DIR" >> $GITHUB_PATH +# echo "Added $DOTNET_DIR to GITHUB_PATH" +# fi - - name: Check for correct formatting - run: dotnet format --verify-no-changes --no-restore -v d \ No newline at end of file +# - name: Check for correct formatting +# run: dotnet format --verify-no-changes --no-restore -v d \ No newline at end of file diff --git a/Yubico.NativeShims/CMakeLists.txt b/Yubico.NativeShims/CMakeLists.txt index b1e6cdfd..279c5f3f 100644 --- a/Yubico.NativeShims/CMakeLists.txt +++ b/Yubico.NativeShims/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.13) -project(Yubico.NativeShims VERSION 1.10.1) +project(Yubico.NativeShims VERSION 1.11.0) include(CheckCCompilerFlag) if (APPLE OR UNIX) diff --git a/Yubico.NativeShims/vcpkg.json b/Yubico.NativeShims/vcpkg.json index 8b893120..3a28962a 100644 --- a/Yubico.NativeShims/vcpkg.json +++ b/Yubico.NativeShims/vcpkg.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", "name": "yubico-nativeshims", - "version": "1.10.0", + "version": "1.11.0", "dependencies": [ "openssl" ] diff --git a/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md b/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md index ccd5dba3..dfa9ef72 100644 --- a/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md +++ b/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md @@ -15,6 +15,62 @@ limitations under the License. --> # What's new in the SDK? Here you can find all of the updates and release notes for published versions of the SDK. + +## 1.11.x Releases +### 1.11.0 + +Release date: June 28th, 2024 + +This release introduces significant enhancements and new features for the latest YubiKeys, including support for +firmware version 5.7, which allows for temporary disabling of NFC connectivity and checking PIN complexity status. +It also expands RSA key support in PIV to 3072 and 4096-bit keys, and includes improvements for YubiKey Bio and +Multi-Protocol Edition keys. +Additionally, there are optimizations in USB reclaim speed and adjustments to the touch sensor sensitivity and a few bug +fixes. +Several command classes have been deprecated due to changes in how device info is read by the SDK, and integration test +guardrails have been implemented for better security. + +Features: +- Support for YubiKeys with the latest firmware (version 5.7): + - NFC connectivity can now be temporarily disabled with [SetIsNfcRestricted()](xref:Yubico.YubiKey.YubiKeyDevice.SetIsNfcRestricted%28System.Boolean%29) ([#91](https://github.com/Yubico/Yubico.NET.SDK/pull/91)). + - Additional property pages on the YubiKey are now read into [YubiKeyDeviceInfo](xref:Yubico.YubiKey.YubiKeyDeviceInfo) ([#92](https://github.com/Yubico/Yubico.NET.SDK/pull/92)). + - PIN complexity status can be checked with [IsPinComplexityEnabled](xref:Yubico.YubiKey.YubiKeyDevice.IsPinComplexityEnabled) ([#92](https://github.com/Yubico/Yubico.NET.SDK/pull/92)). + - PIN complexity specific error messages and exceptions ([#112](https://github.com/Yubico/Yubico.NET.SDK/pull/112)). + - The set of YubiKey applications that are capable of being put into FIPS mode can be retrieved with [FipsCapable](xref:Yubico.YubiKey.YubiKeyDevice.FipsCapable). The set of YubiKey applications that are in FIPS mode can be retrieved with [FipsApproved](xref:Yubico.YubiKey.YubiKeyDevice.FipsApproved) ([#92](https://github.com/Yubico/Yubico.NET.SDK/pull/92)). + - The part number for a key’s Secure Element processor, if available, can be retrieved with [PartNumber](xref:Yubico.YubiKey.YubiKeyDevice.PartNumber) ([#92](https://github.com/Yubico/Yubico.NET.SDK/pull/92)). + - The set of YubiKey applications that are blocked from being reset can be retrieved with [ResetBlocked](xref:Yubico.YubiKey.YubiKeyDevice.ResetBlocked) ([#92](https://github.com/Yubico/Yubico.NET.SDK/pull/92)). + - PIV: 3072 and 4096 RSA keys can now be generated and imported ([#100](https://github.com/Yubico/Yubico.NET.SDK/pull/100)). + - PIV: Keys can be moved between the different slots on the YubiKey. Any key except the **attestation key** can be moved from one slot to another ([#103](https://github.com/Yubico/Yubico.NET.SDK/pull/103)). +- Support for YubiKey Bio/Bio Multi-Protocol Edition keys: + - Get bio metadata ([#108](https://github.com/Yubico/Yubico.NET.SDK/pull/108)) + - Added new verification policy enum values (PIN_OR_MATCH_ONCE, PIN_OR_MATCH_ALWAYS) ([#108](https://github.com/Yubico/Yubico.NET.SDK/pull/108)) + - Bio user verification ([#108](https://github.com/Yubico/Yubico.NET.SDK/pull/108)) + - Device Reset ([#110](https://github.com/Yubico/Yubico.NET.SDK/pull/110)) +- The USB reclaim speed, which controls the time it takes to switch from one YubiKey application to another, has been reduced for compatible YubiKeys. To use the previous 3-second reclaim timeout for all keys, see [UseOldReclaimTimeoutBehavior](xref:Yubico.YubiKey.YubiKeyCompatSwitches.UseOldReclaimTimeoutBehavior) ([#93](https://github.com/Yubico/Yubico.NET.SDK/pull/93)). +- The sensitivity of the YubiKey’s capacitive touch sensor can now be temporarily adjusted with [SetTemporaryTouchThreshold](xref:Yubico.YubiKey.YubiKeyDevice.SetTemporaryTouchThreshold%28System.Int32%29) ([#95](https://github.com/Yubico/Yubico.NET.SDK/pull/95)). + +Bug fixes: +- Update ManagementKeyAlgorithm on PIV Application reset ([#105](https://github.com/Yubico/Yubico.NET.SDK/pull/105)). +- Queue macOS input reports so that large responses aren't dropped ([#84](https://github.com/Yubico/Yubico.NET.SDK/pull/84)). +- Default back to old SCardConnect behavior. Reverts the change in behavior to open smart card handles exclusively. Instead now defaults back to shared like it was before, but allows for applications to toggle between the new and old behavior through the use of AppContext.SetSwitch ([#83](https://github.com/Yubico/Yubico.NET.SDK/pull/83)). + +Miscellaneous: +- The way that YubiKey device info is read by the SDK has changed, and as a result, the following GetDeviceInfo command classes have been deprecated ([#91](https://github.com/Yubico/Yubico.NET.SDK/pull/91)): + - [Yubico.YubiKey.Management.Commands.GetDeviceInfoCommand](Yubico.YubiKey.Management.Commands.GetDeviceInfoCommand) + - [Yubico.YubiKey.Otp.Commands.GetDeviceInfoCommand](xref:Yubico.YubiKey.Otp.Commands.GetDeviceInfoCommand) + - [Yubico.YubiKey.U2f.Commands.GetDeviceInfoCommand](xref:Yubico.YubiKey.U2f.Commands.GetDeviceInfoCommand) + - [Yubico.YubiKey.Management.Commands.GetDeviceInfoResponse](xref:Yubico.YubiKey.Management.Commands.GetDeviceInfoResponse) + - [Yubico.YubiKey.Otp.Commands.GetDeviceInfoResponse](xref:Yubico.YubiKey.Otp.Commands.GetDeviceInfoResponse) + - [Yubico.YubiKey.U2f.Commands.GetDeviceInfoResponse](xref:Yubico.YubiKey.U2f.Commands.GetDeviceInfoResponse) +- The correct certificate OID friendly names are now used for ECDsaCng (nistP256) and ECDsaOpenSsl (ECDSA_P256) ([#78](https://github.com/Yubico/Yubico.NET.SDK/pull/78)). +- Integration test guardrails have been added to ensure tests are done only on specified keys. ([#100](https://github.com/Yubico/Yubico.NET.SDK/pull/100)). +- Fixed build issue when compiling `Yubico.NativeShims` on MacOS ([#109](https://github.com/Yubico/Yubico.NET.SDK/pull/109)). +- Run unit tests on all platforms in CI ([#80](https://github.com/Yubico/Yubico.NET.SDK/pull/80)). + +Dependencies: +- Update xUnit and Microsoft.NET.Test.Sdk ([#94](https://github.com/Yubico/Yubico.NET.SDK/pull/94)). + + ## 1.10.x Releases ### 1.10.0 diff --git a/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/pin-complexity-policy.md b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/pin-complexity-policy.md new file mode 100644 index 00000000..c6cbb64b --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/pin-complexity-policy.md @@ -0,0 +1,57 @@ +--- +uid: UsersManualPinComplexityPolicy +--- + + + +# PIN Complexity policy + +Since firmware 5.7, the YubiKey can enforce usage of non-trivial PINs in its applications, this feature has been named _PIN complexity policy_ and is derived from the current Revision 3 of SP 800-63 (specifically SP 800-63B-3) with additional consideration of Revision 4 of SP 800-63 (specifically SP 800-63B-4). + +If PIN complexity has been enforced, the YubiKey will refuse to set or change values of following, if they violate the policy: +- PIV PIN and PUK +- FIDO2 PIN + +That means that simple values such as `11111111`, `password` or `12345678` will be refused. The YubiKey can also be programmed during the pre-registration to refuse other specific values. More information can be found in our online documentation for the firmware version 5.7 additions. + +The SDK has support for getting information about the feature and also a way how to let the client know that an error is related to PIN complexity. + +## Read current PIN complexity status +The PIN complexity enforcement status is part of the `IYubiKeyDeviceInfo` through `bool IsPinComplexityEnabled` property. + +## Handle PIN complexity errors +The SDK can be used to create a variety of applications. If those support setting or changing PINs, they should handle the situation when a YubiKey refuses the user value because it is violating the PIN complexity. + +The SDK communicates this by throwing specific Exceptions. + +### PIV Session +In PIV session the exception thrown during PIN complexity violations is `SecurityException` with a specific message: `ExceptionMessages.PinComplexityViolation`. + +If the application uses `KeyCollectors`, the violation is reported through `KeyEntryData.IsViolatingPinComplexity`. + +The violations are reported for following operations: +- `PivSession.ChangePin()` +- `PivSession.ChangePuk()` +- `PivSession.ResetPin()` + +### FIDO2 Session +In the FIDO2 application, `Fido2Exception` with `Status` of `CtapStatus.PinPolicyViolation` is thrown after a PIN complexity was violated. For `KeyCollectors`, `KeyEntryData.IsViolatingPinComplexity` will be set to `true` for these situations. + +This applies to following `Fido2Session` operations: +- `Fido2Session.SetPin()` +- `Fido2Session.ChangePin()` + +## Example code +You can find examples of code in the `PivSampleCode` and `Fido2SampleCode` examples as well in `PinComplexityTests` integration tests. \ No newline at end of file diff --git a/Yubico.YubiKey/docs/users-manual/toc.yml b/Yubico.YubiKey/docs/users-manual/toc.yml index 9c555698..e26c86fc 100644 --- a/Yubico.YubiKey/docs/users-manual/toc.yml +++ b/Yubico.YubiKey/docs/users-manual/toc.yml @@ -54,6 +54,8 @@ href: sdk-programming-guide/commands.md - name: Device notifications href: sdk-programming-guide/device-notifications.md + - name: PIN Complexity policy + href: sdk-programming-guide/pin-complexity-policy.md - name: "Application: OTP" homepage: application-otp/otp-overview.md diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs b/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs index 890c491d..88b4771d 100644 --- a/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs +++ b/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs @@ -41,6 +41,18 @@ public virtual bool Fido2SampleKeyCollectorDelegate(KeyEntryData keyEntryData) return false; } + if (keyEntryData.IsViolatingPinComplexity) + { + SampleMenu.WriteMessage(MessageType.Special, 0, "The provided value violates PIN complexity."); + + SampleMenu.WriteMessage(MessageType.Title, 0, "Try again? y/n"); + char[] answer = SampleMenu.ReadResponse(out int _); + if (answer.Length == 0 || (answer[0] != 'y' && answer[0] != 'Y')) + { + return false; + } + } + if (keyEntryData.IsRetry) { SampleMenu.WriteMessage(MessageType.Title, 0, "A previous entry was incorrect, do you want to retry?"); @@ -49,7 +61,7 @@ public virtual bool Fido2SampleKeyCollectorDelegate(KeyEntryData keyEntryData) string retryString = ((int)keyEntryData.RetriesRemaining).ToString("D", CultureInfo.InvariantCulture); SampleMenu.WriteMessage(MessageType.Title, 0, - "(retries remainin until blocked: " + retryString + ")"); + "(retries remaining until blocked: " + retryString + ")"); } SampleMenu.WriteMessage(MessageType.Title, 0, "y/n"); diff --git a/Yubico.YubiKey/examples/PivSampleCode/KeyCollector/SampleKeyCollector.cs b/Yubico.YubiKey/examples/PivSampleCode/KeyCollector/SampleKeyCollector.cs index f1d71021..f6bc8e95 100644 --- a/Yubico.YubiKey/examples/PivSampleCode/KeyCollector/SampleKeyCollector.cs +++ b/Yubico.YubiKey/examples/PivSampleCode/KeyCollector/SampleKeyCollector.cs @@ -48,15 +48,17 @@ public bool SampleKeyCollectorDelegate(KeyEntryData keyEntryData) return false; } - if (keyEntryData.IsRetry) + if (keyEntryData.IsViolatingPinComplexity && + !GetUserInputOnPinComplexityViolation(keyEntryData)) { - if (!(keyEntryData.RetriesRemaining is null)) - { - if (GetUserInputOnRetries(keyEntryData) == false) - { - return false; - } - } + return false; + } + + if (keyEntryData.IsRetry && + keyEntryData.RetriesRemaining.HasValue && + !GetUserInputOnRetries(keyEntryData)) + { + return false; } byte[] currentValue; @@ -142,6 +144,20 @@ private bool GetUserInputOnRetries(KeyEntryData keyEntryData) return response == 0; } + private bool GetUserInputOnPinComplexityViolation(KeyEntryData keyEntryData) + { + SampleMenu.WriteMessage(MessageType.Special, 0, "The provided value violates PIN complexity."); + + string title = "Try again?"; + string[] menuItems = new string[] + { + "Yes, try again", + "No, cancel operation" + }; + int response = _menuObject.RunMenu(title, menuItems); + return response == 0; + } + // Collect a value. // The name describes what to collect. // The defaultValueString is a string describing the default value and diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.Pin.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.Pin.cs index 05a36b3a..41411ecc 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.Pin.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.Pin.cs @@ -522,17 +522,27 @@ public bool TrySetPin() try { - if (!keyCollector(keyEntryData)) + while (keyCollector(keyEntryData)) { - return false; // User cancellation - } + try + { + if (TrySetPin(keyEntryData.GetCurrentValue())) + { + return true; + } + } + catch (Fido2Exception e) + { + if (e.Status == CtapStatus.PinPolicyViolation) + { + keyEntryData.IsViolatingPinComplexity = true; + continue; + } - if (TrySetPin(keyEntryData.GetCurrentValue())) - { - return true; + throw; + } + throw new SecurityException(ExceptionMessages.PinAlreadySet); } - - throw new SecurityException(ExceptionMessages.PinAlreadySet); } finally { @@ -541,6 +551,8 @@ public bool TrySetPin() keyEntryData.Request = KeyEntryRequest.Release; _ = keyCollector(keyEntryData); } + + return false; } /// @@ -673,9 +685,22 @@ public bool TryChangePin() { while (keyCollector(keyEntryData)) { - if (TryChangePin(keyEntryData.GetCurrentValue(), keyEntryData.GetNewValue())) + try { - return true; + if (TryChangePin(keyEntryData.GetCurrentValue(), keyEntryData.GetNewValue())) + { + return true; + } + } + catch (Fido2Exception e) + { + if (e.Status == CtapStatus.PinPolicyViolation) + { + keyEntryData.IsViolatingPinComplexity = true; + continue; + } + + throw; } keyEntryData.IsRetry = true; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/KeyEntryData.cs b/Yubico.YubiKey/src/Yubico/YubiKey/KeyEntryData.cs index 6aab5624..436f16a2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/KeyEntryData.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/KeyEntryData.cs @@ -136,6 +136,11 @@ public sealed class KeyEntryData /// public bool IsRetry { get; set; } + /// + /// Indicates if the current request for an item has violated PIN complexity. + /// + public bool IsViolatingPinComplexity { get; set; } + /// /// This is the result of the last fingerprint sample. This will be null /// if the Request is for something other than @@ -221,6 +226,7 @@ public KeyEntryData() _currentValue = Memory.Empty; _newValue = Memory.Empty; IsRetry = false; + IsViolatingPinComplexity = false; } /// @@ -340,6 +346,7 @@ public void Clear() LastBioEnrollSampleResult = null; SignalUserCancel = null; IsRetry = false; + IsViolatingPinComplexity = false; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ChangeReferenceDataResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ChangeReferenceDataResponse.cs index b467fdb0..8c865cf1 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ChangeReferenceDataResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ChangeReferenceDataResponse.cs @@ -164,7 +164,9 @@ public ChangeReferenceDataResponse(ResponseApdu responseApdu) : [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Readability, avoiding nested conditionals.")] public int? GetData() { - if (Status != ResponseStatus.Success && Status != ResponseStatus.AuthenticationRequired) + if (Status != ResponseStatus.Success && + Status != ResponseStatus.AuthenticationRequired && + Status != ResponseStatus.ConditionsNotSatisfied) { throw new InvalidOperationException(StatusMessage); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ResetRetryResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ResetRetryResponse.cs index 086f838d..af326b4a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ResetRetryResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/ResetRetryResponse.cs @@ -162,7 +162,9 @@ public ResetRetryResponse(ResponseApdu responseApdu) : [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Readability, avoiding nested conditionals.")] public int? GetData() { - if (Status != ResponseStatus.Success && Status != ResponseStatus.AuthenticationRequired) + if (Status != ResponseStatus.Success && + Status != ResponseStatus.AuthenticationRequired && + Status != ResponseStatus.ConditionsNotSatisfied) { throw new InvalidOperationException(StatusMessage); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/VerifyUvCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/VerifyUvCommand.cs index 8f9c8d04..4922da6d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/VerifyUvCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/VerifyUvCommand.cs @@ -105,10 +105,10 @@ public CommandApdu CreateCommandApdu() } var tlvWriter = new TlvWriter(); - const byte getTemporaryPinTag = 0x02; + const byte GetTemporaryPinTag = 0x02; if (RequestTemporaryPin) { - tlvWriter.WriteValue(getTemporaryPinTag, null); + tlvWriter.WriteValue(GetTemporaryPinTag, null); } else { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs index ed31475d..c47f7fb7 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs @@ -1266,7 +1266,7 @@ public bool TryResetPin(ReadOnlyMemory puk, ReadOnlyMemory newPin, o // Command/Response operations (Change or Reset). // If the mode is not None, then set the YubiKey to that mode. private bool TryChangeReference(KeyEntryRequest request, - Func CommandResponse, + Func commandResponse, PivPinOnlyMode mode) { if (KeyCollector is null) @@ -1286,7 +1286,7 @@ private bool TryChangeReference(KeyEntryRequest request, { while (KeyCollector(keyEntryData)) { - ResponseStatus status = CommandResponse(keyEntryData); + ResponseStatus status = commandResponse(keyEntryData); if (status == ResponseStatus.Success) { @@ -1302,10 +1302,6 @@ private bool TryChangeReference(KeyEntryRequest request, return true; } - else if (status == ResponseStatus.ConditionsNotSatisfied) - { - _log.LogInformation("PIN complexity violated."); - } if ((keyEntryData.RetriesRemaining ?? 1) == 0) { @@ -1315,7 +1311,14 @@ private bool TryChangeReference(KeyEntryRequest request, ExceptionMessages.NoMoreRetriesRemaining)); } - keyEntryData.IsRetry = true; + if (status == ResponseStatus.ConditionsNotSatisfied) + { + keyEntryData.IsViolatingPinComplexity = true; + } + else + { + keyEntryData.IsRetry = true; + } } } finally @@ -1333,12 +1336,9 @@ private bool TryChangeReference(KeyEntryRequest request, // TryChangeReference. It executes the ChangeReference command and response. private ResponseStatus ChangePinOrPuk(KeyEntryData keyEntryData) { - byte slotNumber = PivSlot.Puk; - - if (keyEntryData.Request == KeyEntryRequest.ChangePivPin) - { - slotNumber = PivSlot.Pin; - } + byte slotNumber = keyEntryData.Request == KeyEntryRequest.ChangePivPin + ? PivSlot.Pin + : PivSlot.Puk; var changeCommand = new ChangeReferenceDataCommand( slotNumber, keyEntryData.GetCurrentValue(), keyEntryData.GetNewValue()); @@ -1385,25 +1385,28 @@ private ResponseStatus ResetPin(KeyEntryData keyEntryData) // If the mgmt key is not authenticated, it will do nothing. private void UpdateAdminData() { - if (ManagementKeyAuthenticated) + if (!ManagementKeyAuthenticated) { - bool isValid = TryReadObject(out AdminData adminData); + _log.LogDebug("Unauthenticated attempt to update AdminData failed."); + return; + } - using (adminData) - { - if (!isValid || adminData.IsEmpty) - { - return; - } + bool isValid = TryReadObject(out AdminData adminData); - if (!adminData.PinProtected && adminData.Salt is null) - { - return; - } + using (adminData) + { + if (!isValid || adminData.IsEmpty) + { + return; + } - adminData.PinLastUpdated = DateTime.UtcNow; - WriteObject(adminData); + if (!adminData.PinProtected && adminData.Salt is null) + { + return; } + + adminData.PinLastUpdated = DateTime.UtcNow; + WriteObject(adminData); } } } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/DeviceResetTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/DeviceResetTests.cs new file mode 100644 index 00000000..31b996b6 --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/DeviceResetTests.cs @@ -0,0 +1,67 @@ +// Copyright 2024 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Text; +using Xunit; +using Yubico.YubiKey.Fido2; +using Yubico.YubiKey.Piv; +using Yubico.YubiKey.TestUtilities; + +namespace Yubico.YubiKey +{ + /// + /// Executes device wide reset and check that PINs are set to default values (where applicable). + /// + /// + /// Device wide reset is only available on YubiKey Bio Multi-protocol Edition devices. + /// + public class DeviceResetTests + { + + private readonly ReadOnlyMemory _defaultPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("123456")); + private readonly ReadOnlyMemory _complexPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("11234567")); + + [SkippableFact(typeof(DeviceNotFoundException))] + public void Reset() + { + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Bio); + Skip.IfNot(testDevice.HasFeature(YubiKeyFeature.DeviceReset), "Device does not support DeviceReset."); + Skip.IfNot(testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Piv), "Device does not support DeviceReset."); + + testDevice.DeviceReset(); + + /* set PIN for PIV - this will also set the FIDO2 PIN */ + using (var pivSession = new PivSession(testDevice)) + { + Assert.True(pivSession.TryChangePin(_defaultPin, _complexPin, out _)); + } + + testDevice.DeviceReset(); + + /* verify that PIV has default PIN */ + using (var pivSession = new PivSession(testDevice)) + { + Assert.True(pivSession.TryVerifyPin(_defaultPin, out _)); + } + + /* verify that FIDO2 does not have a PIN set */ + using (var fido2Session = new Fido2Session(testDevice)) + { + var optionValue = fido2Session.AuthenticatorInfo.GetOptionValue(AuthenticatorOptions.clientPin); + Assert.Equal(OptionValue.False, optionValue); + } + } + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/PinComplexityTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/PinComplexityTests.cs index 08b8a09f..d6d0e533 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/PinComplexityTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/PinComplexityTests.cs @@ -14,48 +14,42 @@ // limitations under the License. using System; -using System.Diagnostics; -using System.IO; using System.Security; using System.Text; -using System.Threading; -using Microsoft.Extensions.Logging; using Xunit; using Yubico.YubiKey.Fido2; -using Yubico.YubiKey.Otp; using Yubico.YubiKey.Piv; using Yubico.YubiKey.TestUtilities; -using Log = Yubico.Core.Logging.Log; namespace Yubico.YubiKey { /// /// Tests device that it will not accept PINs or PUKs which violate PIN complexity - /// Before running the tests reset the device + /// Before running the tests, reset the FIDO2/PIV application on the device /// public class PinComplexityTests { - private readonly ReadOnlyMemory defaultPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("123456")); - private readonly ReadOnlyMemory complexPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("11234567")); - private readonly ReadOnlyMemory invalidPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("33333333")); + private readonly ReadOnlyMemory _defaultPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("123456")); + private readonly ReadOnlyMemory _complexPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("11234567")); + private readonly ReadOnlyMemory _invalidPin = new ReadOnlyMemory(Encoding.ASCII.GetBytes("33333333")); - private readonly ReadOnlyMemory defaultPuk = new ReadOnlyMemory(Encoding.ASCII.GetBytes("12345678")); - private readonly ReadOnlyMemory complexPuk = new ReadOnlyMemory(Encoding.ASCII.GetBytes("11234567")); - private readonly ReadOnlyMemory invalidPuk = new ReadOnlyMemory(Encoding.ASCII.GetBytes("33333333")); + private readonly ReadOnlyMemory _defaultPuk = new ReadOnlyMemory(Encoding.ASCII.GetBytes("12345678")); + private readonly ReadOnlyMemory _complexPuk = new ReadOnlyMemory(Encoding.ASCII.GetBytes("11234567")); + private readonly ReadOnlyMemory _invalidPuk = new ReadOnlyMemory(Encoding.ASCII.GetBytes("33333333")); [SkippableFact] public void ChangePivPinToInvalidValue_ThrowsSecurityException() { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); Skip.IfNot(testDevice.IsPinComplexityEnabled); using var pivSession = new PivSession(testDevice); pivSession.ResetApplication(); - Assert.True(pivSession.TryChangePin(defaultPin, complexPin, out _)); + Assert.True(pivSession.TryChangePin(_defaultPin, _complexPin, out _)); int? retriesRemaining = 3; - var e = Assert.Throws(() => pivSession.TryChangePin(complexPin, invalidPin, out retriesRemaining)); + var e = Assert.Throws(() => pivSession.TryChangePin(_complexPin, _invalidPin, out retriesRemaining)); Assert.Equal(ExceptionMessages.PinComplexityViolation, e.Message); Assert.Null(retriesRemaining); } @@ -63,16 +57,16 @@ public void ChangePivPinToInvalidValue_ThrowsSecurityException() [SkippableFact] public void ChangePivPukToInvalidValue_ThrowsSecurityException() { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); Skip.IfNot(testDevice.IsPinComplexityEnabled); using var pivSession = new PivSession(testDevice); pivSession.ResetApplication(); - Assert.True(pivSession.TryChangePuk(defaultPuk, complexPuk, out _)); + Assert.True(pivSession.TryChangePuk(_defaultPuk, _complexPuk, out _)); int? retriesRemaining = 3; - var e = Assert.Throws(() => pivSession.TryChangePuk(complexPuk, invalidPuk, out retriesRemaining)); + var e = Assert.Throws(() => pivSession.TryChangePuk(_complexPuk, _invalidPuk, out retriesRemaining)); Assert.Equal(ExceptionMessages.PinComplexityViolation, e.Message); Assert.Null(retriesRemaining); } @@ -80,15 +74,15 @@ public void ChangePivPukToInvalidValue_ThrowsSecurityException() [SkippableFact] public void ResetPivPinToInvalidValue_ThrowsSecurityException() { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); Skip.IfNot(testDevice.IsPinComplexityEnabled); using var pivSession = new PivSession(testDevice); pivSession.ResetApplication(); - Assert.True(pivSession.TryResetPin(defaultPuk, complexPin, out _)); + Assert.True(pivSession.TryResetPin(_defaultPuk, _complexPin, out _)); int? retriesRemaining = 3; - var e = Assert.Throws(() => pivSession.TryResetPin(defaultPuk, invalidPin, out retriesRemaining)); + var e = Assert.Throws(() => pivSession.TryResetPin(_defaultPuk, _invalidPin, out retriesRemaining)); Assert.Equal(ExceptionMessages.PinComplexityViolation, e.Message); Assert.Null(retriesRemaining); } @@ -96,19 +90,78 @@ public void ResetPivPinToInvalidValue_ThrowsSecurityException() [SkippableFact] public void SetFido2PinToInvalidValue_ThrowsFido2Exception() { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); Skip.IfNot(testDevice.IsPinComplexityEnabled); using var fido2Session = new Fido2Session(testDevice); // set violating PIN - var fido2Exception = Assert.Throws(() => fido2Session.TrySetPin(invalidPin)); + var fido2Exception = Assert.Throws(() => fido2Session.TrySetPin(_invalidPin)); Assert.Equal(CtapStatus.PinPolicyViolation, fido2Exception.Status); + // set complex PIN to be able to try to change it later - Assert.True(fido2Session.TrySetPin(complexPin)); + Assert.True(fido2Session.TrySetPin(_complexPin)); + // change to violating PIN - fido2Exception = Assert.Throws(() => fido2Session.TryChangePin(complexPin, invalidPin)); + fido2Exception = Assert.Throws(() => fido2Session.TryChangePin(_complexPin, _invalidPin)); Assert.Equal(CtapStatus.PinPolicyViolation, fido2Exception.Status); } + + [SkippableFact] + public void SetFido2PinToInvalidValue_WithKeyCollector_ThrowsFido2Exception() + { + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(StandardTestDevice.Fw5Fips); + Skip.IfNot(testDevice.IsPinComplexityEnabled); + + using var fido2Session = new Fido2Session(testDevice); + var pinComplexityKeyCollector = new PinComplexityKeyCollector + { + NewPin = _invalidPin + }; + + fido2Session.KeyCollector = pinComplexityKeyCollector.KeyCollectorDelegate; + + // set violating PIN + var fido2Exception = Assert.Throws(() => fido2Session.TrySetPin()); + Assert.Equal(CtapStatus.PinPolicyViolation, fido2Exception.Status); + + // set complex PIN to be able to try to change it later + pinComplexityKeyCollector.NewPin = _complexPin; + Assert.True(fido2Session.TrySetPin()); + + // change to violating PIN + pinComplexityKeyCollector.NewPin = _invalidPin; + fido2Exception = Assert.Throws(() => fido2Session.TryChangePin()); + Assert.Equal(CtapStatus.PinPolicyViolation, fido2Exception.Status); + } + + private class PinComplexityKeyCollector + { + public ReadOnlyMemory NewPin { get; set; } + private ReadOnlyMemory _currentPin; + + public bool KeyCollectorDelegate(KeyEntryData keyEntryData) + { + if (keyEntryData.IsViolatingPinComplexity) + { + throw new Fido2Exception(CtapStatus.PinPolicyViolation, "Test Pin Complexity"); + } + + switch (keyEntryData.Request) + { + case KeyEntryRequest.SetFido2Pin: + keyEntryData.SubmitValue(NewPin.ToArray()); + _currentPin = NewPin; + return true; + case KeyEntryRequest.VerifyFido2Pin: + return true; + case KeyEntryRequest.ChangeFido2Pin: + keyEntryData.SubmitValues(_currentPin.ToArray(), NewPin.ToArray()); + return true; + default: + return false; + } + } + } } } diff --git a/build/Versions.props b/build/Versions.props index e5927050..2f328bca 100644 --- a/build/Versions.props +++ b/build/Versions.props @@ -40,7 +40,7 @@ for external milestones. Increment the minor version whenever we add support for a new class or type. Increment the patch version for bug fixes. --> - 1.10.0 + 1.11.0 - 1.10.0 + 1.11.0 - 1.10.0 + 1.11.0