Skip to content

Additions for PIN complexity violation #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
uid: UsersManualPinComplexityPolicy
---

<!-- 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. -->

# 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 <a href="https://docs.yubico.com/hardware/yubikey/yk-tech-manual/5.7-firmware-specifics.html#pin-complexity">online documentation</a> 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.
2 changes: 2 additions & 0 deletions Yubico.YubiKey/docs/users-manual/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?");
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Yubico.YubiKey/src/Resources/ExceptionMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@
<data name="NoMoreRetriesRemaining" xml:space="preserve">
<value>There are no retries remaining for a PIN, PUK, or other authentication element.</value>
</data>
<data name="PinComplexityViolation" xml:space="preserve">
<value>The provided PIN or PUK value violates current complexity conditions.</value>
</data>
<data name="YubiKeyNotAuthenticatedInPiv" xml:space="preserve">
<value>PIV management key mutual authentication failed because the YubiKey did not authenticate.</value>
</data>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Yubico.YubiKey/src/Resources/ResponseStatusMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@
<data name="Fido2PinNotVerified" xml:space="preserve">
<value>The request was rejected because the PIN and/or UV was not verified.</value>
</data>
<data name="Fido2PinComplexityViolation" xml:space="preserve">
<value>The request was rejected because the PIN violates PIN complexity setting.</value>
</data>
<data name="Fido2PinBlocked" xml:space="preserve">
<value>The request was rejected because the PIN and/or the UV was blocked.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public Fido2Response(ResponseApdu responseApdu) : base(responseApdu)
CtapStatus.NoCredentials => new ResponseStatusPair(ResponseStatus.NoData, ResponseStatusMessages.Fido2NoCredentials),
CtapStatus.NotAllowed => new ResponseStatusPair(ResponseStatus.Failed, ResponseStatusMessages.Fido2NotAllowed),
CtapStatus.PinRequired => new ResponseStatusPair(ResponseStatus.Failed, ResponseStatusMessages.Fido2PinNotVerified),
CtapStatus.PinPolicyViolation => new ResponseStatusPair(ResponseStatus.ConditionsNotSatisfied, ResponseStatusMessages.Fido2PinComplexityViolation),
CtapStatus.PinNotSet => new ResponseStatusPair(ResponseStatus.Failed, ResponseStatusMessages.Fido2PinNotSet),
CtapStatus.PinInvalid => new ResponseStatusPair(ResponseStatus.ConditionsNotSatisfied, ResponseStatusMessages.Fido2PinNotVerified),
CtapStatus.PinBlocked => new ResponseStatusPair(ResponseStatus.Failed, ResponseStatusMessages.Fido2PinBlocked),
Expand Down
10 changes: 10 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Exception.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public Fido2Exception(string message) : base(message)

}

/// <summary>
/// Initializes a new instance of the <see cref="Fido2Exception"/> class.
/// </summary>
/// <param name="status">The CTAP error.</param>
/// <param name="message">The message that describes the error.</param>
public Fido2Exception(CtapStatus status, string message) : base(message)
{
Status = status;
}

/// <summary>
/// Initializes a new instance of the <see cref="Fido2Exception"/> class.
/// </summary>
Expand Down
49 changes: 37 additions & 12 deletions Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.Pin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -541,6 +551,8 @@ public bool TrySetPin()
keyEntryData.Request = KeyEntryRequest.Release;
_ = keyCollector(keyEntryData);
}

return false;
}

/// <summary>
Expand Down Expand Up @@ -589,7 +601,7 @@ public bool TrySetPin(ReadOnlyMemory<byte> newPin)
return false; // PIN is already set.
}

throw new Fido2Exception(result.StatusMessage);
throw new Fido2Exception(GetCtapError(result), result.StatusMessage);
}

/// <summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -742,7 +767,7 @@ public bool TryChangePin(ReadOnlyMemory<byte> currentPin, ReadOnlyMemory<byte> n
return false; // PIN is invalid
}

throw new Fido2Exception(result.StatusMessage);
throw new Fido2Exception(GetCtapError(result), result.StatusMessage);
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/KeyEntryData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ public sealed class KeyEntryData
/// </remarks>
public bool IsRetry { get; set; }

/// <summary>
/// Indicates if the current request for an item has violated PIN complexity.
/// </summary>
public bool IsViolatingPinComplexity { get; set; }

/// <summary>
/// This is the result of the last fingerprint sample. This will be null
/// if the <c>Request</c> is for something other than
Expand Down Expand Up @@ -221,6 +226,7 @@ public KeyEntryData()
_currentValue = Memory<byte>.Empty;
_newValue = Memory<byte>.Empty;
IsRetry = false;
IsViolatingPinComplexity = false;
}

/// <summary>
Expand Down Expand Up @@ -340,6 +346,7 @@ public void Clear()
LastBioEnrollSampleResult = null;
SignalUserCancel = null;
IsRetry = false;
IsViolatingPinComplexity = false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading
Loading