diff --git a/.editorconfig b/.editorconfig index 33ca8339e..4650be58f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,6 +21,10 @@ root = true dotnet_diagnostic.ide0073.severity = suggestion file_header_template = Copyright 2024 Yubico AB\n\nLicensed under the Apache License, Version 2.0 (the "License").\nYou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an "AS IS" BASIS, \nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License. +# ReSharper properties +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_parameters_style = chop_always + # C# files [*.cs] @@ -357,4 +361,4 @@ dotnet_diagnostic.ca1014.severity = none # var preferences csharp_style_var_elsewhere = false:warning csharp_style_var_for_built_in_types = false:warning -csharp_style_var_when_type_is_apparent = true:warning \ No newline at end of file +csharp_style_var_when_type_is_apparent = true:warning diff --git a/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs b/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs index cfd30ac68..28e507a8a 100644 --- a/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs +++ b/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs @@ -104,7 +104,7 @@ public IDisposable BeginTransaction(out bool cardWasReset) uint result = SCardBeginTransaction(_cardHandle); _log.SCardApiCall(nameof(SCardBeginTransaction), result); - // Sometime the smart card is left in a state where it needs to be reset prior to beginning + // Sometimes the smart card is left in a state where it needs to be reset prior to beginning // a transaction. We should automatically handle this case. if (result == ErrorCode.SCARD_W_RESET_CARD) { diff --git a/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs new file mode 100644 index 000000000..8fd00ab60 --- /dev/null +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs @@ -0,0 +1,198 @@ +// 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.IO; +using System.Linq; + +namespace Yubico.Core.Tlv +{ + /// + /// Tag, length, Value structure that helps to parse APDU response data. + /// This class handles BER-TLV encoded data with determinate length. + /// + public class TlvObject + { + /// + /// Returns the tag. + /// + public int Tag { get; } + + /// + /// Returns the value. + /// + public Memory Value => _bytes.Skip(_offset).Take(Length).ToArray(); + + /// + /// Returns the length of the value. + /// + public int Length { get; } + + private readonly byte[] _bytes; + private readonly int _offset; + + /// + /// Creates a new TLV (Tag-Length-Value) object with the specified tag and value. + /// + /// The tag value, must be between 0x00 and 0xFFFF. + /// The value data as a read-only span of bytes. + /// Thrown when the tag value is outside the supported range (0x00-0xFFFF). + /// + /// This constructor creates a BER-TLV encoded structure where: + /// - The tag is encoded in the minimum number of bytes needed + /// - The length is encoded according to BER-TLV rules + /// - The value is stored as provided + /// + public TlvObject(int tag, ReadOnlySpan value) + { + // Validate that the tag is within the supported range (0x00-0xFFFF) + if (tag < 0 || tag > 0xFFFF) + { + throw new TlvException(ExceptionMessages.TlvUnsupportedTag); + } + + Tag = tag; + // Create a copy of the input value + byte[] valueBuffer = value.ToArray(); + using var ms = new MemoryStream(); + + // Convert tag to bytes and remove leading zeros, maintaining BER-TLV format + byte[] tagBytes = BitConverter.GetBytes(tag).Reverse().SkipWhile(b => b == 0).ToArray(); + ms.Write(tagBytes, 0, tagBytes.Length); + + Length = valueBuffer.Length; + // For lengths < 128 (0x80), use single byte encoding + if (Length < 0x80) + { + ms.WriteByte((byte)Length); + } + // For lengths >= 128, use multi-byte encoding with length indicator + else + { + // Convert length to bytes and remove leading zeros + byte[] lnBytes = BitConverter.GetBytes(Length).Reverse().SkipWhile(b => b == 0).ToArray(); + // Write number of length bytes with high bit set (0x80 | number of bytes) + ms.WriteByte((byte)(0x80 | lnBytes.Length)); + // Write the actual length bytes + ms.Write(lnBytes, 0, lnBytes.Length); + } + + // Store the position where the value begins + _offset = (int)ms.Position; + + // Write the value bytes + ms.Write(valueBuffer, 0, Length); + + // Store the complete TLV encoding + _bytes = ms.ToArray(); + } + + /// + /// Returns a copy of the Tlv as a BER-TLV encoded byte array. + /// + public Memory GetBytes() => _bytes.ToArray(); + + /// + /// Parse a Tlv from a BER-TLV encoded byte array. + /// + /// A byte array containing the TLV encoded data (and nothing more). + /// The parsed Tlv + public static TlvObject Parse(ReadOnlySpan data) + { + ReadOnlySpan buffer = data; + return ParseFrom(ref buffer); + } + + /// + /// Parses a TLV from a BER-TLV encoded byte array. + /// + /// A byte array containing the TLV encoded data. + /// The parsed + /// + /// This method will parse a TLV from the given buffer and return the parsed Tlv. + /// The method will consume the buffer as much as necessary to parse the TLV. + /// + /// Thrown if the buffer does not contain a valid TLV. + internal static TlvObject ParseFrom(ref ReadOnlySpan buffer) + { + // The first byte of the TLV is the tag. + int tag = buffer[0]; + + // Determine if the tag is in long form. + // Long form tags have more than one byte, starting with 0x1F. + if ((buffer[0] & 0x1F) == 0x1F) + { + // Ensure there is enough data for a long form tag. + if (buffer.Length < 2) + { + throw new ArgumentException("Insufficient data for long form tag"); + } + // Combine the first two bytes to form the tag. + tag = (buffer[0] << 8) | buffer[1]; + buffer = buffer[2..]; // Skip the tag bytes + } + else + { + buffer = buffer[1..]; // Skip the tag byte + } + + if (buffer.Length < 1) + { + throw new ArgumentException("Insufficient data for length"); + } + + // Read the length of the TLV value. + int length = buffer[0]; + buffer = buffer[1..]; + + // If the length is more than one byte, process remaining bytes. + if (length > 0x80) + { + int lengthLn = length - 0x80; + length = 0; + for (int i = 0; i < lengthLn; i++) + { + length = (length << 8) | buffer[0]; + buffer = buffer[1..]; + } + } + + ReadOnlySpan value = buffer[..length]; + buffer = buffer[length..]; // Advance the buffer to the end of the value + + return new TlvObject(tag, value); + } + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + /// + /// The string is of the form Tlv(0xTAG, LENGTH, VALUE). + /// + /// The tag is written out in hexadecimal, prefixed by 0x. + /// The length is written out in decimal. + /// The value is written out in hexadecimal. + /// + /// + public override string ToString() + { +#if NETSTANDARD2_1_OR_GREATER + return $"Tlv(0x{Tag:X}, {Length}, {BitConverter.ToString(Value.ToArray()).Replace("-", "", StringComparison.Ordinal)})"; +#else + return $"Tlv(0x{Tag:X}, {Length}, {BitConverter.ToString(Value.ToArray()).Replace("-", "")})"; +#endif + } + } +} diff --git a/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs new file mode 100644 index 000000000..aa75d1d23 --- /dev/null +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Yubico.Core.Tlv +{ + /// + /// Utility methods to encode and decode BER-TLV data. + /// + public static class TlvObjects + { + /// + /// Decodes a sequence of BER-TLV encoded data into a list of Tlvs. + /// + /// Sequence of TLV encoded data + /// List of + public static IReadOnlyList DecodeList(ReadOnlySpan data) + { + var tlvs = new List(); + ReadOnlySpan buffer = data; + while (!buffer.IsEmpty) + { + var tlv = TlvObject.ParseFrom(ref buffer); + tlvs.Add(tlv); + } + + return tlvs.AsReadOnly(); + } + + /// + /// Decodes a sequence of BER-TLV encoded data into a mapping of Tag-Value pairs. + /// Iteration order is preserved. If the same tag occurs more than once only the latest will be kept. + /// + /// Sequence of TLV encoded data + /// Dictionary of Tag-Value pairs + public static IReadOnlyDictionary> DecodeMap(ReadOnlySpan data) + { + var tlvs = new Dictionary>(); + ReadOnlySpan buffer = data; + while (!buffer.IsEmpty) + { + var tlv = TlvObject.ParseFrom(ref buffer); + tlvs[tlv.Tag] = tlv.Value; + } + + return tlvs; + } + + /// + /// Encodes a list of Tlvs into a sequence of BER-TLV encoded data. + /// + /// List of Tlvs to encode + /// BER-TLV encoded list + public static byte[] EncodeList(IEnumerable list) + { + if (list is null) + { + throw new ArgumentNullException(nameof(list)); + } + + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + foreach (TlvObject? tlv in list) + { + ReadOnlyMemory bytes = tlv.GetBytes(); + writer.Write(bytes.Span.ToArray()); + } + + return stream.ToArray(); + } + + /// + /// Encodes an array of Tlvs into a sequence of BER-TLV encoded data. + /// + /// Array of Tlvs to encode + /// BER-TLV encoded array + public static byte[] EncodeMany(params TlvObject[] tlvs) => EncodeList(tlvs); + + /// + /// Decode a single TLV encoded object, returning only the value. + /// + /// The expected tag value of the given TLV data + /// The TLV data + /// The value of the TLV + /// If the TLV tag differs from expectedTag + public static Memory UnpackValue(int expectedTag, ReadOnlySpan tlvData) + { + var tlv = TlvObject.Parse(tlvData); + if (tlv.Tag != expectedTag) + { + throw new InvalidOperationException($"Expected tag: {expectedTag:X2}, got {tlv.Tag:X2}"); + } + + return tlv.Value.ToArray(); + } + } +} diff --git a/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTests.cs b/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTests.cs new file mode 100644 index 000000000..3f6325405 --- /dev/null +++ b/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTests.cs @@ -0,0 +1,164 @@ +// 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.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Yubico.Core.Tlv.UnitTests +{ + public class TlvObjectTests + { + [Fact] + public void TestDoubleByteTags() + { + var tlv = TlvObject.Parse(new byte[] { 0x7F, 0x49, 0 }); + Assert.Equal(0x7F49, tlv.Tag); + Assert.Equal(0, tlv.Length); + + tlv = TlvObject.Parse(new byte[] { 0x80, 0 }); + Assert.Equal(0x80, tlv.Tag); + Assert.Equal(0, tlv.Length); + + tlv = new TlvObject(0x7F49, null); + Assert.Equal(0x7F49, tlv.Tag); + Assert.Equal(0, tlv.Length); + Assert.Equal(new byte[] { 0x7F, 0x49, 0 }, tlv.GetBytes()); + + tlv = new TlvObject(0x80, null); + Assert.Equal(0x80, tlv.Tag); + Assert.Equal(0, tlv.Length); + Assert.Equal(new byte[] { 0x80, 0 }, tlv.GetBytes()); + } + + + [Fact] + public void TlvObject_Encode_ReturnsCorrectBytes() + { + // Arrange + int tag = 0x1234; + byte[] value = { 0x01, 0x02, 0x03 }; + TlvObject tlv = new TlvObject(tag, value); + + // Act + byte[] encodedBytes = tlv.GetBytes().ToArray(); + + // Assert + byte[] expectedBytes = { 0x12, 0x34, 0x03, 0x01, 0x02, 0x03 }; + Assert.True(encodedBytes.SequenceEqual(expectedBytes)); + } + + [Fact] + public void TestUnwrap() + { + TlvObjects.UnpackValue(0x80, new byte[] { 0x80, 0 }); + + TlvObjects.UnpackValue(0x7F49, new byte[] { 0x7F, 0x49, 0 }); + + var value = TlvObjects.UnpackValue(0x7F49, new byte[] { 0x7F, 0x49, 3, 1, 2, 3 }); + Assert.Equal(new byte[] { 1, 2, 3 }, value); + } + + [Fact] + public void TestUnwrapThrowsException() + { + Assert.Throws(() => TlvObjects.UnpackValue(0x7F48, new byte[] { 0x7F, 0x49, 0 })); + } + + [Fact] + public void DecodeList_ValidInput_ReturnsCorrectTlvs() + { + var input = new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x02, 0xAA, 0xBB }; + var result = TlvObjects.DecodeList(input); + + Assert.Equal(2, result.Count); + Assert.Equal(0x01, result[0].Tag); + Assert.Equal(new byte[] { 0xFF }, result[0].Value.ToArray()); + Assert.Equal(0x02, result[1].Tag); + Assert.Equal(new byte[] { 0xAA, 0xBB }, result[1].Value.ToArray()); + } + + [Fact] + public void DecodeList_EmptyInput_ReturnsEmptyList() + { + var result = TlvObjects.DecodeList(Array.Empty()); + Assert.Empty(result); + } + + [Fact] + public void DecodeMap_ValidInput_ReturnsCorrectDictionary() + { + var input = new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x02, 0xAA, 0xBB }; + var result = TlvObjects.DecodeMap(input); + + Assert.Equal(2, result.Count); + Assert.Equal(new byte[] { 0xFF }, result[0x01].ToArray()); + Assert.Equal(new byte[] { 0xAA, 0xBB }, result[0x02].ToArray()); + } + + [Fact] + public void DecodeMap_DuplicateTags_KeepsLastValue() + { + var input = new byte[] { 0x01, 0x01, 0xFF, 0x01, 0x01, 0xEE }; + var result = TlvObjects.DecodeMap(input); + + Assert.Single(result); + Assert.Equal(new byte[] { 0xEE }, result[0x01].ToArray()); + } + + [Fact] + public void EncodeList_ValidInput_ReturnsCorrectBytes() + { + var tlvs = new List + { + new TlvObject(0x01, new byte[] { 0xFF }), + new TlvObject(0x02, new byte[] { 0xAA, 0xBB }) + }; + + var result = TlvObjects.EncodeList(tlvs); + Assert.Equal(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x02, 0xAA, 0xBB }, result.ToArray()); + } + + [Fact] + public void EncodeList_EmptyInput_ReturnsEmptyArray() + { + var result = TlvObjects.EncodeList(new List()); + Assert.Empty(result.ToArray()); + } + + [Fact] + public void UnpackValue_CorrectTag_ReturnsValue() + { + var input = new byte[] { 0x01, 0x02, 0xAA, 0xBB }; + var result = TlvObjects.UnpackValue(0x01, input); + Assert.Equal(new byte[] { 0xAA, 0xBB }, result.ToArray()); + } + + [Fact] + public void UnpackValue_IncorrectTag_ThrowsBadResponseException() + { + var input = new byte[] { 0x01, 0x02, 0xAA, 0xBB }; + Assert.Throws(() => TlvObjects.UnpackValue(0x02, input)); + } + + [Fact] + public void UnpackValue_EmptyValue_ReturnsEmptyArray() + { + var input = new byte[] { 0x01, 0x00 }; + var result = TlvObjects.UnpackValue(0x01, input); + Assert.Empty(result.ToArray()); + } + } +} diff --git a/Yubico.NET.SDK.sln b/Yubico.NET.SDK.sln index 2527551a3..deb368ae2 100644 --- a/Yubico.NET.SDK.sln +++ b/Yubico.NET.SDK.sln @@ -178,7 +178,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk Programming Guide", "Sd Yubico.YubiKey\docs\users-manual\sdk-programming-guide\device-notifications.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\device-notifications.md Yubico.YubiKey\docs\users-manual\sdk-programming-guide\key-collector.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\key-collector.md Yubico.YubiKey\docs\users-manual\sdk-programming-guide\making-a-connection.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\making-a-connection.md - Yubico.YubiKey\docs\users-manual\sdk-programming-guide\secure-channel-protocol-3.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\secure-channel-protocol-3.md + Yubico.YubiKey\docs\users-manual\sdk-programming-guide\secure-channel-protocol.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\secure-channel-protocol.md Yubico.YubiKey\docs\users-manual\sdk-programming-guide\sensitive-data.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\sensitive-data.md Yubico.YubiKey\docs\users-manual\sdk-programming-guide\threads.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\threads.md Yubico.YubiKey\docs\users-manual\sdk-programming-guide\appcompat.md = Yubico.YubiKey\docs\users-manual\sdk-programming-guide\appcompat.md diff --git a/Yubico.YubiKey/docs/index.md b/Yubico.YubiKey/docs/index.md index dfa460f79..07cb64423 100644 --- a/Yubico.YubiKey/docs/index.md +++ b/Yubico.YubiKey/docs/index.md @@ -101,11 +101,19 @@ Read more about FIDO2 [here](xref:Fido2Overview). ### YubiHSM Auth YubiHSM Auth is a YubiKey CCID application that stores the long-lived credentials used to establish -secure sessions with a YubiHSM 2. The secure session protocol is based on Secure Channel Protocol 3 -(SCP03). YubiHSM Auth is supported by YubiKey firmware version 5.4.3. +secure sessions with a YubiHSM 2. The secure session protocol is based on Secure Channel Protocol (SCP). +YubiHSM Auth is supported by YubiKey firmware version 5.4.3. YubiHSM Auth uses hardware to protect these long-lived credentials. In addition to providing robust security for the YubiHSM Auth application itself, this hardware protection subsequently increases the security of the default password-based solution for YubiHSM 2's authentication. -Read more about YubiHSM Auth [here.](xref:YubiHsmAuthOverview) +Read more about YubiHSM Auth [here](xref:YubiHsmAuthOverview). + +### Security Domain + +The Security Domain is a special application on the YubiKey responsible for managing secure communication channels and cryptographic keys. +It implements protocols defined by [Global Platform Consortium](https://globalplatform.org/) that provide confidentiality and integrity for +commands sent between host applications and the YubiKey. + +Read more about Security Domain [here](xref:SecurityDomainOverview). diff --git a/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-certificates.md b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-certificates.md new file mode 100644 index 000000000..4d15cc4b3 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-certificates.md @@ -0,0 +1,230 @@ +--- +uid: SecurityDomainCertificates +--- + + + +# Security Domain certificate management + +The Security Domain manages X.509 certificates primarily for SCP11 protocol operations. These certificates are used for authentication and establishing secure channels. For detailed information about certificate usage in secure channels, see the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. + +## Certificate operations + +### Storing certificates + +Certificates are stored in chains associated with specific key references. A typical certificate chain includes: + +- Root CA certificate +- Intermediate certificates (optional) +- Leaf (end-entity) certificate + +```csharp +// Store certificate chain +var certificates = new List +{ + rootCert, + intermediateCert, + leafCert +}; +session.StoreCertificates(keyReference, certificates); +``` + +### CA configuration + +For SCP11a and SCP11c, you need to configure the Certificate Authority (CA) information: + +```csharp +// Store CA issuer information using Subject Key Identifier (SKI) +byte[] subjectKeyIdentifier = GetSkiFromCertificate(caCert); +session.StoreCaIssuer(keyReference, subjectKeyIdentifier); +``` + +### Retrieving certificates + +```csharp +// Get all certificates for a key reference +var certificates = session.GetCertificates(keyReference); + +// Get leaf certificate (last in chain) +var leafCert = certificates.Last(); + +// Get supported CA identifiers +var caIds = session.GetSupportedCaIdentifiers( + kloc: true, // Key Loading OCE Certificate + klcc: true // Key Loading Card Certificate +); +``` + +## Access control + +### Certificate allowlists + +Use of the allowlist can provide the following benefits: +- A strong binding to one (or multiple) OCE(s) +- Protection against compromised OCEs +It is recommended to use the allowlist also as a revocation mechanism for OCE certificates. However, if the +allowlist is used in this way, special care shall be taken never to empty/remove the allowlist (i.e. if created, the +allowlist shall always contain at least one certificate) because no restrictions apply (i.e. all certificates are +accepted) once an allowlist is removed. + +Control which certificates can be used for authentication by maintaining an allowlist of serial numbers: + +```csharp +// Store allowlist of certificate serial numbers +var allowedSerials = new List +{ + "7F4971B0AD51F84C9DA9928B2D5FEF5E16B2920A", + "6B90028800909F9FFCD641346933242748FBE9AD" +}; +session.StoreAllowlist(keyReference, allowedSerials); + +// Clear allowlist (allows any valid certificate) +session.ClearAllowList(keyReference); +``` + +## Certificate management by SCP11 variant + +Different SCP11 variants have different certificate requirements: + +### SCP11a + +- Full certificate chain required +- OCE (Off-Card Entity) certificates needed +- Supports authorization rules in certificates +- Used for mutual authentication + +Example setup: + +```csharp +// Setup with full chain for SCP11a +using var session = new SecurityDomainSession(yubiKeyDevice, scp03Params); +var keyRef = KeyReference.Create(ScpKeyIds.Scp11A, kvn); + +// Store certificates +session.StoreCertificates(keyRef, certificates); + +// Configure OCE +var oceRef = KeyReference.Create(OceKid, kvn); +session.StoreCaIssuer(oceRef, skiBytes); +``` + +### SCP11b + +- Simplest variant, no mutual authentication +- Suitable when host authentication isn't required + +Example setup: + +```csharp +// Basic SCP11b setup +using var session = new SecurityDomainSession(yubiKeyDevice, scp03Params); +var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, kvn); + +// Only store device certificate +session.StoreCertificates(keyRef, new[] { deviceCert }); +``` + +### SCP11c + +- Mutual authentication +- Similar to SCP11a but with additional features +- such as offline scripting usage (See [GlobalPlatform SCP11 Specification Annex B](https://globalplatform.org/specs-library/secure-channel-protocol-11-amendment-f/)) + +## Security considerations + +1. **Certificate Validation** + + - Verify certificate chains completely + - Check certificate revocation status + - Validate certificate purposes and extensions + - Ensure proper key usage constraints + +2. **Access Control** + + - Use allowlists in production environments + - Regularly review and update allowlists + - Monitor for unauthorized certificate usage + - Document certificate authorization policies + +3. **Certificate Lifecycle** + + - Plan for certificate renewal + - Handle certificate revocation + - Maintain certificate inventory + - Test certificate rotation procedures + +4. **Storage Limitations** + - Be aware of YubiKey storage constraints + - Optimize certificate chain length + - Consider certificate compression if needed + - Monitor available storage space + +> [!IMPORTANT] +> Most certificate operations require an authenticated session. Operations are typically only available when using SCP11a or SCP11c variants. + +## Common tasks + +### Initial certificate setup + +1. Generate or obtain required certificates +2. Store certificate chain on YubiKey +3. Configure CA information if needed +4. Set up allowlist for production use + +```csharp +// Example of complete setup +using var session = new SecurityDomainSession(yubiKeyDevice, scp03Params); +var keyRef = KeyReference.Create(ScpKeyIds.Scp11A, kvn); + +// Store full chain +session.StoreCertificates(keyRef, certificateChain); + +// Configure CA +var oceRef = KeyReference.Create(OceKid, kvn); +session.StoreCaIssuer(oceRef, GetSkiFromCertificate(caCert)); + +// Set up allowlist +session.StoreAllowlist(keyRef, allowedSerials); +``` + +### Certificate rotation + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); + +// Store new certificates +session.StoreCertificates(keyRef, newCertificateChain); + +// Update allowlist if needed +session.StoreAllowlist(keyRef, newAllowedSerials); +``` + +### Troubleshooting + +1. **Certificate Loading Issues** + + - Verify certificate format (X.509 v3) + - Check certificate chain order + - Ensure sufficient storage space + - Validate key references + +2. **Authentication Problems** + - Verify certificate trust chain + - Check allowlist configuration + - Confirm proper SCP variant usage + - Validate certificate dates + +> [!NOTE] +> For additional details on secure channel establishment and certificate usage, refer to the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. diff --git a/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-device.md b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-device.md new file mode 100644 index 000000000..3e970c5e9 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-device.md @@ -0,0 +1,236 @@ +--- +uid: SecurityDomainDevice +--- + + + +# Security Domain device information + +The Security Domain provides access to various device information and configuration data. This document covers device information retrieval and generic data operations. For protocol details, see the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. + +## Card recognition data + +The card recognition data provides information about the YubiKey's capabilities and configuration according to GlobalPlatform Card Specification v2.3.1 §H.2. + +### Retrieving card data + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice); + +// Get card recognition data (TLV encoded) +var cardData = session.GetCardRecognitionData(); +``` + +### Card data structure + +The card recognition data follows this TLV structure: + +| Tag | Description | +| ---- | --------------------------- | +| 0x66 | Card Data template | +| 0x73 | Card Recognition Data | +| ... | Card-specific data elements | + +## Generic data operations + +The Security Domain supports general-purpose data retrieval and storage using TLV (Tag-Length-Value) formatting. + +### Retrieving data + +```csharp +// Get data for a specific tag +var data = session.GetData(tag); + +// Get data with additional parameters +var data = session.GetData(tag, additionalData); +``` + +### Storing data + +```csharp +// Store TLV-formatted data +byte[] tlvData = PrepareTlvData(); +session.StoreData(tlvData); +``` + +### Common data tags + +| Tag | Description | Access Level | +| ------ | ----------------- | ------------ | +| 0x66 | Card Data | Read-only | +| 0x73 | Card Recognition | Read-only | +| 0xE0 | Key Information | Read-only | +| 0xBF21 | Certificate Store | Read/Write | +| 0xFF33 | KLOC Identifiers | Read/Write | +| 0xFF34 | KLCC Identifiers | Read/Write | + +## Device configuration + +### Checking capabilities + +```csharp +// Check SCP11 support +if (yubiKeyDevice.HasFeature(YubiKeyFeature.Scp11)) +{ + // Device supports SCP11 +} + +// Get installed key information +var keyInfo = session.GetKeyInformation(); + +// Get supported CA identifiers +var caIds = session.GetSupportedCaIdentifiers( + kloc: true, + klcc: true +); +``` + +### Device status information + +1. **Key Status** + +```csharp +var keyInfo = session.GetKeyInformation(); +foreach (var entry in keyInfo) +{ + var keyRef = entry.Key; + var components = entry.Value; + + // Check key type and components + bool isScp03 = keyRef.Id == ScpKeyIds.Scp03; + bool hasAllComponents = components.Count == 3; // For SCP03 +} +``` + +2. **Certificate Status** + +```csharp +// Check certificate configuration +foreach (var key in keyInfo.Keys) +{ + try + { + var certs = session.GetCertificates(key); + // Analyze certificate chain + AnalyzeCertificateChain(certs); + } + catch (Exception ex) + { + // Handle missing certificates + } +} +``` + +## Data management + +### TLV data handling + +1. **Reading TLV Data** + +```csharp +var tlvData = session.GetData(tag); +var tlvReader = new TlvReader(tlvData); + +// Parse TLV structure +while (tlvReader.HasData) +{ + var tag = tlvReader.PeekTag(); + var value = tlvReader.ReadValue(); + ProcessTlvData(tag, value); +} +``` + +2. **Writing TLV Data** + +```csharp +using var ms = new MemoryStream(); +using var writer = new BinaryWriter(ms); + +// Build TLV structure +writer.Write((byte)tag); +writer.Write((byte)length); +writer.Write(value); + +session.StoreData(ms.ToArray()); +``` + +### Data organization + +The Security Domain organizes data hierarchically: + +``` +Root +├── Card Data (0x66) +│ └── Card Recognition (0x73) +├── Key Information (0xE0) +│ ├── Key References +│ └── Key Components +├── Certificate Store (0xBF21) +│ ├── Certificate Chains +│ └── CA Information +└── Device Configuration + ├── KLOC Data (0xFF33) + └── KLCC Data (0xFF34) +``` + +## Maintenance operations + +### Data validation + +```csharp +// Validate stored data +public void ValidateDeviceConfiguration() +{ + // Check card data + var cardData = session.GetCardRecognitionData(); + ValidateCardData(cardData); + + // Check key information + var keyInfo = session.GetKeyInformation(); + ValidateKeyConfiguration(keyInfo); + + // Check certificate store + foreach (var key in keyInfo.Keys) + { + ValidateCertificateConfiguration(key); + } +} +``` + +### Data cleanup + +```csharp +// Remove unused data +public void CleanupDeviceData() +{ + // Clear unused certificates + foreach (var key in keyInfo.Keys) + { + if (IsKeyExpired(key)) + { + session.DeleteKey(key); + } + } + + // Clear obsolete allowlists + foreach (var key in keyInfo.Keys) + { + if (IsAllowlistObsolete(key)) + { + session.ClearAllowList(key); + } + } +} +``` \ No newline at end of file diff --git a/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-keys.md b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-keys.md new file mode 100644 index 000000000..2c06af9a8 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-keys.md @@ -0,0 +1,170 @@ +--- +uid: SecurityDomainKeys +--- + + + +# Security Domain key management + +The Security Domain supports management of both symmetric (SCP03) and asymmetric (SCP11) keys. This document describes the key types, their usage, and management operations. + +For protocol details and secure channel implementation, see the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. + +## Key types + +The Security Domain manages two main types of keys: + +- **SCP03 Keys**: Symmetric AES-128 keys used for secure messaging +- **SCP11 Keys**: Asymmetric NIST P-256 keys and X.509-certificates used for authentication and key agreement + +## SCP03 key management + +Each SCP03 key set consists of three AES-128 keys that work together to secure communications: + +| Key Type | Purpose | +|----------|---------| +| Key-ENC | Channel encryption key for securing messages | +| Key-MAC | Channel MAC key for message authentication | +| Key-DEK | Data encryption key for sensitive data | + +### Managing SCP03 keys + +```csharp +// Put a new SCP03 key set +var keyRef = KeyReference.Create(ScpKeyIds.Scp03, 0x01); // KVN=1 +var staticKeys = new StaticKeys(keyDataMac, keyDataEnc, keyDataDek); +session.PutKey(keyRef, staticKeys); // 0 means new key + +// Replace existing keys +var newKeys = new StaticKeys(newMacKey, newEncKey, newDekKey); +session.PutKey(keyRef, newKeys, currentKvn); +``` + +### Key Version Numbers (KVN) + +SCP03 key sets are identified by Key Version Numbers: +- Default key set: KVN=0xFF (publicly known, no security) +- Each YubiKey can store up to three custom SCP03 key sets + +> [!NOTE] +> When adding the first custom key set, the default keys are automatically removed. + +## SCP11 key management + +SCP11 uses NIST P-256 elliptic curve cryptography. Keys can be: +- Generated on the YubiKey (recommended) +- Imported from external sources + +### Generating keys + +```csharp +// Generate new EC key pair +var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var publicKey = session.GenerateEcKey(keyRef); +``` + +### Importing keys + +```csharp +// Import existing private key +var privateKey = new ECPrivateKeyParameters(ecdsa); +session.PutKey(keyRef, privateKey); + +// Import public key +var publicKey = new ECPublicKeyParameters(ecdsaPublic); +session.PutKey(keyRef, publicKey); +``` + +## Key management operations + +### Querying key information + +```csharp +// Get information about installed keys +var keyInfo = session.GetKeyInformation(); +foreach (var entry in keyInfo) +{ + var keyRef = entry.Key; // KeyReference containing ID and Version + var components = entry.Value; // Dictionary of key components + Console.WriteLine($"Key {keyRef.Id:X2}:{keyRef.VersionNumber:X2}"); +} +``` + +### Deleting keys + +Keys can be deleted individually or reset to factory defaults: + +```csharp +// Delete specific key +session.DeleteKey(keyRef); + +// Reset all keys to factory defaults +session.Reset(); +``` + +> [!WARNING] +> Resetting removes all custom keys and restores factory defaults (within the Security Domain). Ensure you have backups before resetting. + +## Key rotation + +Regular key rotation is recommended for security. Here are typical rotation procedures: + +### SCP03 key rotation + +```csharp +// Authenticate with current keys +using var session = new SecurityDomainSession(yubiKeyDevice, currentScp03Params); + +// Replace with new keys +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); +session.PutKey(newKeyRef, newStaticKeys, currentKvn); +``` + +### SCP11 key rotation + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); + +// Generate new key pair +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); // Replaces oldKvn +``` + +## Security considerations + +1. **Key Protection** + - Store keys securely + - Use unique keys per device when possible + - Consider using SCP11 for mutual authentication + - Avoid storing sensitive keys in source code or configuration files + +2. **Key Version Management** + - Track which keys are loaded on each YubiKey + - Track KVNs in use + +3. **Recovery Planning** + - Maintain backup keys + - Document key recovery procedures + - Test recovery processes regularly + - Keep track of key histories + +4. **Default Keys** + - Default SCP03 keys provide no security + - Replace default keys in production environments + - Cannot retain default keys alongside custom keys + - Use proper key management in production + +> [!IMPORTANT] +> The YubiKey provides no metadata about installed keys beyond what's available through `GetKeyInformation()`. Your application must track additional key management details. \ No newline at end of file diff --git a/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-overview.md b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-overview.md new file mode 100644 index 000000000..526978209 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-overview.md @@ -0,0 +1,73 @@ +--- +uid: SecurityDomainOverview +--- + + + +# Security Domain overview + +The Security Domain is a special application on the YubiKey responsible for managing secure communication channels and cryptographic keys. It implements protocols defined by [Global Platform Consortium](https://globalplatform.org/) that provide confidentiality and integrity for commands sent between host applications and the YubiKey. + +For detailed information about the protocols, use cases, and transport options, see the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. + +## Requirements + +Hardware: +- YubiKey 5 Series or later +- For SCP03: Firmware 5.3 or later +- For SCP11: Firmware 5.7.2 or later + +Transport Protocols: +- Smartcard over USB or NFC + +## Core features + +The Security Domain provides: +- Management of secure communication channels (SCP03 and SCP11) +- Storage and management of cryptographic keys +- Certificate management for asymmetric protocols +- Access control through certificate allowlists + +## Basic usage + +```csharp +// Create session without SCP protection +using var session = new SecurityDomainSession(yubiKeyDevice); +session.GetKeyInformation(); + +// Create SCP protected session +using var session = new SecurityDomainSession(yubiKeyDevice, scpKeyParameters); +session.GenerateEcKey(parameters...); // Protected by secure channel +``` + +## Documentation structure + +The Security Domain functionality is documented in the following sections: + +- [Key Management](xref:SecurityDomainKeys) - Managing symmetric (SCP03) and asymmetric (SCP11) keys +- [Certificate Operations](xref:SecurityDomainCertificates) - Working with X.509 certificates and certificate chains +- [Common Tasks](xref:SecurityDomainTasks) - Setup, configuration, and maintenance operations +- [Device Information](xref:SecurityDomainDevice) - Device data and configuration management + +## Basic security considerations + +When working with the Security Domain: +- Most operations require an authenticated session +- Default SCP03 keys provide no security, replace them in production +- Some operations permanently modify the YubiKey +- Maintain proper key and certificate backups + +> [!NOTE] +> For detailed implementation guidance and best practices, refer to the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. \ No newline at end of file diff --git a/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-tasks.md b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-tasks.md new file mode 100644 index 000000000..2f0bb6bd8 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-tasks.md @@ -0,0 +1,264 @@ +--- +uid: SecurityDomainTasks +--- + + + +# Security Domain common tasks + +This document covers common operational tasks and workflows for managing the Security Domain. For detailed information about secure channels, see the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. + +## Setting up a new YubiKey + +### 1. Initial state assessment + +Check the current configuration of your YubiKey: + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice); +var keyInfo = session.GetKeyInformation(); +var hasDefaultKeys = keyInfo.Any(k => k.Key.VersionNumber == 0xFF); +``` + +### 2. Replacing default SCP03 keys + +Always replace default keys in production environments: + +```csharp +// Start with default keys +using var defaultSession = new SecurityDomainSession( + yubiKeyDevice, + Scp03KeyParameters.DefaultKey); + +// Generate or obtain your secure keys +var newKeys = new StaticKeys(newMacKey, newEncKey, newDekKey); +var keyRef = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); +defaultSession.PutKey(keyRef, newKeys); +``` + +> [!WARNING] +> Default keys provide no security. Replace them before deploying to production. + +## Setting up SCP11 + +### 1. Generate initial keys + +Start with an authenticated SCP03 session: + +```csharp +// Use SCP03 session to set up SCP11 +using var session = new SecurityDomainSession(yubiKeyDevice, scp03Params); + +// Generate SCP11b key pair +var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var publicKey = session.GenerateEcKey(keyRef); +``` + +### 2. Configure certificate chain + +```csharp +// Store certificates +session.StoreCertificates(keyRef, certificateChain); + +// Configure CA for SCP11a/c +var caRef = KeyReference.Create(OceKid, kvn); +session.StoreCaIssuer(caRef, skiBytes); +``` + +### 3. Set up access control + +```csharp +// Configure certificate allowlist +var allowedSerials = GetAllowedCertificateSerials(); +session.StoreAllowlist(keyRef, allowedSerials); +``` + +## Key management tasks + +### Rotating SCP03 keys + +```csharp +// Authenticate with current keys +using var session = new SecurityDomainSession(yubiKeyDevice, currentScp03Params); + +// Replace with new keys +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, newKvn); +session.PutKey(newKeyRef, newStaticKeys, currentKvn); +``` + +### Rotating SCP11 keys + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); + +// Generate new key pair +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, newKvn); +var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); // Will be replaced +``` + +## Recovery operations + +### Status check + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice); + +// Get key information +var keyInfo = session.GetKeyInformation(); +var activeKeys = keyInfo.Select(k => k.Key).ToList(); + +// Check certificates +foreach (var key in activeKeys) +{ + try + { + var certs = session.GetCertificates(key); + Console.WriteLine($"Key {key} has {certs.Count} certificates"); + } + catch + { + Console.WriteLine($"No certificates for key {key}"); + } +} +``` + +### Factory reset + +```csharp +// Warning: This removes all custom keys in the Security Domain +using var session = new SecurityDomainSession(yubiKeyDevice); +session.Reset(); +``` + +> [!IMPORTANT] +> Resetting removes all custom keys and certificates. Have a recovery plan ready. + +## Integration with other applications + +### PIV with secure channel + +```csharp +// Using SCP03 +using var pivSession = new PivSession(yubiKeyDevice, scp03Params); +pivSession.GenerateKeyPair(...); // Protected by SCP03 + +// Using SCP11 +using var pivSession = new PivSession(yubiKeyDevice, scp11Params); +pivSession.GenerateKeyPair(...); // Protected by SCP11 +``` + +### OATH with secure channel + +```csharp +// Using SCP03 +using var oathSession = new OathSession(yubiKeyDevice, scp03Params); +oathSession.PutCredential(...); // Protected by SCP03 + +// Using SCP11 +using var oathSession = new OathSession(yubiKeyDevice, scp11Params); +oathSession.PutCredential(...); // Protected by SCP11 +``` + +## Production deployment tasks + +### Initial provisioning + +1. **Prepare Keys and Certificates** +```csharp +var scp03Keys = GenerateSecureKeys(); +var (privateKey, publicKey, certificates) = GenerateScp11Credentials(); +``` + +2. **Configure YubiKey for SCP11B** +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); + +// Replace SCP03 keys +var scp03Ref = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); +session.PutKey(scp03Ref, scp03Keys); + +// Set up SCP11 +var scp11Ref = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var scp11Public = session.GenerateEcKey(scp11Ref); +session.StoreCertificates(scp11Ref, certificates); +``` + +3. **Validate Configuration** +```csharp +// Test new keys +using var verifySession = new SecurityDomainSession( + yubiKeyDevice, + new Scp03KeyParameters(scp03Ref, scp03Keys)); + +var keyInfo = verifySession.GetKeyInformation(); +// Verify expected keys are present +``` + +### Regular maintenance + +1. **Monitor Key Status** +```csharp +// Check key information regularly +var keyInfo = session.GetKeyInformation(); +foreach (var key in keyInfo) +{ + // Log key status and plan rotation if needed + LogKeyStatus(key); +} +``` + +2. **Certificate Management** +```csharp +// Check certificate expiration +var certificates = session.GetCertificates(keyRef); +foreach (var cert in certificates) +{ + if (cert.NotAfter < DateTime.Now.AddMonths(3)) + { + // Plan certificate renewal + PlanCertificateRenewal(cert); + } +} +``` + +## Troubleshooting + +### Key issues + +1. **Unable to Authenticate** + - Verify key version numbers + - Check key reference values + - Confirm key components + +2. **Failed Key Import** + - Validate key formats + - Check authentication status + - Verify available space + - Confirm key compatibility + +### Certificate issues + +1. **Certificate Chain Problems** + - Verify chain order + - Check CA configuration + - Validate certificate formats + +2. **Access Control Issues** + - Check allowlist configuration + - Verify certificate serials + - Validate certificate dates + +> [!NOTE] +> Always maintain detailed logs of key and certificate operations for troubleshooting. \ No newline at end of file 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 5f80ef871..0fd8f4e15 100644 --- a/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md +++ b/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md @@ -38,7 +38,7 @@ Bug Fixes: - The [PivSession.ChangeManagementKey](xref:Yubico.YubiKey.Piv.PivSession.ChangeManagementKey(Yubico.YubiKey.Piv.PivTouchPolicy)) method was incorrectly assuming Triple-DES was the default management key algorithm for FIPS keys. The SDK now verifies the management key alorithm based on key type and firmware version. ([#162](https://github.com/Yubico/Yubico.NET.SDK/pull/162)) - The SDK now correctly sets the IYubiKeyDeviceInfo property [IsSkySeries](xref:Yubico.YubiKey.IYubiKeyDeviceInfo.IsSkySeries) to True for YubiKey Security Key Series Enterprise Edition keys. ([#158](https://github.com/Yubico/Yubico.NET.SDK/pull/158)) -- Exceptions are now caught when running [PivSession.Dispose](xref:Yubico.YubiKey.Piv.PivSession.Dispose). This fixes an issue where the Dispose method could not close the Connection in the event of a disconnected YubiKey. ([#104](https://github.com/Yubico/Yubico.NET.SDK/issues/104)) +- Exceptions are now caught when running PivSession.Dispose. This fixes an issue where the Dispose method could not close the Connection in the event of a disconnected YubiKey. ([#104](https://github.com/Yubico/Yubico.NET.SDK/issues/104)) - A dynamic DLL resolution based on process architecture (x86/x64) has been implemented for NativeShims.dll. This fixes a reported issue with the NativeShims.dll location for 32-bit processes. ([#154](https://github.com/Yubico/Yubico.NET.SDK/pull/154)) Deprecations: @@ -401,7 +401,7 @@ Features: allows clients of smart cards to encrypt all traffic to and from the card. Since the YubiKey can act as a smart card, this means that it is now possible to encrypt all traffic for the PIV application. In order for this to work, however, your YubiKey must be pre-configured for this feature. Read more about - [SCP03 here](xref:UsersManualScp03). + [SCP03 here](xref:UsersManualScp). - **Debian, RHEL, and CentOS support**. Our testing of Linux platforms has expanded to include the Debian, Red Hat Enterprise Linux (RHEL), and CentOS distributions. Please read [running on Linux](xref:RunningOnLinux) for more details. diff --git a/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol-3.md b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol-3.md deleted file mode 100644 index d1e28e050..000000000 --- a/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol-3.md +++ /dev/null @@ -1,507 +0,0 @@ ---- -uid: UsersManualScp03 ---- - - - -# Secure Channel Protocol 3 ("SCP03") - -Commands sent to the YubiKey, or responses from the YubiKey, may contain -senstive data that should not leak to or be tampered with by other applications -on the host machine. The operating system of the host machine may provide at -least one layer of protection by isolating applications from each other using -separation of memory spaces, permissioned access to system resources like -devices, and other techniques. - -The YubiKey also supports an additional layer of protection that aims to provide -confidentiality and intergrity of communication to and from the YubiKey, using a -standardized protocol called "Secure Channel Protocol 3" (commonly referred to -as "SCP03"). This standard prescribes methods to encrypt and authenticate smart card -(CCID) messages. That is, APDUs and responses are encrypted and contain checksums. If -executed properly, the only entities that can see the contents of the messages (and -verify their correctness) will be the YubiKey itself and authorized applications. This -protocol is produced by GlobalPlatform, an industry consortium of hardware security -vendors that produce standards. - -Think of SCP03 as wrapping or unwrapping commands and responses. Before sending the actual -command to the YubiKey, wrap it in an SCP03 package. The YubiKey will be able to unwrap it -and execute the recovered command. After the YubiKey builds the actual response, it wraps -it in an SCP03 package, and the SDK can then unwrap the package and process the result. - -Only YubiKey 5 Series devices with firmware version 5.3 or later support the Secure -Channel Protocol (version 3). Other SCP03 versions are not supported by any YubiKey. In -addition, while the PIV, OATH, OpenPGP, and YubiCrypt applications use the smart card -protocols, only the PIV application supports sending and receiving SCP03 messages. - -SCP03 relies entirely on symmetric cryptography. Hence, its security is dependent on -making sure only authorized applications have access to the symmetric keys. The standard -specifies no method for distributing keys securely. - -This added layer of protection makes the most sense when the communication -channel between the host machine and the device could feasibly be compromised. -For example, if you tunnel YubiKey commands and resonses over the Internet, in -addition to standard web security protocols like TLS, it could makes sense to -leverage SCP03 as an added layer of defense. Additionally, several 'card -management systems' use SCP03 to securely remotely manage devices. - -> [!NOTE] -> SCP03 works only with SmartCard applications, namely PIV, OATH, and OpenPgp. -> However, SCP03 is supported only on series 5 YubiKeys with firmware version 5.3 -> and later, and only the PIV application. - -## Static Keys - -SCP03 relies on a set of shared, secret, symmetric cryptographic keys, called the -["static keys"](xref:Yubico.YubiKey.Scp03.StaticKeys), which are known to the application -and the YubiKey. - -Most YubiKeys are manufactured with a default set of keys. The value of these keys is -specified by the standard, so they are not secret. It is important to emphasize that using -the default SCP03 keys to connect to a device offers *no additional protection* over -cleartext communication. - -It is possible to manufacture YubiKeys with a non-default SCP03 key set (this will be a -custom order, see your sales rep if you are interested in a custom order), or to change -the keys on a YubiKey at any time. Hence, if you want to take advantage of SCP03, your -first task will be to make sure the YubiKey is loaded with a set of keys that only -authorized applications will know. See the sections below on -[replacing the default key set](#replacing-the-default-key-set) for a discussion on how to -do that. - -The three keys that comprise the `StaticKeys` are 16 byte, AES-128 cryptographic -keys, referred to in the GlobalPlatform SCP03 Specification as the channel encryption key -(Key-ENC), channel MAC key (Key-MAC), and data encryption key (Key-DEK). In the SDK, these -keys are held in a [StaticKeys](xref:Yubico.YubiKey.Scp03.StaticKeys) object. - -### Three key sets - -A YubiKey can contain up to three SCP03 key sets. Think of the YubiKey as having three -slots for SCP03 keys. - -```txt - slot 1: ENC MAC DEK - slot 2: ENC MAC DEK - slot 3: ENC MAC DEK -``` - -Each key is 16 bytes. YubiKeys do not support any other key size. - -Standard YubiKeys are manufactured with one key set, and each key in that set is the -default value. The default value (prescribed by the standard) is `0x40 41 42 ... 4F`. - -```txt - slot 1: ENC(default) MAC(default) DEK(default) - slot 2: --empty-- - slot 3: --empty-- -``` - -The SCP03 standard specifies that each key set be given a Key Version Number (KVN). That -is, when you specify a particular key set, you won't specify it by slot number but rather -by KVN. Think of the KVN as the key set's name. - -The standard declares that the default key set will have the KVN of 255 (0xFF). It also -specifies that the KVN for a non-default key set can be any number from 0x01 to 0x7F. The -standard places no other restrictions on the KVN. For example, a standard-compliant device -that holds three key sets could allow a caller to specify 0x5A, 0x21, 0x30 as the three -KVNs. - -However, the standard also specifies that a device can put its own limitations onto the -KVNs, and that's what the YubiKey does. A YubiKey only supports KVNs of 1, 2, 3, and 255. - -In addition, each key in the set is given a Key Identifier (KeyId). The YubiKey allows -only 1, 2, and 3 as the KeyIds, but there is no place in the SDK where a KeyId is needed. -That is, if you use the SDK to perform SCP03 on the YubiKey, the KeyId will never be used. - -This is the initial state of the standard YubiKey. - -```txt - slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) - slot 2: --empty-- - slot 3: --empty-- -``` - -When you add or replace a key set, you must specify the Key Version Number. Remember, the -YubiKey allows only 1, 2, or 3. For example, if you replace the default key set and you -specify 1 as the Key Version Number, your YubiKey's SCP03 key situation would look like -this: - -```txt - slot 1: KVN=1 ENC(new) MAC(new) DEK(new) - slot 2: --empty-- - slot 3: --empty-- -``` - -Suppose you have a YubiKey that has only the default key set, and you put a new SCP03 key -set onto the device. But this time, you specify 2 as the Key Version Number. In this case, -the new key set would be placed into the second slot, but the default key set would still -be removed. - -```txt - slot 1: --empty-- - slot 2: KVN=2 ENC(new) MAC(new) DEK(new) - slot 3: --empty-- -``` - -This happens only when the default key set is on the YubiKey. Once the default key set has -been removed, it is possible to add a new key set without removing a previous one. That -is, after the default key set has been removed, you can add a new key set to an empty slot -without removing any existing key set. - -For example, if there is a key set for KVN=2, and only KVN=2, and you add a key set to -KVN=3, the YubiKey's state will be this: - -```txt - slot 1: --empty-- - slot 2: KVN=2 ENC MAC DEK - slot 3: KVN=3 ENC(new) MAC(new) DEK(new) -``` - -It is also possible to replace a key set, that is described [below](#replacing-a-key-set). - -#### Slot number and KVN - -In SCP03, there is no concept of slots or slot number. We use those terms just to -illustrate the model. They are meant to describe a location on the YubiKey where key sets -reside. - -When writing code to use SCP03, you use the KVN. That is what the standard specifies. -Your code will always specify a KVN, and the only KVNs you can use with a YubiKey are -1, 2, 3, and 255. - -You can imagine the actual key data located in slots on the YubiKey, and in the model -described in this document, the slot number and KVN are almost always the same. That is, -slot 1 holds the key set with KVN=1, and so on. The exception is when the only key set -on a YubiKey is the default key. In that case, slot 1 is holding the key set with -KVN=255. - -### The `StaticKeys` class - -In order to perform SCP03 operations in the SDK, you must supply one of the key sets -currently residing on the YubiKey. This is done using the `StaticKeys` class. - -```csharp - using var scp03Keys = new StaticKeys(keyDataMac, keyDataEnc, keyDataDek) - { - KeyVersionNumber = 2 - } -``` - -The `StaticKeys` class implements `IDisposable`. Hence, you should use the `using` keyword -when instantiating. When the object goes out of scope, the `Dispose` method will be called -and the key data will be overwritten. - -When you supply a `StaticKeys` object to the SDK (version 1.9 and later), the object is -cloned (a deep copy is made). - -### Managing the keys - -It is the responsibility of the application to know which SCP03 keys are loaded on a -YubiKey. There are no calls to return "metadata". For example, there is no command that -can return how many key sets are loaded on a YubiKey or whether the default key has been -replaced. - -Your application must know if a particular YubiKey has been programmed at manufacture with -non-default keys, or if the YubiKey is still configured with the default key set. If you -replace the default key set, your application must manage the keys that were loaded -because it will need to provide those keys the next time you use SCP03. - -Your application must know if more than one key set is loaded onto a YubiKey, what the key -data is, and what Key Version Number is used for each set. - -## Using SCP03 - -There are two categories of SCP03 operations: - -* Using SCP03 to secure Smart Card communications -* Performing SCP03 operations, such as changing or adding a key set - -### Securing Smart Card communications - -Suppose you are performing PIV operations. You would normally get a YubiKey and then -instantiate the `PivSession` class. - -```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } - using (var pivSession = new PivSession(yubiKeyDevice)) - { - . . . - } -``` - -In order to use SCP03 to securely communicate with the YubiKey, all you need to do is -obtain your `StaticKeys` and supply that key set to the `PivSession` constructor. - -```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } - // This is the only change you need to make in order to make sure your PIV operations - // are protected using SCP03 (assuming you have some method to retrieve the key set). - using StaticKeys scp03Keys = RetrieveScp03KeySet(); - using (var pivSession = new PivSession(yubiKeyDevice, scp03Keys)) - { - . . . - } -``` - -Once you have built the `PivSession` with SCP03, there's no other SCP03 operation you -need to do. Each PIV operation will now be protected with SCP03. - -Under the covers, each command sent to the YubiKey will first pass through an SCP03 object -which encrypts the data and appends a checksum. That is, before "leaving the SDK", each -command is "wrapped" using SCP03. - -The YubiKey returns responses that have been protected using SCP03. Before parsing the -response, the SDK must verify the checksum and decrypt the data. In other words, the first -thing the SDK does is remove the response's SCP03 "wrapper". - -If you are calling commands directly instead of using the `PivSession`, you can still use -SCP03 by making a connection using -[ConnectScp03](xref:Yubico.YubiKey.IYubiKeyDevice.ConnectScp03%2A): - -```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } - - using IYubiKeyConnection connection = yubiKeyDevice.ConnectScp03(YubiKeyApplication.Piv, scp03Keys); -``` - -### Performing SCP03 operations - -It is possible you must perform some SCP03 operation directly. That is, you want to -perform an operation that is not wrapping a PIV command or unwrapping a response. For -example, you might need to replace the default SCP03 key set. In this case, use the -[Scp03Session](xref:Yubico.YubiKey.Scp03.Scp03Session) class. - -```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } - using StaticKeys scp03Keys = RetrieveScp03KeySet(); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - . . . - } -``` - -Operations "inside" the `Scp03Session` will be protected using SCP03. That is, in order to -preform an SCP03 operation, one or more commands will be sent to the YubiKey. These -commands are instructing the YubiKey to perform some set of SCP03 operations. Each of -these commands, and the responses, will be wrapped using SCP03 as well. - -The SDK has methods that perform these SCP03 operations: - -* [Replace the default key set](#replacing-the-default-key-set) -* [Add a new key set](#adding-a-new-key-set) -* [Replace a non-default key set](#replacing-a-key-set) -* [Removing a key set](#removing-a-key-set) -* [Removing all key sets](#removing-all-key-sets) (reset the YubiKey to the default key set) - -#### Replacing the default key set - -One of the first things you want to do with SCP03 is to replace the default key set. To do -so, call [Scp03Session.PutKeySet](xref:Yubico.YubiKey.Scp03.Scp03Session.PutKeySet%2A). - -```csharp - bool isValid = YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - // using the no-arg constructor will build a StaticKeys object using the default - // key set with the KVN=0xFF. - using var scp03Keys = new StaticKeys(); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - // Assume you have some method that will retrieve the key set you want to use. - // Perhaps it uses a key derivation function based on a YubiKey's serial number. - // In this sample, assume the GetStaticKeys method will return a StaticKeys object - // with the KVN of the input arg, in this case, the KVN will be 1. - using StaticKeys newKeys = GetStaticKeys(1); - scp03Session.PutKeySet(newKeys); - } -``` - -The `StaticKeys` object contains a `KeyVersionNumber`. Remember, the KVN is essentially -the key set's "name". The only KVNs for non-default key sets the YubiKey allows are -1, 2, and 3. - -Once a new key set has been added, the default key set is no longer available. That is, -even if you specify KVN=2 or 3 when you put a new key set onto a YubiKey, the default -key set will not remain. You might think that the default key set is in slot 1, and if -you put a new key set into slot 2 (KVN=2), the default key set will not be affected. -However, if a YubiKey is set with the default key set, and you call the `PutKeySet` -method, the default key set will be removed, no matter what the new key set's KVN is. - -#### Adding a new key set - -To add a new key set, simply make sure the KVN of the `StaticKeys` you add is not the same -as the KVN of the existing key set. For example, suppose you replaced the default key set, -specifying the new key set's KVN as 1. - -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: --empty-- - slot 3: --empty-- -``` - -To add a new key set with KVN=2, do the following: - -```csharp - bool isValid = YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice); - // Assume you have a method to retrieve key sets, and you specify retrieving - // the key set with KVN of 1. - using StaticKeys scp03Keys = GetStaticKeys(1); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - // Now get the key set with KVN of 2. You have the keys, they just have - // not been loaded onto the YubiKey yet. - StaticKeys newKeys = GetStaticKeys(2); - // If you want, make sure the StaticKeys object has the correct KVN. - newKeys.KeyVersionNumber = 2; - scp03Session.PutKeySet(newKeys); - newKeys.Clear(); - } -``` - -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: KVN=2 ENC(new) MAC(new) DEK(new) - slot 3: --empty-- -``` - -#### Replacing a key set - -In order to replace a key set, use the `PutKeySet` method. However, there is one -restriction: you can replace a key set only if you build the `Scp03Session` using the key -set that is to be replaced. - -```csharp - // This works - // Replace the key set with KVN=1 with new keys. - // Use the current KVN=1 key set to build the Scp03Session, get the new key set (the - // StaticKeys object holding the new key set will have KeyVersionNumber=1), and Put that - // new key set. - using StaticKeys scp03Keys = GetStaticKeys(1); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - // Assume you have a program that will retrieve a new key set for the given KVN. - using StaticKeys newKeySet = GetNewStaticKeys(1); - newKeySet.KeyVersionNumber = 1; - scp03Session.PutKeySet(newKeySet); - } -``` - -The following demonstrates an attempt to replace a key set that will not work. The result -will be an exception. In this case, suppose a YubiKey has two key sets loaded, KVN=1 and -KVN=2. - -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: KVN=2 ENC MAC DEK - slot 3: --empty-- -``` - -```csharp - // This does NOT work - // Assume you have a key set with KVN of 1, and you already have a key set with KVN of 2. - // Use the current KVN=1 key set to build the Scp03Session, get the new key set (the - // StaticKeys object holding the new key set will have KeyVersionNumber=2), and try to - // Put that new key set. - using StaticKeys scp03Keys = GetStaticKeys(1); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - using StaticKeys newKeySet = GetNewStaticKeys(2); - newKeySet.KeyVersionNumber = 2; - scp03Session.PutKeySet(newKeySet); - } -``` - -The `PutKeySet` method will throw an exception. In order to change the key set with KVN=2, -you must create the `Scp03Session` using the key set with KVN=2. - -#### Removing a key set - -To delete a key set, simply call the `DeleteKeySet` method. There is one restriction: the -key set used to build the `Scp03Session` must NOT be the key set deleted, unless there are -no more key sets on the YubiKey. Otherwise no key set will be deleted and the SDK will -throw an exception. - -```csharp - using StaticKeys scp03Keys = GetStaticKeys(1); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - scp03Session.DeleteKeySet(2, false); - } -``` - -Notice that this sample code created the `Scp03Session` using KVN=1, but deleted the key -set with KVN=2. - -#### Removing all key sets - -After you use one of the key sets to remove the other two, it is possible to remove that -last key set. If it is removed, then the YubiKey will reset its SCP03 program to the -original, default state. Namely, there will be one key set, and it will be the default -(KVN=0xff). - -For example, suppose you have three SCP03 key sets on the YubiKey. - -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: KVN=2 ENC MAC DEK - slot 3: KVN=3 ENC MAC DEK -``` - -Use the KVN=3 key set to delete key sets 1 and 2. - -```csharp - using StaticKeys scp03Keys = GetStaticKeys(3); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - scp03Session.DeleteKeySet(1, false); - scp03Session.DeleteKeySet(2, false); - } -``` - -```txt - slot 1: --empty-- - slot 2: --empty-- - slot 3: KVN=3 ENC MAC DEK -``` - -Now it is possible to delete the KVN=3 key set using an `Scp03Session` created using -the KVN=3 key set. Pass true for the second argument. - -```csharp - using StaticKeys scp03Keys = GetStaticKeys(3); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - scp03Session.DeleteKeySet(3, true); - } -``` - -Passing `true` lets the SDK know this is the last key to be deleted. It can then call the -DELETE KEY command with the appropriate parameters, and the YubiKey will be able to delete -the key set. After this delete, the state of SCP03 key sets will be the following: - -```txt - slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) - slot 2: --empty-- - slot 3: --empty-- -``` diff --git a/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol.md b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol.md new file mode 100644 index 000000000..fe9ae9780 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol.md @@ -0,0 +1,568 @@ +--- +uid: UsersManualScp +--- + + + + +# Secure Channel Protocol (SCP) + +Commands sent to the YubiKey, or responses from the YubiKey, may contain sensitive data that should not leak to or be tampered with by other applications on the host machine. While the operating system provides some protection through memory space isolation and permissioned access, YubiKeys support additional layers of protection through Secure Channel Protocols (SCP). + +These protocols, defined by GlobalPlatform, provide confidentiality and integrity of communication between the host and YubiKey. +This standard prescribes methods to encrypt and authenticate smart card +(CCID) messages. That is, APDUs and responses are encrypted and contain checksums. If executed properly, the only entities that can see the contents of the messages (and +verify their correctness) will be the YubiKey itself and authorized applications. +The YubiKey supports two main variants of SCP: + +- **SCP03** - A symmetric key protocol using AES-128 for encryption and authentication +- **SCP11** - An asymmetric protocol using elliptic curve cryptography (ECC) and X.509-certificates + +## Protocol overview + +### SCP03 (symmetric) +SCP03 provides a secure channel using shared secret keys. It is simpler to implement but requires secure key distribution. Think of SCP03 as wrapping commands and responses in an encrypted envelope that only trusted parties can open. + +Key characteristics: +- Uses AES-128 symmetric keys +- Three keys per set: encryption, MAC, and data encryption +- Supported on YubiKey 5 Series with firmware 5.3+ + +### SCP11 (asymmetric) +SCP11 uses public key cryptography for authentication and key agreement. It provides stronger security guarantees and simpler key management, but with more complex implementation. SCP11 comes in three variants: + +- **SCP11a** - Mutual authentication between YubiKey and host +- **SCP11b** - YubiKey authenticates to host only +- **SCP11c** - Mutual authentication with additional features, such as offline scripting usage (See [GlobalPlatform SCP11 Specification Annex B](https://globalplatform.org/specs-library/secure-channel-protocol-11-amendment-f/)) + +Key characteristics: +- Uses NIST P-256 elliptic curve +- Certificate-based authentication +- Supported on YubiKey 5 Series with firmware 5.7.2+ + +## When to use secure channels + +Secure channels are particularly valuable when: + +- Communicating with YubiKeys over networks or untrusted channels (e.g. NFC) +- Managing YubiKeys remotely through card management systems +- Ensuring end-to-end security beyond transport encryption + +For example, if you tunnel YubiKey commands over the Internet, you might use TLS for transport security and add SCP as an additional layer of defense. + +## Security considerations + +SCP03 relies entirely on symmetric cryptography, making key distribution a critical security concern. Most YubiKeys ship with default SCP03 keys that are publicly known - using these provides no additional security over cleartext communication. + +SCP11, being asymmetric, simplifies key management but requires proper certificate handling and validation. Each variant (a/b/c) offers different security properties suitable for different use cases. + + It is possible to manufacture YubiKeys with custom non-default SCP key sets (this requires a custom order - contact your Yubico sales representative for details). + +The following sections detail how to implement both protocols, manage keys and certificates, and integrate secure channels with various YubiKey applications. + +## Using secure channels with YubiKey applications + +The SDK provides a consistent way to use secure channels across different YubiKey applications. You can enable secure channel communication by providing SCP key parameters when creating application sessions. + +### Common pattern + +Each application session (PIV, OATH, OTP, YubiHSM Auth) accepts an optional `ScpKeyParameters` parameter. This can be either `Scp03KeyParameters` or `Scp11KeyParameters` depending on which protocol you want to use. + +```csharp +// Using SCP03 +using var scp03Params = Scp03KeyParameters.DefaultKey; // For testing only +using var pivSession = new PivSession(yubiKeyDevice, scp03Params); + +// Using SCP11b +using var sdSession = new SecurityDomainSession(yubiKeyDevice, scp03Params); + +// Create SCP11b key parameters from public key on YubiKey +var keyVersionNumber = 0x1; // Example kvn +var keyId = ScpKeyIds.SCP11B; +var keyReference = KeyReference.Create(keyId, keyVersionNumber); + +// Get certificate from YubiKey +var certificates = sdSession.GetCertificates(keyReference); + +// Verify the Yubikey's certificate chain against a trusted root using your implementation +CertificateChainVerifier.Verify(certificateList) + +// Use the verified leaf certificate to construct ECPublicKeyParameters +var publicKey = certificates.Last().GetECDsaPublicKey(); +var scp11Params = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(publicKey)); + +// Use SCP11b parameters to open connection +using (var pivSession = new PivSession(yubiKeyDevice, scp11Params)) +{ + // All PivSession-commands are now automatically protected by SCP11 + session.GenerateKeyPair(PivSlot.Retired12, PivAlgorithm.EccP256, PivPinPolicy.Always); // Protected by SCP11 +} +``` + +### Application examples + +#### PIV with secure channel + +```csharp +// Using SCP03 +StaticKeys scp03Keys = RetrieveScp03KeySet(); // Your static keys +using Scp03KeyParameters scp03Params = Scp03KeyParameters.FromStaticKeys(scp03Keys); +using (var pivSession = new PivSession(yubiKeyDevice, scp03params)) +{ + // All PivSession-commands are now automatically protected by SCP03 +} + +// Using SCP11b +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); +using (var pivSession = new PivSession(yubiKeyDevice, scp11Params)) +{ + // All PivSession-commands are now automatically protected by SCP11 +} +``` + +#### OATH with secure channel +```csharp + +// Using SCP03 +StaticKeys scp03Keys = RetrieveScp03KeySet(); // Your static keys +using Scp03KeyParamaters scp03Params = Scp03KeyParameters.FromStaticKeys(scp03Keys); +using (var oathSession = new OathSession(yubiKeyDevice, scp03params)) +{ + // All oathSession-commands are now automatically protected by SCP03 +} + +// Using SCP11b +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); +using (var oathSession = new OathSession(yubiKeyDevice, scp11Params)) +{ + // All OathSession-commands are now automatically protected by SCP11 +} +``` + +#### OTP with secure channel +```csharp + +// Using SCP03 +StaticKeys scp03Keys = RetrieveScp03KeySet(); // Your static keys +using Scp03KeyParamaters scp03Params = Scp03KeyParameters.FromStaticKeys(scp03Keys); +using (var otpSession = new OtpSession(yubiKeyDevice, scp03params)) +{ + // All otpSession-commands are now automatically protected by SCP03 +} + +// Using SCP11b +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); +using (var otpSession = new OtpSession(yubiKeyDevice, scp11Params)) +{ + // All OtpSession-commands are now automatically protected by SCP11 +} +``` + +#### YubiHSM Auth with secure channel +```csharp +// Using SCP03 +StaticKeys scp03Keys = RetrieveScp03KeySet(); // Your static keys +using Scp03KeyParamaters scp03Params = Scp03KeyParameters.FromStaticKeys(scp03Keys); +using (var yubiHsmSession = new YubiHsmAuthSession(yubiKeyDevice, scp03params)) +{ + // All YubiHsmSession-commands are now automatically protected by SCP03 +} + +// Using SCP11b +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); +using (var yubiHsmSession = new YubiHsmSession(yubiKeyDevice, scp11Params)) +{ + // All yubiHsmSession-commands are now automatically protected by SCP11 +} + +``` + +### Direct connection + +If you need lower-level control, you can establish secure connections directly using [`Connect`](xref:Yubico.YubiKey.IYubiKeyDevice.Connect*): + +```csharp +// Using application ID +using var connection = yubiKeyDevice.Connect( + applicationId, // byte array for ISO7816 applicationId + Scp03KeyParameters.DefaultKey); + +// Using YubiKeyApplication enum +using var connection = yubiKeyDevice.Connect( + YubiKeyApplication.Piv, + scp11Parameters); + +// Try pattern +if (yubiKeyDevice.TryConnect( + YubiKeyApplication.Oath, + scpParameters, + out var connection)) +{ + using (connection) + { + // Use connection + } +} +``` + +### Security Domain management + +The [`SecurityDomainSession`](xref:Yubico.YubiKey.Scp.SecurityDomainSession) class provides methods to manage SCP configurations: + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); + +// Get information about installed keys +var keyInfo = session.GetKeyInformation(); + +// Store certificates for SCP11 +session.StoreCertificates(keyReference, certificates); + +// Manage allowed certificate serials +session.StoreAllowlist(keyReference, allowedSerials); + +// Import private key +session.PutKey(keyReference, privateKeyParameters) + +// Import public key +session.PutKey(keyReference, publicKeyParameters) + +// Reset to factory defaults +session.Reset(); +``` + +> [!NOTE] +> Using `DefaultKey` in production code provides no security. Always use proper key management in production environments. + +The next sections will detail specific key management and protocol details for both SCP03 and SCP11. + +## SCP03 (symmetric key protocol) + +### Static keys structure + +SCP03 relies on a set of shared, secret, symmetric cryptographic keys. Each key set consists of three 16-byte AES-128 keys encapsulated in the [`StaticKeys`](xref:Yubico.YubiKey.Scp03.StaticKeys) class: + +- Channel encryption key (Key-ENC) +- Channel MAC key (Key-MAC) +- Data encryption key (Key-DEK) + +These keys are encapsulated in a `StaticKeys` class and provided to the SDK via `Scp03KeyParameters`: + +```csharp +var staticKeys = new StaticKeys(keyDataMac, keyDataEnc, keyDataDek); +var scp03Params = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); +``` + +### Key sets on the YubiKey + +A YubiKey can contain up to three SCP03 key sets. Each set is identified by a Key Version Number (KVN): + +```txt + slot 1: ENC MAC DEK (KVN=1) + slot 2: ENC MAC DEK (KVN=2) + slot 3: ENC MAC DEK (KVN=3) +``` + +Standard YubiKeys are manufactured with a default key set (KVN=0xFF): + +```txt + slot 1: ENC(default) MAC(default) DEK(default) + slot 2: --empty-- + slot 3: --empty-- +``` + +The default keys are publicly known (0x40 41 42 ... 4F) and provide no security. You should replace them in production environments. + +### Managing key sets + +Use `SecurityDomainSession` to manage SCP03 key sets: + +```csharp +// Replace default keys +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); +var newKeys = new StaticKeys(newKeyDataMac, newKeyDataEnc, newKeyDataDek); +var newKeyParams = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, newKeys); +session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys); + +// Add another key set +using var session = new SecurityDomainSession(yubiKeyDevice, existingKeyParams); +var keyRef = KeyReference.Create(ScpKeyIds.Scp03, 0x02); // KVN=2 +session.PutKey(keyRef, additionalKeys); + +// Delete a key set +session.DeleteKey(keyRef, false); + +// Reset to factory defaults (restore default keys) +session.Reset(); +``` + +### Key set rules + +1. **Key Version Numbers (KVN):** + - Default key set: KVN=0xFF + - Accepted values are between 1 and 0x7F + +2. **Key Id's (KID)** + - Default value: 1 + - Accepted values are between 1 and 3 + +3. **Default Key Replacement:** + - When adding first custom key set, default keys are always removed + - Cannot retain default keys alongside custom keys + +4. **Multiple Key Sets:** + - After default keys are replaced, can have 1-3 custom key sets + - Each must have unique KID + - Can add/remove without affecting other sets + +### Example: complete key management flow + +```csharp +// Start with default keys +var defaultScp03Params = Scp03KeyParameters.DefaultKey; +using (var session = new SecurityDomainSession(yubiKeyDevice, defaultScp03Params)) +{ + // Add first custom key set (removes default) + var keyRef1 = KeyReference.Create(ScpKeyIds.Scp03, 0x01); + session.PutKey(keyRef1, newKeys); +} + +// Now authenticate with new keys +var newScp03Params = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, newKeys); +using (var session = new SecurityDomainSession(yubiKeyDevice, newScp03Params)) +{ + // Add second key set + var keyRef2 = KeyReference.Create(ScpKeyIds.Scp03, 0x02); + session.PutKey(keyRef2, customKeys2); + + // Check current key information + var keyInfo = session.GetKeyInformation(); +} +``` + +### Key management responsibilities + +Your application must: +- Track which keys are loaded on each YubiKey +- Know if a YubiKey has custom keys from manufacturing +- Manage key distribution and storage +- Track KVNs in use +- Handle key rotation + +The YubiKey provides no metadata about installed keys beyond what's available through `GetKeyInformation()`. + +> [!NOTE] +> Always use proper key management in production. Never store sensitive keys in source code or configuration files. + +## SCP11 (asymmetric key protocol) + +SCP11 uses asymmetric cryptography based on elliptic curves (NIST P-256) for authentication and key agreement. Compared to SCP03, it uses certificates instead of pre-shared keys, providing greater flexibility in cases where the two entities setting up the secure channel are not deployed in strict pairs. The secure channel can be embedded into complex use cases, such as: +- Installation of payment credentials on wearables +- Production systems +- Remote provisioning of cell phone subscriptions + +Detailed information about SCP11 can be found in [GlobalPlatform Card Technology, Secure Channel Protocol '11' Card Specification v2.3 – Amendment F, Chapter 2](https://globalplatform.org/specs-library/secure-channel-protocol-11-amendment-f/) + + +It comes in three variants, each offering different security properties: + +### SCP11 variants + +- **SCP11a**: Full mutual authentication between host and YubiKey using certificates + - Basic mutual authentication + - Uses both static and ephemeral key pairs + - Requires certificate chain and off-card entity (OCE) verification + - Supports authorization rules in OCE certificates + - Suitable for direct host-to-YubiKey communication + +- **SCP11b**: YubiKey authenticates to host only + - Simplest variant, no mutual authentication + - Uses both static and ephemeral key pairs + - Suitable when host authentication isn't required + +- **SCP11c**: Enhanced mutual authentication with additional features + - Uses both static and ephemeral key pairs + - Supports offline scripting mode: + - Can precompute personalization scripts for groups of cards + - Scripts can be deployed via online services or companion apps + - Cryptographic operations remain on secure OCE server + - Supports authorization rules in OCE certificates + +### Key benefits of SCP11 over SCP03 + +SCP11 provides several advantages over SCP03: +- Uses certificates instead of pre-shared keys for authentication +- More flexible deployment - doesn't require strict pairing of entities +- Supports ECC for key establishment with AES-128 +- Better suited for complex deployment scenarios + +### Key parameters + +Unlike SCP03's static keys, SCP11 uses `Scp11KeyParameters` which can contain: +- Public/private key pairs +- Certificates +- Key references +- Off-card entity (OCE) information + +```csharp +// SCP11b basic parameters +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, 0x1); +var scp11Params = new Scp11KeyParameters( + keyReference, + new ECPublicKeyParameters(publicKey)); + +// SCP11a/c with full certificate chain +var scp11Params = new Scp11KeyParameters( + keyReference, // Key reference for this connection + pkSdEcka, // Public key for key agreement + oceKeyReference, // Off-card entity reference + skOceEcka, // Private key for key agreement + certificateChain); // Certificate chain for authentication +``` + +### Key management + +Use `SecurityDomainSession` to manage SCP11 keys and certificates: + +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); + +// Generate new EC key pair +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, 0x3); +var publicKey = session.GenerateEcKey(keyReference); + +// Import existing key pair +var privateKey = new ECPrivateKeyParameters(ecdsa); +session.PutKey(keyReference, privateKey); + +// Store certificates +session.StoreCertificates(keyReference, certificates); + +// Manage certificate serial number allowlist +var serials = new List { + "7F4971B0AD51F84C9DA9928B2D5FEF5E16B2920A", // Examples + "6B90028800909F9FFCD641346933242748FBE9AD" +}; +session.StoreAllowlist(oceKeyReference, serials); +``` + +### SCP11b example + +Simplest variant, where YubiKey authenticates to host: + +```csharp +// Get certificates stored on YubiKey +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, 0x1); +IReadOnlyCollection certificateList; +using (var session = new SecurityDomainSession(yubiKeyDevice)) +{ + certificateList = session.GetCertificates(keyReference); +} + +// Verify the certificate chain against a trusted root using your implementation +CertificateChainVerifier.Verify(certificateList) + +// Create parameters using leaf certificate which has now been verified +var leaf = certificateList.Last(); +var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); +var keyParams = new Scp11KeyParameters( + keyReference, + new ECPublicKeyParameters(ecDsaPublicKey)); + +// Use with any application +using var pivSession = new PivSession(yubiKeyDevice, keyParams); +``` + +### SCP11a example + +Full mutual authentication requires more setup: + +```csharp +// Start with default SCP03 connection +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); + +const byte kvn = 0x03; +var keyRef = KeyReference.Create(ScpKeyIds.Scp11A, kvn); + +// Generate new key pair on YubiKey +var newPublicKey = session.GenerateEcKey(keyRef); + +// Setup off-card entity (OCE) +var oceRef = KeyReference.Create(OceKid, kvn); +var ocePublicKey = new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()); +session.PutKey(oceRef, ocePublicKey); + +// Store CA identifier +var ski = GetSubjectKeyIdentifier(oceCerts.Ca); +session.StoreCaIssuer(oceRef, ski); + +// Create SCP11a parameters +var scp11Params = new Scp11KeyParameters( + keyRef, + new ECPublicKeyParameters(newPublicKey.Parameters), + oceRef, + new ECPrivateKeyParameters(privateKey), + certChain); + +// Use the secure connection +using var session = new SecurityDomainSession(yubiKeyDevice, scp11Params); +``` + +### Security considerations + +1. **Certificate Management:** + - Proper certificate validation is crucial + - Consider using certificate allowlists + - Manage certificate chains carefully + +2. **Key Generation:** + - Can generate keys on YubiKey or import existing + - YubiKey-generated keys never leave the device + - Imported keys must be properly protected + +3. **Protocol Selection:** + - SCP11b: Simplest variant, no mutual authentication + - SCP11a: Better security through mutual authentication + - SCP11c: Additional features over SCP11a + +4. **Certificate Allowlists:** + - Restrict which certificates can authenticate + - Update lists as certificates change + - Clear allowlists when no longer needed + - Can be used as a part of a certificate revocation stategy + +### Checking SCP support + +```csharp +// Check firmware version for SCP11 support +if (yubiKeyDevice.HasFeature(YubiKeyFeature.Scp11)) +{ + // Device supports SCP11 +} + +// Get information about installed keys +using var session = new SecurityDomainSession(yubiKeyDevice); +var keyInfo = session.GetKeyInformation(); + +// Get supported CA identifiers +var caIds = session.GetSupportedCaIdentifiers(true, true); +``` + +> [!NOTE] +> SCP11 requires firmware version 5.7.2 or later. Earlier firmware versions only support SCP03. + +# Additional documentation +- [Global Platform Consortium](https://globalplatform.org/) +- [GlobalPlatform SCP11 Specification](https://globalplatform.org/specs-library/secure-channel-protocol-11-amendment-f/) \ 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 6875406d6..473fc1866 100644 --- a/Yubico.YubiKey/docs/users-manual/toc.yml +++ b/Yubico.YubiKey/docs/users-manual/toc.yml @@ -48,8 +48,8 @@ href: sdk-programming-guide/alternate-crypto.md - name: Sensitive data href: sdk-programming-guide/sensitive-data.md - - name: Secure Channel Protocol 3 (SCP03) - href: sdk-programming-guide/secure-channel-protocol-3.md + - name: Secure Channel Protocol (SCP03, SCP11) + href: sdk-programming-guide/secure-channel-protocol.md - name: Commands href: sdk-programming-guide/commands.md - name: Device notifications @@ -242,6 +242,17 @@ href: application-piv/apdu/verify-uv.md - name: "Get firmware version" href: application-piv/apdu/version.md +- name: "Application: Security Domain" + homepage: application-security-domain/security-domain-overview.md + items: + - name: Key Management + href: application-security-domain/security-domain-keys.md + - name: Certificate Operations + href: application-security-domain/security-domain-certificates.md + - name: Common Tasks + href: application-security-domain/security-domain-tasks.md + - name: Device Information + href: application-security-domain/security-domain-device.md - name: "Application: FIDO U2F" homepage: application-u2f/fido-u2f-overview.md diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs index cdc9dd487..aa7863731 100644 --- a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs +++ b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs @@ -1283,6 +1283,15 @@ internal static string InvalidVerifyUvArguments { } } + /// + /// Looks up a localized string similar to Key agreement receipts do not match. + /// + internal static string KeyAgreementReceiptMissmatch { + get { + return ResourceManager.GetString("KeyAgreementReceiptMissmatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to The keyboard connection does not support writing more than 64 bytes to the device.. /// @@ -1913,15 +1922,6 @@ internal static string PinTooShort { } } - /// - /// Looks up a localized string similar to Exception caught when disposing PivSession: {0}, {1}. - /// - internal static string PivSessionDisposeUnknownError { - get { - return ResourceManager.GetString("PivSessionDisposeUnknownError", resourceCulture); - } - } - /// /// Looks up a localized string similar to The private ID must be set either explicitly or by specifying that it should be generated before it can be read.. /// @@ -2003,6 +2003,15 @@ internal static string Scp03KeyMismatch { } } + /// + /// Looks up a localized string similar to Exception caught when disposing Session: {0}, {1}. + /// + internal static string SessionDisposeUnknownError { + get { + return ResourceManager.GetString("SessionDisposeUnknownError", resourceCulture); + } + } + /// /// Looks up a localized string similar to The HMAC-SHA512 algorithm is not supported on YubiKeys with firmware version less than 4.3.1.. /// @@ -2175,11 +2184,11 @@ internal static string UnknownFidoError { } /// - /// Looks up a localized string similar to An unknown SCP03 error has occurred.. + /// Looks up a localized string similar to An unknown SCP error has occurred. /// - internal static string UnknownScp03Error { + internal static string UnknownScpError { get { - return ResourceManager.GetString("UnknownScp03Error", resourceCulture); + return ResourceManager.GetString("UnknownScpError", resourceCulture); } } diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.resx b/Yubico.YubiKey/src/Resources/ExceptionMessages.resx index 479c635b4..f73f2e6a9 100644 --- a/Yubico.YubiKey/src/Resources/ExceptionMessages.resx +++ b/Yubico.YubiKey/src/Resources/ExceptionMessages.resx @@ -266,7 +266,7 @@ The provided PIN or PUK value violates current complexity conditions. - + PIV management key mutual authentication failed because the YubiKey did not authenticate. @@ -296,7 +296,7 @@ The temporary PIN length is invalid. - + The length of input to a Put Data operation, {0}, was invalid, the maximum is {1}. @@ -352,8 +352,8 @@ An unknown error has occurred. Candidate for removal - - An unknown SCP03 error has occurred. + + An unknown SCP error has occurred Cannot convert the data to the requested type. Expected only {0} byte(s) of data, but {1} byte(s) were present. @@ -904,7 +904,10 @@ The URI query cannot be null or empty. - - Exception caught when disposing PivSession: {0}, {1} + + Exception caught when disposing Session: {0}, {1} + + + Key agreement receipts do not match \ No newline at end of file diff --git a/Yubico.YubiKey/src/Yubico.YubiKey.csproj b/Yubico.YubiKey/src/Yubico.YubiKey.csproj index e5558830c..edba741b4 100644 --- a/Yubico.YubiKey/src/Yubico.YubiKey.csproj +++ b/Yubico.YubiKey/src/Yubico.YubiKey.csproj @@ -1,4 +1,4 @@ - + diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionPasswordTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionPasswordTests.cs index ce3043ab7..dc492b2d4 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionPasswordTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionPasswordTests.cs @@ -13,6 +13,7 @@ // limitations under the License. using Xunit; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.Oath @@ -21,101 +22,130 @@ namespace Yubico.YubiKey.Oath [Trait(TraitTypes.Category, TestCategories.Simple)] public sealed class OathSessionPasswordTests { - [Theory, TestPriority(0)] - [InlineData(StandardTestDevice.Fw5)] - public void SetPassword(StandardTestDevice testDeviceType) + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, false)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, false)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void SetPassword( + StandardTestDevice testDeviceType, bool useScp) { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice)) + var keyParameters = useScp ? Scp03KeyParameters.DefaultKey : null; + using (var resetSession = new OathSession(testDevice, keyParameters)) { - var collectorObj = new SimpleOathKeyCollector(); - oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + resetSession.ResetApplication(); + } - oathSession.SetPassword(); + using var oathSession = new OathSession(testDevice, keyParameters); + var collectorObj = new SimpleOathKeyCollector(); + oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; - Assert.False(oathSession._oathData.Challenge.IsEmpty); - } + oathSession.SetPassword(); + + Assert.False(oathSession._oathData.Challenge.IsEmpty); } - [Theory, TestPriority(1)] - [InlineData(StandardTestDevice.Fw5)] - public void VerifyCorrectPassword(StandardTestDevice testDeviceType) + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, false)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, false)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void VerifyCorrectPassword( + StandardTestDevice testDeviceType, bool useScp) { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice)) - { - var collectorObj = new SimpleOathKeyCollector(); - oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + SetTestPassword(testDevice); - bool isVerified = oathSession.TryVerifyPassword(); - Assert.True(isVerified); - } + using var oathSession = new OathSession(testDevice, useScp ? Scp03KeyParameters.DefaultKey : null); + var collectorObj = new SimpleOathKeyCollector(); + oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + + var isVerified = oathSession.TryVerifyPassword(); + + Assert.True(isVerified); } - [Theory, TestPriority(2)] - [InlineData(StandardTestDevice.Fw5)] - public void VerifyWrongPassword(StandardTestDevice testDeviceType) + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, false)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, false)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void VerifyWrongPassword( + StandardTestDevice testDeviceType, bool useScp) { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice)) - { - var collectorObj = new SimpleOathKeyCollector(); - oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + SetTestPassword(testDevice); - collectorObj.KeyFlag = 1; + using var oathSession = new OathSession(testDevice, useScp ? Scp03KeyParameters.DefaultKey : null); + var collectorObj = new SimpleOathKeyCollector(); + oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; - bool isVerified = oathSession.TryVerifyPassword(); - Assert.False(isVerified); - } + collectorObj.KeyFlag = 1; + + var isVerified = oathSession.TryVerifyPassword(); + Assert.False(isVerified); } - [Theory, TestPriority(3)] - [InlineData(StandardTestDevice.Fw5)] - public void ChangePassword(StandardTestDevice testDeviceType) + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, false)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, false)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void ChangePassword( + StandardTestDevice testDeviceType, bool useScp) { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice)) - { - var collectorObj = new SimpleOathKeyCollector(); - oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + SetTestPassword(testDevice); - collectorObj.KeyFlag = 1; - oathSession.SetPassword(); + using var oathSession = new OathSession(testDevice, useScp ? Scp03KeyParameters.DefaultKey : null); + var collectorObj = new SimpleOathKeyCollector(); + oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + collectorObj.KeyFlag = 1; - Assert.False(oathSession._oathData.Challenge.IsEmpty); - } + oathSession.SetPassword(); + + Assert.False(oathSession._oathData.Challenge.IsEmpty); } - [Theory, TestPriority(4)] - [InlineData(StandardTestDevice.Fw5)] - public void UnsetPassword(StandardTestDevice testDeviceType) + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, false)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, false)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void UnsetPassword( + StandardTestDevice testDeviceType, bool useScp) { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice)) - { - oathSession.ResetApplication(); + SetTestPassword(testDevice); + using var oathSession = new OathSession(testDevice, useScp ? Scp03KeyParameters.DefaultKey : null); + var collectorObj = new SimpleOathKeyCollector(); + oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; - var collectorObj = new SimpleOathKeyCollector(); - oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + oathSession.UnsetPassword(); - oathSession.SetPassword(); + Assert.False(oathSession.IsPasswordProtected); + } - Assert.True(oathSession.IsPasswordProtected); + private void SetTestPassword(IYubiKeyDevice testDevice, Scp03KeyParameters? keyParameters = null) + { + using (var resetSession = new OathSession(testDevice, keyParameters)) + { + resetSession.ResetApplication(); } - using (var oathSession = new OathSession(testDevice)) + using (var oathSession = new OathSession(testDevice, keyParameters)) { var collectorObj = new SimpleOathKeyCollector(); oathSession.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + oathSession.SetPassword(); - oathSession.UnsetPassword(); - - Assert.False(oathSession.IsPasswordProtected); + Assert.True(oathSession.IsPasswordProtected); } } } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs deleted file mode 100644 index 13db59558..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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. - -using System; -using Xunit; -using Yubico.Core.Devices.Hid; -using Yubico.YubiKey.Otp.Operations; -using Yubico.YubiKey.TestUtilities; - -namespace Yubico.YubiKey.Otp -{ - public class ConfigureStaticTests - { - - [Trait(TraitTypes.Category, TestCategories.Simple)] - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void ConfigureStaticPassword_Succeeds(StandardTestDevice testDeviceType) - { - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - - using (var otpSession = new OtpSession(testDevice)) - { - if (otpSession.IsLongPressConfigured) - { - otpSession.DeleteSlot(Slot.LongPress); - } - - ConfigureStaticPassword configObj = otpSession.ConfigureStaticPassword(Slot.LongPress); - Assert.NotNull(configObj); - - var generatedPassword = new Memory(new char[16]); - - configObj = configObj.WithKeyboard(KeyboardLayout.en_US); - configObj = configObj.AllowManualUpdate(false); - configObj = configObj.AppendCarriageReturn(false); - configObj = configObj.SendTabFirst(false); - configObj = configObj.SetAllowUpdate(); - configObj = configObj.GeneratePassword(generatedPassword); - configObj.Execute(); - } - } - } -} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/OtpSessionTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/OtpSessionTests.cs new file mode 100644 index 000000000..9e2859178 --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/OtpSessionTests.cs @@ -0,0 +1,83 @@ +// 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. + +using System; +using Xunit; +using Yubico.Core.Devices.Hid; +using Yubico.YubiKey.Otp.Operations; +using Yubico.YubiKey.Scp; +using Yubico.YubiKey.TestUtilities; + +namespace Yubico.YubiKey.Otp +{ + public class OtpSessionTests + { + + [Trait(TraitTypes.Category, TestCategories.Simple)] + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void ConfigureStaticPassword_Succeeds(StandardTestDevice testDeviceType) + { + IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + + using var otpSession = new OtpSession(testDevice); + if (otpSession.IsLongPressConfigured) + { + otpSession.DeleteSlot(Slot.LongPress); + } + + ConfigureStaticPassword configObj = otpSession.ConfigureStaticPassword(Slot.LongPress); + Assert.NotNull(configObj); + + var generatedPassword = new Memory(new char[16]); + + configObj = configObj.WithKeyboard(KeyboardLayout.en_US); + configObj = configObj.AllowManualUpdate(false); + configObj = configObj.AppendCarriageReturn(false); + configObj = configObj.SendTabFirst(false); + configObj = configObj.SetAllowUpdate(); + configObj = configObj.GeneratePassword(generatedPassword); + configObj.Execute(); + } + + [Trait(TraitTypes.Category, TestCategories.Simple)] + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void ConfigureStaticPassword_WithWScp_Succeeds(StandardTestDevice testDeviceType) + { + IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + + using var otpSession = new OtpSession(testDevice, Scp03KeyParameters.DefaultKey); + if (otpSession.IsLongPressConfigured) + { + otpSession.DeleteSlot(Slot.LongPress); + } + + ConfigureStaticPassword configObj = otpSession.ConfigureStaticPassword(Slot.LongPress); + Assert.NotNull(configObj); + + var generatedPassword = new Memory(new char[16]); + + configObj = configObj.WithKeyboard(KeyboardLayout.en_US); + configObj = configObj.AllowManualUpdate(false); + configObj = configObj.AppendCarriageReturn(false); + configObj = configObj.SendTabFirst(false); + configObj = configObj.SetAllowUpdate(); + configObj = configObj.GeneratePassword(generatedPassword); + configObj.Execute(); + } + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs index cf3c6e7b0..4d1bb46cf 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs @@ -15,6 +15,7 @@ using System; using Xunit; using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; @@ -37,13 +38,14 @@ public void SimpleGenerate(PivAlgorithm expectedAlgorithm, bool useScp03 = false var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(Transport.SmartCard, FirmwareVersion.V5_3_0); Assert.True(testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Piv)); + using var pivSession = useScp03 + ? new PivSession(testDevice, Scp03KeyParameters.DefaultKey) + : new PivSession(testDevice); - using var pivSession = useScp03 ? new PivSession(testDevice, new StaticKeys()) : new PivSession(testDevice); var collectorObj = new Simple39KeyCollector(); pivSession.KeyCollector = collectorObj.Simple39KeyCollectorDelegate; var result = pivSession.GenerateKeyPair(PivSlot.Retired12, expectedAlgorithm); - Assert.Equal(expectedAlgorithm, result.Algorithm); } @@ -58,12 +60,6 @@ public void GenerateAndSign(PivAlgorithm algorithm) Assert.True(testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Piv)); Assert.True(testDevice is YubiKeyDevice); - // if (testDevice is YubiKeyDevice device) - // { - // #pragma warning disable CS0618 // Specifically testing this feature - // // testDevice = device.WithScp03(new StaticKeys()); - // #pragma warning restore CS0618 // - // } var isValid = DoGenerate(testDevice, 0x86, algorithm, PivPinPolicy.Once, PivTouchPolicy.Never); Assert.True(isValid); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs index 9ff52ba1a..35d6b94ed 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs @@ -17,6 +17,7 @@ using Xunit; using Yubico.Core.Tlv; using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; @@ -29,7 +30,7 @@ public class GetPutDataTests public void Cert_Auth_Req(StandardTestDevice testDeviceType) { var isValid = SampleKeyPairs.GetMatchingKeyAndCert(PivAlgorithm.Rsa2048, - out X509Certificate2 cert, out PivPrivateKey privateKey); + out var cert, out var privateKey); Assert.True(isValid); var certDer = cert.GetRawCertData(); @@ -45,7 +46,7 @@ public void Cert_Auth_Req(StandardTestDevice testDeviceType) var certData = tlvWriter.Encode(); tlvWriter.Clear(); - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -61,13 +62,13 @@ public void Cert_Auth_Req(StandardTestDevice testDeviceType) { // There should be no data. var getDataCommand = new GetDataCommand((int)PivDataTag.Authentication); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now put some data. // This should fail because the mgmt key is needed. var putDataCommand = new PutDataCommand((int)PivDataTag.Authentication, certData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); // Verify the PIN @@ -87,7 +88,7 @@ public void Cert_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.Authentication, certData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -95,10 +96,10 @@ public void Cert_Auth_Req(StandardTestDevice testDeviceType) { // There should be data this time. var getDataCommand = new GetDataCommand((int)PivDataTag.Authentication); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(certData.Length, getData.Length); } } @@ -114,21 +115,24 @@ public void Chuid_Auth_Req(StandardTestDevice testDeviceType) 0x08, 0x32, 0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var pivSession = new PivSession(testDevice, new StaticKeys())) + using (var pivSession = new PivSession(testDevice)) { pivSession.ResetApplication(); + } + using (var pivSession = new PivSession(testDevice, Scp03KeyParameters.DefaultKey)) + { // There should be no data. var getDataCommand = new GetDataCommand((int)PivDataTag.Chuid); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now put some data. // This should fail because the mgmt key is needed. var putDataCommand = new PutDataCommand((int)PivDataTag.Chuid, chuidData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); // Verify the PIN @@ -148,7 +152,7 @@ public void Chuid_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.Chuid, chuidData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -157,10 +161,10 @@ public void Chuid_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.Chuid); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(61, getData.Length); } } @@ -176,7 +180,7 @@ public void Capability_Auth_Req(StandardTestDevice testDeviceType) 0x00, 0xFD, 0x00, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -186,13 +190,13 @@ public void Capability_Auth_Req(StandardTestDevice testDeviceType) #pragma warning disable CS0618 // Testing an obsolete feature var getDataCommand = new GetDataCommand(PivDataTag.Capability); #pragma warning restore CS0618 // Type or member is obsolete - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now put some data. // This should fail because the mgmt key is needed. var putDataCommand = new PutDataCommand((int)PivDataTag.Capability, capabilityData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); // Verify the PIN @@ -214,7 +218,7 @@ public void Capability_Auth_Req(StandardTestDevice testDeviceType) #pragma warning disable CS0618 // Testing an obsolete feature var putDataCommand = new PutDataCommand(PivDataTag.Capability, capabilityData); #pragma warning restore CS0618 // Type or member is obsolete - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -225,10 +229,10 @@ public void Capability_Auth_Req(StandardTestDevice testDeviceType) #pragma warning disable CS0618 // Testing an obsolete feature var getDataCommand = new GetDataCommand(PivDataTag.Capability); #pragma warning restore CS0618 // Type or member is obsolete - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(53, getData.Length); } } @@ -242,7 +246,7 @@ public void Discovery_Auth_Req(StandardTestDevice testDeviceType) 0x2F, 0x02, 0x40, 0x00, }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -250,10 +254,10 @@ public void Discovery_Auth_Req(StandardTestDevice testDeviceType) // There should be data. var getDataCommand = new GetDataCommand((int)PivDataTag.Discovery); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(20, getData.Length); // Now put some data. @@ -271,21 +275,8 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) byte[] printedData = { 0x53, 0x04, 0x04, 0x02, 0xd4, 0xe7 }; - byte[] key1 = { - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff - }; - byte[] key2 = { - 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11 - }; - byte[] key3 = { - 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22 - }; - var newKeys = new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 2 - }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -293,7 +284,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) // There should be no data, but even so, the error should be Auth Required. var getDataCommand = new GetDataCommand((int)PivDataTag.Printed); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); // Authenticate the mgmt key and try again. It should still @@ -304,8 +295,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); } - - using (var pivSession = new PivSession(testDevice, newKeys)) + using (var pivSession = new PivSession(testDevice)) { // Verify the PIN pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -314,7 +304,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) // Get the data. This time we should be able to see that there's // NoData. var getDataCommand = new GetDataCommand((int)PivDataTag.Printed); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); } @@ -325,7 +315,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) // With no PIN nor mgmt key, or with only the PIN, this should // fail. var putDataCommand = new PutDataCommand(0x5FC109, printedData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -342,7 +332,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand(0x5FC109, printedData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -351,7 +341,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.Printed); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); pivSession.KeyCollector = MgmtKeyOnlyKeyCollectorDelegate; @@ -369,10 +359,10 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) pivSession.VerifyPin(); var getDataCommand = new GetDataCommand((int)PivDataTag.Printed); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(6, getData.Length); } } @@ -386,7 +376,7 @@ public void Security_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x08, 0xBA, 0x01, 0x11, 0xBB, 0x01, 0x22, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -394,13 +384,13 @@ public void Security_Auth_Req(StandardTestDevice testDeviceType) // There should be no data. var getDataCommand = new GetDataCommand((int)PivDataTag.SecurityObject); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now put some data. // This should fail because the mgmt key is needed. var putDataCommand = new PutDataCommand((int)PivDataTag.SecurityObject, securityData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); // Verify the PIN @@ -420,7 +410,7 @@ public void Security_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.SecurityObject, securityData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -429,10 +419,10 @@ public void Security_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.SecurityObject); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(10, getData.Length); } } @@ -445,7 +435,7 @@ public void KeyHistory_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x0A, 0xC1, 0x01, 0x00, 0xC2, 0x01, 0x00, 0xF3, 0x00, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -453,13 +443,13 @@ public void KeyHistory_Auth_Req(StandardTestDevice testDeviceType) // There should be no data. var getDataCommand = new GetDataCommand((int)PivDataTag.KeyHistory); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now put some data. // This should fail because the mgmt key is needed. var putDataCommand = new PutDataCommand((int)PivDataTag.KeyHistory, keyHistoryData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); // Verify the PIN @@ -479,7 +469,7 @@ public void KeyHistory_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.KeyHistory, keyHistoryData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -488,10 +478,10 @@ public void KeyHistory_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.KeyHistory); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(12, getData.Length); } } @@ -505,7 +495,7 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x05, 0xBC, 0x01, 0x11, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -513,7 +503,7 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) // There should be no data, but even so, the error should be Auth Required. var getDataCommand = new GetDataCommand((int)PivDataTag.IrisImages); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); // Authenticate the mgmt key and try to get data again. It should still @@ -534,7 +524,7 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) // Get the data. This time we should be able to see that there's // NoData. var getDataCommand = new GetDataCommand((int)PivDataTag.IrisImages); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); } @@ -544,7 +534,7 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) // With no PIN nor mgmt key, or with only the PIN, this should // fail. var putDataCommand = new PutDataCommand((int)PivDataTag.IrisImages, irisData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -561,7 +551,7 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.IrisImages, irisData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -570,7 +560,7 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.IrisImages); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); pivSession.KeyCollector = MgmtKeyOnlyKeyCollectorDelegate; @@ -588,10 +578,10 @@ public void Iris_Auth_Req(StandardTestDevice testDeviceType) pivSession.VerifyPin(); var getDataCommand = new GetDataCommand((int)PivDataTag.IrisImages); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(7, getData.Length); } } @@ -605,7 +595,7 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x05, 0xBC, 0x01, 0x11, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -613,7 +603,7 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) // There should be no data, but even so, the error should be Auth Required. var getDataCommand = new GetDataCommand((int)PivDataTag.FacialImage); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); // Authenticate the mgmt key and try to get data again. It should still @@ -634,7 +624,7 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) // Get the data. This time we should be able to see that there's // NoData. var getDataCommand = new GetDataCommand((int)PivDataTag.FacialImage); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); } @@ -644,7 +634,7 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) // With no PIN nor mgmt key, or with only the PIN, this should // fail. var putDataCommand = new PutDataCommand((int)PivDataTag.FacialImage, facialData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -661,7 +651,7 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.FacialImage, facialData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -670,7 +660,7 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.FacialImage); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); pivSession.KeyCollector = MgmtKeyOnlyKeyCollectorDelegate; @@ -688,10 +678,10 @@ public void Facial_Auth_Req(StandardTestDevice testDeviceType) pivSession.VerifyPin(); var getDataCommand = new GetDataCommand((int)PivDataTag.FacialImage); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(7, getData.Length); } } @@ -705,7 +695,7 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x05, 0xBC, 0x01, 0x11, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -713,7 +703,7 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) // There should be no data, but even so, the error should be Auth Required. var getDataCommand = new GetDataCommand((int)PivDataTag.Fingerprints); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); // Authenticate the mgmt key and try to get data again. It should still @@ -734,7 +724,7 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) // Get the data. This time we should be able to see that there's // NoData. var getDataCommand = new GetDataCommand((int)PivDataTag.Fingerprints); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); } @@ -744,7 +734,7 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) // With no PIN nor mgmt key, or with only the PIN, this should // fail. var putDataCommand = new PutDataCommand((int)PivDataTag.Fingerprints, fingerprintData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -761,7 +751,7 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.Fingerprints, fingerprintData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -770,7 +760,7 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.Fingerprints); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); pivSession.KeyCollector = MgmtKeyOnlyKeyCollectorDelegate; @@ -788,10 +778,10 @@ public void Fingerprint_Auth_Req(StandardTestDevice testDeviceType) pivSession.VerifyPin(); var getDataCommand = new GetDataCommand((int)PivDataTag.Fingerprints); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(7, getData.Length); } } @@ -805,7 +795,7 @@ public void Bitgt_Auth_Req(StandardTestDevice testDeviceType) 0x7F, 0x61, 0x07, 0x02, 0x01, 0x01, 0x7F, 0x60, 0x01, 0x01 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -815,7 +805,7 @@ public void Bitgt_Auth_Req(StandardTestDevice testDeviceType) #pragma warning disable CS0618 // Testing an obsolete feature var getDataCommand = new GetDataCommand(PivDataTag.BiometricGroupTemplate); #pragma warning restore CS0618 // Type or member is obsolete - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now try to put some data. @@ -837,7 +827,7 @@ public void SMSigner_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x08, 0x70, 0x01, 0x11, 0x71, 0x01, 0x00, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { @@ -846,7 +836,7 @@ public void SMSigner_Auth_Req(StandardTestDevice testDeviceType) // There is no auth required to get data, but there should be no // data at the moment. var getDataCommand = new GetDataCommand((int)PivDataTag.SecureMessageSigner); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); } @@ -856,7 +846,7 @@ public void SMSigner_Auth_Req(StandardTestDevice testDeviceType) // With no PIN nor mgmt key, or with only the PIN, this should // fail. var putDataCommand = new PutDataCommand((int)PivDataTag.SecureMessageSigner, smSignerData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -873,7 +863,7 @@ public void SMSigner_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.SecureMessageSigner, smSignerData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -882,10 +872,10 @@ public void SMSigner_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.SecureMessageSigner); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(10, getData.Length); } } @@ -899,7 +889,7 @@ public void PCRef_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x0C, 0x99, 0x08, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0xFE, 0x00 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { pivSession.ResetApplication(); @@ -907,7 +897,7 @@ public void PCRef_Auth_Req(StandardTestDevice testDeviceType) // There is no auth required to get data, but there should be no // data at the moment. var getDataCommand = new GetDataCommand((int)PivDataTag.PairingCodeReferenceData); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); } @@ -917,7 +907,7 @@ public void PCRef_Auth_Req(StandardTestDevice testDeviceType) // With no PIN nor mgmt key, or with only the PIN, this should // fail. var putDataCommand = new PutDataCommand((int)PivDataTag.PairingCodeReferenceData, pcRefData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -934,7 +924,7 @@ public void PCRef_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand((int)PivDataTag.PairingCodeReferenceData, pcRefData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -943,10 +933,10 @@ public void PCRef_Auth_Req(StandardTestDevice testDeviceType) using (var pivSession = new PivSession(testDevice)) { var getDataCommand = new GetDataCommand((int)PivDataTag.PairingCodeReferenceData); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(14, getData.Length); } } @@ -959,19 +949,19 @@ public void AdminData_Auth_Req(StandardTestDevice testDeviceType) 0x53, 0x09, 0x80, 0x07, 0x81, 0x01, 0x00, 0x03, 0x02, 0x5C, 0x29 }; - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); using (var pivSession = new PivSession(testDevice)) { // There should be no data. var getDataCommand = new GetDataCommand(0x5FFF00); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.NoData, getDataResponse.Status); // Now put some data. // This should fail because the mgmt key is needed. var putDataCommand = new PutDataCommand(0x5FFF00, adminData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, putDataResponse.Status); // Verify the PIN @@ -991,7 +981,7 @@ public void AdminData_Auth_Req(StandardTestDevice testDeviceType) pivSession.AuthenticateManagementKey(); var putDataCommand = new PutDataCommand(0x5FFF00, adminData); - PutDataResponse putDataResponse = pivSession.Connection.SendCommand(putDataCommand); + var putDataResponse = pivSession.Connection.SendCommand(putDataCommand); Assert.Equal(ResponseStatus.Success, putDataResponse.Status); } @@ -999,10 +989,10 @@ public void AdminData_Auth_Req(StandardTestDevice testDeviceType) { // There should be data this time. var getDataCommand = new GetDataCommand(0x5FFF00); - GetDataResponse getDataResponse = pivSession.Connection.SendCommand(getDataCommand); + var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.Success, getDataResponse.Status); - ReadOnlyMemory getData = getDataResponse.GetData(); + var getData = getDataResponse.GetData(); Assert.Equal(11, getData.Length); } } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs index cded8cb9f..08fa81cc4 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs @@ -18,6 +18,7 @@ using Yubico.Core.Tlv; using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; @@ -47,9 +48,8 @@ public void Sign_EccP256_Succeeds(bool useScp03, StandardTestDevice device, PivP bool isValid = LoadKey(PivAlgorithm.EccP256, 0x89, pinPolicy, PivTouchPolicy.Never, testDevice); Assert.True(isValid); Assert.True(testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Piv)); - using var pivSession = useScp03 - ? new PivSession(testDevice, new StaticKeys()) + ? new PivSession(testDevice, Scp03KeyParameters.DefaultKey) : new PivSession(testDevice); var collectorObj = new Simple39KeyCollector(); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs new file mode 100644 index 000000000..4887c8883 --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -0,0 +1,568 @@ +// Copyright 2023 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.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using Xunit; +using Yubico.Core.Tlv; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Piv; +using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp03; +using Yubico.YubiKey.TestUtilities; +using GetDataCommand = Yubico.YubiKey.Scp.Commands.GetDataCommand; + +namespace Yubico.YubiKey.Scp +{ + [Trait(TraitTypes.Category, TestCategories.Simple)] + public class Scp03Tests + { + private readonly ReadOnlyMemory _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; + + public Scp03Tests() + { + ResetSecurityDomainOnAllowedDevices(); + } + + private IYubiKeyDevice GetDevice( + StandardTestDevice desiredDeviceType, + Transport transport = Transport.All, + FirmwareVersion? minimumFirmwareVersion = null) + { + var testDevice = + IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, minimumFirmwareVersion); + + // Since we are in testing, we assume that the keys the default keys are present on the device and haven't been changed + // Therefore we will throw an exception if the device is FIPS and the transport is NFC + // This can be changed in the future if needed + Assert.False( + desiredDeviceType == StandardTestDevice.Fw5Fips && + transport == Transport.NfcSmartCard && + testDevice.IsFipsSeries, + "SCP03 with the default static keys is not allowed over NFC on FIPS capable devices"); + + return testDevice; + } + + private static void ResetSecurityDomainOnAllowedDevices() + { + foreach (var availableDevice in IntegrationTestDeviceEnumeration.GetTestDevices()) + { + using var session = new SecurityDomainSession(availableDevice); + session.Reset(); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_PutKey_with_StaticKey_Imports_Key( + StandardTestDevice desiredDeviceType, + Transport transport) + { + byte[] sk = + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + }; + + var testDevice = GetDevice(desiredDeviceType, transport); + var newKeyParams = Scp03KeyParameters.FromStaticKeys(new StaticKeys(sk, sk, sk)); + + // Authenticate with default key, then replace default key + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); + } + + using (_ = new SecurityDomainSession(testDevice, newKeyParams)) + { + } + + // Default key should not work now and throw an exception + Assert.Throws(() => + { + using (_ = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + } + }); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + [Obsolete("Use Scp03_PutKey_with_StaticKey_Imports_Key instead")] + public void Obsolete_Scp03_PutKey_with_StaticKey_Imports_Key( + StandardTestDevice desiredDeviceType, + Transport transport) + { + byte[] sk = + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + }; + + var testDevice = GetDevice(desiredDeviceType, transport); + var defaultStaticKeys = new Scp03.StaticKeys(); + var newStaticKeys = new Scp03.StaticKeys(sk, sk, sk); + + // Authenticate with default key, then replace default key + using (var session = new Scp03Session(testDevice, defaultStaticKeys)) + { + session.PutKeySet(newStaticKeys); + } + + using (_ = new Scp03Session(testDevice, newStaticKeys)) + { + } + + // Default key should not work now and throw an exception + Assert.Throws(() => + { + using (_ = new Scp03Session(testDevice, defaultStaticKeys)) + { + } + }); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_PutKey_with_PublicKey_Imports_Key( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var keyReference = new KeyReference(ScpKeyIds.ScpCaPublicKey, 0x3); + var testDevice = GetDevice(desiredDeviceType, transport, FirmwareVersion.V5_7_2); + + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + var publicKey = new ECPublicKeyParameters(ecdsa); + session.PutKey(keyReference, publicKey, 0); + + // Verify the generated key was stored + var keyInformation = session.GetKeyInformation(); + Assert.True(keyInformation.ContainsKey(keyReference)); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_AuthenticateWithWrongKey_Should_ThrowException( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + var incorrectKeys = RandomStaticKeys(); + var keyRef = Scp03KeyParameters.FromStaticKeys(incorrectKeys); + + // Authentication with incorrect key should throw + Assert.Throws(() => + { + using (var session = new SecurityDomainSession(testDevice, keyRef)) { } + }); + + // Authentication with default key should succeed + using (_ = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_GetInformation_WithDefaultKey_Returns_DefaultKey( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + using var session = new SecurityDomainSession(testDevice); + + var result = session.GetKeyInformation(); + if (testDevice.FirmwareVersion < FirmwareVersion.V5_7_2) + { + Assert.Equal(3, result.Count); + } + else + { + Assert.Equal(4, result.Count); + } + + Assert.Equal(0xFF, result.Keys.First().VersionNumber); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_Connect_GetKeyInformation_WithDefaultKey_Returns_DefaultKey( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + using var connection = testDevice.Connect(YubiKeyApplication.SecurityDomain); + + const byte TAG_KEY_INFORMATION = 0xE0; + var response = connection.SendCommand(new GetDataCommand(TAG_KEY_INFORMATION)); + var result = response.GetData(); + + var keyInformation = new Dictionary>(); + foreach (var tlvObject in TlvObjects.DecodeList(result.Span)) + { + var value = TlvObjects.UnpackValue(0xC0, tlvObject.GetBytes().Span); + var keyRef = new KeyReference(value.Span[0], value.Span[1]); + var keyComponents = new Dictionary(); + + // Iterate while there are more key components, each component is 2 bytes, so take 2 bytes at a time + while (!(value = value[2..]).IsEmpty) + { + keyComponents.Add(value.Span[0], value.Span[1]); + } + + keyInformation.Add(keyRef, keyComponents); + } + + Assert.NotEmpty(keyInformation); + Assert.Equal(0xFF, keyInformation.Keys.First().VersionNumber); // 0xff, Default kvn + + if (testDevice.FirmwareVersion < FirmwareVersion.V5_7_2) + { + Assert.Equal(3, keyInformation.Keys.Count); + } + else + { + Assert.Equal(4, keyInformation.Keys.Count); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_GetCertificates_ReturnsCerts( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport, FirmwareVersion.V5_7_2); + + using var session = new SecurityDomainSession(testDevice); + + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var certificateList = session.GetCertificates(keyReference); + + Assert.NotEmpty(certificateList); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_Reset_Restores_SecurityDomainKeys_To_FactoryKeys( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + byte[] sk = + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + }; + + var newKeyParams = new Scp03KeyParameters( + ScpKeyIds.Scp03, + 0x01, + new StaticKeys(sk, sk, sk)); + + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); + } + + // Authentication with new key should succeed + using (var session = new SecurityDomainSession(testDevice, newKeyParams)) + { + session.GetKeyInformation(); + } + + // Default key should not work now and throw an exception + Assert.Throws(() => + { + using (_ = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + } + }); + + using (var session = new SecurityDomainSession(testDevice)) + { + session.Reset(); + } + + // Successful authentication with default key means key has been restored to factory settings + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + _ = session.GetKeyInformation(); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_GetSupportedCaIdentifiers_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport, FirmwareVersion.V5_7_2); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); + + var result = session.GetSupportedCaIdentifiers(true, true); + Assert.NotEmpty(result); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_GetCardRecognitionData_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); + var result = session.GetCardRecognitionData(); + + Assert.True(result.Length > 0); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_GetData_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); + var result = session.GetData(0x66); // Card Data + + Assert.True(result.Length > 0); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + var testDevice = GetDevice(desiredDeviceType, transport); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); + Assert.True(testDevice.HasFeature(YubiKeyFeature.Scp03)); + + using var pivSession = new PivSession(testDevice, Scp03KeyParameters.DefaultKey); + + pivSession.ResetApplication(); + + if (desiredDeviceType == StandardTestDevice.Fw5Fips) + { + ScpTestUtilities.SetFipsApprovedCredentials(pivSession); + + var isVerified = pivSession.TryVerifyPin(ScpTestUtilities.FipsPin, out _); + Assert.True(isVerified); + } + else + { + var isVerified = pivSession.TryVerifyPin(_defaultPin, out _); + Assert.True(isVerified); + } + + var metadata = pivSession.GetMetadata(PivSlot.Pin)!; + Assert.Equal(3, metadata.RetryCount); + } + + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_Device_Connect_ApplicationId_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + // Arrange + var testDevice = GetDevice(desiredDeviceType, transport); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); + var keyParams = Scp03KeyParameters.DefaultKey; + var pivAppId = new byte[] { 0xA0, 0x00, 0x00, 0x03, 0x08 }; + var pin = GetValidPin(desiredDeviceType, testDevice, keyParams); + + // Act + using var connection = testDevice.Connect( + pivAppId, Scp03KeyParameters.DefaultKey); + + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(pin); + var rsp = connection.SendCommand(cmd); + + // Assert + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_Device_Connect_With_Application_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + // Arrange + var testDevice = GetDevice(desiredDeviceType, transport); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); + + var keyParams = Scp03KeyParameters.DefaultKey; + var pin = GetValidPin(desiredDeviceType, testDevice, keyParams); + + // Act + using var connection = testDevice.Connect( + YubiKeyApplication.Piv, + keyParams); + + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(pin); + var rsp = connection.SendCommand(cmd); + + // Assert + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_Device_TryConnect_With_ApplicationId_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + // Arrange + var testDevice = GetDevice(desiredDeviceType, transport); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); + + var keyParams = Scp03KeyParameters.DefaultKey; + var pivAppId = new byte[] { 0xA0, 0x00, 0x00, 0x03, 0x08 }; + var pin = GetValidPin(desiredDeviceType, testDevice, keyParams); + + // Act + var isValid = testDevice.TryConnect( + pivAppId, + keyParams, + out var connection); + + Assert.True(isValid); + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(pin); + var rsp = connection.SendCommand(cmd); + + // Assert + Assert.Equal(ResponseStatus.Success, rsp.Status); + + connection.Dispose(); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.UsbSmartCard)] + [InlineData(StandardTestDevice.Fw5Fips, Transport.NfcSmartCard)] + public void Scp03_Device_TryConnect_With_Application_Succeeds( + StandardTestDevice desiredDeviceType, + Transport transport) + { + // Arrange + var testDevice = GetDevice(desiredDeviceType, transport); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); + + var keyParams = Scp03KeyParameters.DefaultKey; + var pin = GetValidPin(desiredDeviceType, testDevice, keyParams); + + // Act + var isValid = testDevice.TryConnect( + YubiKeyApplication.Piv, + keyParams, + out var connection); + + Assert.True(isValid); + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(pin); + var rsp = connection.SendCommand(cmd); + + // Assert + Assert.Equal(ResponseStatus.Success, rsp.Status); + + connection.Dispose(); + } + + private byte[] GetValidPin( + StandardTestDevice desiredDeviceType, + IYubiKeyDevice testDevice, + Scp03KeyParameters keyParams) + { + byte[] pin; + if (desiredDeviceType == StandardTestDevice.Fw5Fips) + { + ScpTestUtilities.SetFipsApprovedCredentials(testDevice, YubiKeyApplication.Piv, keyParams); + pin = ScpTestUtilities.FipsPin; + } + else + { + pin = _defaultPin.ToArray(); + } + + return pin; + } + + #region Helpers + + private static StaticKeys RandomStaticKeys() => + new StaticKeys( + GetRandom16Bytes(), + GetRandom16Bytes(), + GetRandom16Bytes() + ); + + private static ReadOnlyMemory GetRandom16Bytes() + { + var buffer = new byte[16]; + Random.Shared.NextBytes(buffer); + return buffer; + } + + #endregion + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11TestData.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11TestData.cs new file mode 100644 index 000000000..e54d026ec --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11TestData.cs @@ -0,0 +1,79 @@ +using System; +using System.Text; + +namespace Yubico.YubiKey.Scp +{ + public static class Scp11TestData + { + public readonly static ReadOnlyMemory OceCerts = Encoding.UTF8.GetBytes( + "-----BEGIN CERTIFICATE-----\n" + + "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + + "T0NFIEludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49\n" + + "AwEHA0IABMXbjb+Y33+GP8qUznrdZSJX9b2qC0VUS1WDhuTlQUfg/RBNFXb2/qWt\n" + + "h/a+Ag406fV7wZW2e4PPH+Le7EwS1nyjgZUwgZIwHQYDVR0OBBYEFJzdQCINVBES\n" + + "R4yZBN2l5CXyzlWsMB8GA1UdIwQYMBaAFDGqVWafYGfoHzPc/QT+3nPlcZ89MBIG\n" + + "A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMCwGA1UdIAEB/wQiMCAw\n" + + "DgYMKoZIhvxrZAAKAgEoMA4GDCqGSIb8a2QACgIBADAKBggqhkjOPQQDAgNHADBE\n" + + "AiBE5SpNEKDW3OehDhvTKT9g1cuuIyPdaXGLZ3iX0x0VcwIgdnIirhlKocOKGXf9\n" + + "ijkE8e+9dTazSPLf24lSIf0IGC8=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIB2zCCAYGgAwIBAgIUSf59wIpCKOrNGNc5FMPTD9zDGVAwCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA2MjcwOTIyMDlaMCoxKDAmBgNVBAMMH0V4YW1wbGUg\n" + + "T0NFIFJvb3QgQ0EgQ2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + + "AASPrxfpSB/AvuvLKaCz1YTx68Xbtx8S9xAMfRGwzp5cXMdF8c7AWpUfeM3BQ26M\n" + + "h0WPvyBJKhCdeK8iVCaHyr5Jo4GEMIGBMB0GA1UdDgQWBBQxqlVmn2Bn6B8z3P0E\n" + + "/t5z5XGfPTASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjA8BgNV\n" + + "HSABAf8EMjAwMA4GDCqGSIb8a2QACgIBFDAOBgwqhkiG/GtkAAoCASgwDgYMKoZI\n" + + "hvxrZAAKAgEAMAoGCCqGSM49BAMCA0gAMEUCIHv8cgOzxq2n1uZktL9gCXSR85mk\n" + + "TieYeSoKZn6MM4rOAiEA1S/+7ez/gxDl01ztKeoHiUiW4FbEG4JUCzIITaGxVvM=\n" + + "-----END CERTIFICATE-----").AsMemory(); + + // PKCS12 certificate with a private key and full certificate chain + public static readonly Memory Oce = Convert.FromBase64String( + "MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCMEgggfMIIIGzCCBtIGCSqGSIb3DQEHBqCCBsMwgga/" + + "AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAg8IcJO44iS" + + "gAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIHdoQx/USA3jmRMeciiAggZQAHCP" + + "J5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc+0EcbKQHig1Jx7rqC3q4G4sboIRw1vDH6q5O8eG" + + "sbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3s0Yx5yMm/xzw" + + "204TEK5/1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEYAn0F3" + + "LoMETQytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz/+FYdF43cjmwGfSb3OpaxOND4" + + "PBCpwzbFfVCLa6mUBlwq1KQWRm1+PFm4LnL+3s2mxfjJAsVYP4U722/FHpW8rdTsyvdift9lsQja" + + "s2jIjCu8PFClFZJLQldu5FxOhKzx2gsjYS/aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD/sy3" + + "Vj0i5sbWwTx7iq67joWydWAMp/lGSZ6akWRsyku/282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5" + + "NKfulnJ1gH+i3e3RT3TauAKlqCeAfvDvA3+jxEDy/puPncod7WH0m9P4OmXjZ0s5EI4U+v6bKPgL" + + "7LlTCEI6yj15P7kxmruoxZlDAmhixVmlwJ8ZbVxD6Q+AOhXYPg+il3AYaRAS+VyJla0K+ac6hpYV" + + "AnbZCPzgHVkKC6iq4a/azf2b4uq9ks109jjnryAChdBsGdmStpZaPW4koMSAIJf12vGRp5jNjSax" + + "aIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpewiLL7C22lerUT7pYvKLCq/nnPYtb5UrST" + + "HrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5/UaU/yKq1RonMRaPhOZEESZEwLKVCqyDVEbAt7Hd" + + "ahp+Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u+P2kVWjxrGBuRrlgEkKuHcohWoO9EMX/bLK9KcY4s1" + + "ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO55LN3rNpcD9+" + + "fZt6ldoZCpg+t6y5xqHy+7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4/ycmFUEyoGv8Ib/ieUB" + + "bebPz0Uhn+jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB/FOJh1s4KI6kQgzCSObrIVXBcLC" + + "TXPfZ3jWxspKIREHn+zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u" + + "7W5HREX4CwmKu+12R6iYQ/po9Hcy6NJ8ShLdAzU0+q/BzgH7Cb8qimjgfGBA3Mesc+P98FlCzAjB" + + "2EgucRuXuehM/FemmZyNl0qI1Mj9qOgx/HeYaJaYD+yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7" + + "pd7OybKdSjDq25CCTOZvfR0DD55FDIGCy0FsJTcferzPFlkz/Q45vEwuGfEBnXXS9IhH4ySvJmDm" + + "yfLMGiHW6t+9gjyEEg+dwSOq9yXYScfCsefRl7+o/9nDoNQ8s/XS7LKlJ72ZEBaKeAxcm6q4wVwU" + + "WITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4pfQqMWcPpqVp4FuIsEpDWZYuv71s+WMYCs1JMfH" + + "bHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6cVrgt3EJWLey5sXY01WpMm526fwtLolS" + + "MpCf+dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl+lCJDYxBFFtnd6hq4OcVr5HiNAbLnS" + + "jBWbzqxhHMmgoojy4rwtHmrfyVYKXyl+98r+Lobitv2tpnBqmjL6dMPRBOJvQl8+Wp4MGBsi1gvT" + + "gW/+pLlMXT++1iYyxBeK9/AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYrDg7DAg/+Qc" + + "Oi+2mgo9zJPzR2jIXF0wP+9FA4+MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3uGZ" + + "beJEpU1hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm+UNYz+hB9vCb8+3OHA06" + + "9M0CAlJVOTF9uEpLVRzK+1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0B" + + "DAoBAqCB7zCB7DBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwG" + + "CCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAkK96h6gHJglyJl1/yEylvBIGQh62z7u5RoQ9y5wIX" + + "bE3/oMQTKVfCSrtqGUmj38sxDY7yIoTVQq7sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0/H" + + "Z2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzTl5MLFAwn3NE49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6" + + "JAo/y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvcNAQkVMRYEFJBU0s1/6SLbIRbyeq65gLWqClWN" + + "MEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k23PH+qUXUGPEuYkrGy+DzEQiikECB0B" + + "XjHOZZhuAgIIAA=="); + + public static readonly ReadOnlyMemory OcePassword = "password".AsMemory(); + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs new file mode 100644 index 000000000..742e13eeb --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -0,0 +1,650 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Xunit; +using Yubico.Core.Devices.Hid; +using Yubico.Core.Tlv; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Oath; +using Yubico.YubiKey.Otp; +using Yubico.YubiKey.Piv; +using Yubico.YubiKey.TestUtilities; +using Yubico.YubiKey.YubiHsmAuth; +using ECCurve = System.Security.Cryptography.ECCurve; +using ECPoint = System.Security.Cryptography.ECPoint; + + +namespace Yubico.YubiKey.Scp +{ + [Trait(TraitTypes.Category, TestCategories.Simple)] + public class Scp11Tests + { + private const byte OceKid = 0x010; + + public Scp11Tests() + { + ResetSecurityDomainOnAllowedDevices(); + } + + private IYubiKeyDevice GetDevice( + StandardTestDevice desiredDeviceType, + Transport transport = Transport.SmartCard, + FirmwareVersion? minimumFirmwareVersion = null) => + IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, + minimumFirmwareVersion ?? FirmwareVersion.V5_7_2); + + private static void ResetSecurityDomainOnAllowedDevices() + { + foreach (var availableDevice in IntegrationTestDeviceEnumeration.GetTestDevices()) + { + using var session = new SecurityDomainSession(availableDevice); + session.Reset(); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_App_PivSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_SecureConnection_Parameters(testDevice, keyReference); + + using var session = new PivSession(testDevice, keyParams); + + session.KeyCollector = new Simple39KeyCollector().Simple39KeyCollectorDelegate; + if (desiredDeviceType == StandardTestDevice.Fw5Fips) + { + ScpTestUtilities.SetFipsApprovedCredentials(session); + } + + var result = session.GenerateKeyPair(PivSlot.Retired12, PivAlgorithm.EccP256, PivPinPolicy.Always); + Assert.Equal(PivAlgorithm.EccP256, result.Algorithm); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_App_OathSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_SecureConnection_Parameters(testDevice, keyReference); + + using (var resetSession = new OathSession(testDevice, keyParams)) + { + resetSession.ResetApplication(); + } + + using var session = new OathSession(testDevice, keyParams); + session.KeyCollector = new SimpleOathKeyCollector().SimpleKeyCollectorDelegate; + + session.SetPassword(); + Assert.True(session.IsPasswordProtected); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_App_OtpSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_SecureConnection_Parameters(testDevice, keyReference); + + using var session = new OtpSession(testDevice, keyParams); + if (session.IsLongPressConfigured) + { + session.DeleteSlot(Slot.LongPress); + } + + var configObj = session.ConfigureStaticPassword(Slot.LongPress); + var generatedPassword = new Memory(new char[16]); + configObj = configObj.WithKeyboard(KeyboardLayout.en_US); + configObj = configObj.GeneratePassword(generatedPassword); + + configObj.Execute(); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_App_YubiHsmSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = YhaTestUtilities.GetCleanDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_SecureConnection_Parameters(testDevice, keyReference); + + using var session = new YubiHsmAuthSession(testDevice, keyParams); + + byte[] managementKey; + if (desiredDeviceType == StandardTestDevice.Fw5Fips) // Must change from default management key for FIPS + { + session.ChangeManagementKey(YhaTestUtilities.DefaultMgmtKey, YhaTestUtilities.AlternateMgmtKey); + managementKey = YhaTestUtilities.AlternateMgmtKey; + } + else + { + managementKey = YhaTestUtilities.DefaultMgmtKey; + } + + session.AddCredential(managementKey, YhaTestUtilities.DefaultAes128Cred); + + var result = session.ListCredentials(); + Assert.Single(result); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_EstablishSecureConnection_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + + IReadOnlyCollection certificateList; + using (var session = new SecurityDomainSession(testDevice)) + { + certificateList = session.GetCertificates(keyReference); + } + + var leaf = certificateList.Last(); + // Remember to verify the cert chain + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!; + var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); + + using (var session = new SecurityDomainSession(testDevice, keyParams)) + { + _ = session.GetKeyInformation(); + Assert.Throws(() => VerifyScp11bAuth(session)); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_Import_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x2); + + // Start authenticated session with default key + Scp11KeyParameters keyParameters; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + // Generate a new EC key on the host and import via PutKey + var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var privateKey = new ECPrivateKeyParameters(ecdsa); + session.PutKey(keyReference, privateKey, 0); + + keyParameters = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecdsa)); + } + + using (var _ = new SecurityDomainSession(testDevice, keyParameters)) + { + // Secure channel established + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_GetCertificates_IsNotEmpty( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + using var session = new SecurityDomainSession(testDevice); + + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var certificateList = session.GetCertificates(keyReference); + + Assert.NotEmpty(certificateList); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_StoreCertificates_CanBeRetrieved( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); + var oceCertificates = GetOceCertificates(Scp11TestData.OceCerts.Span); + + session.StoreCertificates(keyReference, oceCertificates.Bundle); + var result = session.GetCertificates(keyReference); + + // Assert that we can store and retrieve the off card entity certificate + var oceThumbprint = oceCertificates.Bundle.Single().Thumbprint; + Assert.Single(result); + Assert.Equal(oceThumbprint, result[0].Thumbprint); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_GenerateEcKey_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x3); + + // Start authenticated session + Scp11KeyParameters keyParameters; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + // Generate a new EC key on the Yubikey + var publicKey = session.GenerateEcKey(keyReference, 0); + + // Verify the generated key + Assert.NotNull(publicKey.Parameters.Q.X); + Assert.NotNull(publicKey.Parameters.Q.Y); + Assert.Equal(32, publicKey.Parameters.Q.X.Length); + Assert.Equal(32, publicKey.Parameters.Q.Y.Length); + Assert.Equal(ECCurve.NamedCurves.nistP256.Oid.Value, publicKey.Parameters.Curve.Oid.Value); + + using var ecdsa = ECDsa.Create(publicKey.Parameters); + Assert.NotNull(ecdsa); + + keyParameters = new Scp11KeyParameters(keyReference, publicKey); + } + + using (var _ = new SecurityDomainSession(testDevice, keyParameters)) + { + // Secure channel established + } + } + + private static void VerifyScp11bAuth( + SecurityDomainSession session) + { + var keyRef = new KeyReference(ScpKeyIds.Scp11B, 0x7f); + session.GenerateEcKey(keyRef, 0); + session.DeleteKey(keyRef); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11a_WithAllowList_AllowsApprovedSerials( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + const byte kvn = 0x05; + var oceKeyRef = new KeyReference(OceKid, kvn); + + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + keyParams = LoadKeys(session, ScpKeyIds.Scp11A, kvn); + } + + using (var session = new SecurityDomainSession(testDevice, keyParams)) + { + var serials = new List + { + // Serial numbers from oce certs + "7F4971B0AD51F84C9DA9928B2D5FEF5E16B2920A", + "6B90028800909F9FFCD641346933242748FBE9AD" + }; + + // Only the above serials shall work. + session.StoreAllowlist(oceKeyRef, serials); + } + + using (var session = new SecurityDomainSession(testDevice, keyParams)) + { + session.DeleteKey(new KeyReference(ScpKeyIds.Scp11A, kvn)); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11a_WithAllowList_BlocksUnapprovedSerials( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + const byte kvn = 0x03; + var oceKeyRef = new KeyReference(OceKid, kvn); + + Scp03KeyParameters scp03KeyParams; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + // Import SCP03 key and get key parameters + scp03KeyParams = ImportScp03Key(session); + } + + Scp11KeyParameters scp11KeyParams; + using (var session = new SecurityDomainSession(testDevice, scp03KeyParams)) + { + // Make space for new key + session.DeleteKey(new KeyReference(ScpKeyIds.Scp11B, 0x01), false); + + // Load SCP11a keys + scp11KeyParams = LoadKeys(session, ScpKeyIds.Scp11A, kvn); + + // Create list of serial numbers + var serials = new List + { + "01", + "02", + "03" + }; + + // Store the allow list + session.StoreAllowlist(oceKeyRef, serials); + } + + // This is the test. Authenticate with SCP11a should throw. + Assert.Throws(() => + { + using (var _ = new SecurityDomainSession(testDevice, scp11KeyParams)) + { + // ... Authenticated + } + }); + + // Reset the allow list + using (var session = new SecurityDomainSession(testDevice, scp03KeyParams)) + { + session.ClearAllowList(oceKeyRef); + } + + // Now, with the allowlist removed, authenticate with SCP11a should now succeed + using (var _ = new SecurityDomainSession(testDevice, scp11KeyParams)) + { + // ... Authenticated + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11a_Authenticate_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + const byte kvn = 0x03; + var keyRef = new KeyReference(ScpKeyIds.Scp11A, kvn); + + // Start secure session with default key + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + keyParams = LoadKeys(session, ScpKeyIds.Scp11A, kvn); + } + + // Start authenticated session using new key params and public key from yubikey + // Authenticating and deleting should work + using (var session = new SecurityDomainSession(testDevice, keyParams)) + { + session.DeleteKey(keyRef); + } + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11c_EstablishSecureConnection_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + const byte kvn = 0x03; + var keyReference = new KeyReference(ScpKeyIds.Scp11C, kvn); + + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) + { + keyParams = LoadKeys(session, ScpKeyIds.Scp11C, kvn); + } + + Assert.Throws(() => + { + using var session = new SecurityDomainSession(testDevice, keyParams); + session.DeleteKey(keyReference); + }); + } + + private Scp11KeyParameters LoadKeys( + SecurityDomainSession session, + byte scpKid, + byte kvn) + { + var sessionRef = new KeyReference(scpKid, kvn); + var oceRef = new KeyReference(OceKid, kvn); + + // Generate new key pair on YubiKey and store public key for later use + var newPublicKey = session.GenerateEcKey(sessionRef, 0); + + var oceCerts = GetOceCertificates(Scp11TestData.OceCerts.Span); + if (oceCerts.Ca == null) + { + throw new InvalidOperationException("Missing CA certificate"); + } + + // Put Oce Keys + var ocePublicKey = new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()!); + session.PutKey(oceRef, ocePublicKey, 0); + + // Get Oce subject key identifier + var ski = GetSki(oceCerts.Ca); + if (ski.IsEmpty) + { + throw new InvalidOperationException("CA certificate missing Subject Key Identifier"); + } + + // Store the key identifier with the referenced off card entity on the Yubikey + session.StoreCaIssuer(oceRef, ski); + + var (certChain, privateKey) = + GetOceCertificateChainAndPrivateKey(Scp11TestData.Oce, Scp11TestData.OcePassword); + + // Now we have the EC private key parameters and cert chain + return new Scp11KeyParameters( + sessionRef, + new ECPublicKeyParameters(newPublicKey.Parameters), + oceRef, + new ECPrivateKeyParameters(privateKey), + certChain + ); + } + + private static (List certChain, ECParameters privateKey) GetOceCertificateChainAndPrivateKey( + ReadOnlyMemory ocePkcs12, + ReadOnlyMemory ocePassword) + { + // Load the OCE PKCS12 using Bouncy Castle Pkcs12 Store + using var pkcsStream = new MemoryStream(ocePkcs12.ToArray()); + var pkcs12Store = new Pkcs12Store(pkcsStream, ocePassword.ToArray()); + + // Get the first alias (usually there's only one) + var alias = pkcs12Store.Aliases.Cast().FirstOrDefault(); + if (alias == null || !pkcs12Store.IsKeyEntry(alias)) + { + throw new InvalidOperationException("No private key entry found in PKCS12"); + } + + // Get the certificate chain + var x509CertificateEntries = pkcs12Store.GetCertificateChain(alias); + var x509Certs = x509CertificateEntries + .Select(certEntry => + { + var cert = DotNetUtilities.ToX509Certificate(certEntry.Certificate); + return new X509Certificate2( + cert.Export(X509ContentType.Cert) + ); + }); + + var certs = ScpCertificates.From(x509Certs); + var certChain = new List(certs.Bundle); + if (certs.Leaf != null) + { + certChain.Add(certs.Leaf); + } + + // Get the private key + var privateKeyEntry = pkcs12Store.GetKey(alias); + if (!(privateKeyEntry.Key is Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters ecPrivateKey)) + { + throw new InvalidOperationException("Private key is not an EC key"); + } + + return (certChain, ConvertToECParameters(ecPrivateKey)); + } + + static ECParameters ConvertToECParameters( + Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters bcPrivateKey) + { + // Convert the BigInteger D to byte array + var dBytes = bcPrivateKey.D.ToByteArrayUnsigned(); + + // Calculate public key point Q = d*G + var Q = bcPrivateKey.Parameters.G.Multiply(bcPrivateKey.D); + + // Get X and Y coordinates as byte arrays + var xBytes = Q.XCoord.ToBigInteger().ToByteArrayUnsigned(); + var yBytes = Q.YCoord.ToBigInteger().ToByteArrayUnsigned(); + + // Create ECParameters with P-256 curve + return new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + D = dBytes, + Q = new ECPoint + { + X = xBytes, + Y = yBytes + } + }; + } + + private ScpCertificates GetOceCertificates( + ReadOnlySpan pem) + { + try + { + var certificates = new List(); + + // Convert PEM to a string + var pemString = Encoding.UTF8.GetString(pem); + + // Split the PEM string into individual certificates + var pemCerts = pemString.Split( + new[] { "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----" }, + StringSplitOptions.RemoveEmptyEntries + ); + + foreach (var certString in pemCerts) + { + if (!string.IsNullOrWhiteSpace(certString)) + { + // Remove any whitespace and convert to byte array + var certData = Convert.FromBase64String(certString.Trim()); + var cert = new X509Certificate2(certData); + certificates.Add(cert); + } + } + + return ScpCertificates.From(certificates); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to parse PEM certificates", ex); + } + } + + private Memory GetSki( + X509Certificate2 certificate) + { + var extension = certificate.Extensions["2.5.29.14"]; + if (!(extension is X509SubjectKeyIdentifierExtension skiExtension)) + { + throw new InvalidOperationException("Invalid Subject Key Identifier extension"); + } + + var rawData = skiExtension.RawData; + if (rawData == null || rawData.Length == 0) + { + throw new InvalidOperationException("Missing Subject Key Identifier"); + } + + var tlv = TlvObject.Parse(skiExtension.RawData); + return tlv.Value; + } + + private static Scp03KeyParameters ImportScp03Key( + SecurityDomainSession session) + { + var scp03Ref = new KeyReference(0x01, 0x01); + var staticKeys = new StaticKeys( + GetRandomBytes(16), + GetRandomBytes(16), + GetRandomBytes(16) + ); + + session.PutKey(scp03Ref, staticKeys, 0); + + return new Scp03KeyParameters(scp03Ref, staticKeys); + } + + private static Memory GetRandomBytes( + byte length) + { + using var rng = CryptographyProviders.RngCreator(); + Span hostChallenge = stackalloc byte[length]; + rng.GetBytes(hostChallenge); + + return hostChallenge.ToArray(); + } + + /// + /// This is a copy of Scp11b_EstablishSecureConnection_Succeeds test + /// + private static Scp11KeyParameters Get_Scp11b_SecureConnection_Parameters( + IYubiKeyDevice testDevice, + KeyReference keyReference) + { + IReadOnlyCollection certificateList; + using (var session = new SecurityDomainSession(testDevice)) + { + certificateList = session.GetCertificates(keyReference); + } + + var leaf = certificateList.Last(); + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!; + var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); + + return keyParams; + } + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs new file mode 100644 index 000000000..87197bab2 --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Yubico.YubiKey.Scp +{ + public class ScpCertificates + { + public X509Certificate2? Ca { get; } + public IReadOnlyList Bundle { get; } + public X509Certificate2? Leaf { get; } + + private ScpCertificates(X509Certificate2? ca, IReadOnlyList bundle, X509Certificate2? leaf) + { + Ca = ca; + Bundle = bundle ?? throw new ArgumentNullException(nameof(bundle)); + Leaf = leaf; + } + + public static ScpCertificates From(IEnumerable? certificates) + { + if (certificates == null || !certificates.Any()) + { + return new ScpCertificates(null, Array.Empty(), null); + } + + var certList = certificates.ToList(); + X509Certificate2? ca = null; + byte[]? seenSerial = null; + + // Order certificates with the Root CA on top + var ordered = new List { certList[0] }; + certList.RemoveAt(0); + + while (certList.Count > 0) + { + var head = ordered[0]; + var tail = ordered[^1]; + var cert = certList[0]; + certList.RemoveAt(0); + + if (IsIssuedBy(cert, cert)) + { + ordered.Insert(0, cert); + ca = ordered[0]; + continue; + } + + if (IsIssuedBy(cert, tail)) + { + ordered.Add(cert); + continue; + } + + if (IsIssuedBy(head, cert)) + { + ordered.Insert(0, cert); + continue; + } + + if (seenSerial != null && cert.GetSerialNumber().SequenceEqual(seenSerial)) + { + throw new InvalidOperationException($"Cannot decide the order of {cert} in {string.Join(", ", ordered)}"); + } + + // This cert could not be ordered, try to process rest of certificates + // but if you see this cert again fail because the cert chain is not complete + certList.Add(cert); + seenSerial = cert.GetSerialNumber(); + } + + // Find ca and leaf + if (ca != null) + { + ordered.RemoveAt(0); + } + + X509Certificate2? leaf = null; + if (ordered.Count > 0) + { + var lastCert = ordered[^1]; + var keyUsage = lastCert.Extensions.OfType().FirstOrDefault()?.KeyUsages ?? default; + if (keyUsage.HasFlag(X509KeyUsageFlags.DigitalSignature)) + { + leaf = lastCert; + ordered.RemoveAt(ordered.Count - 1); + } + } + + return new ScpCertificates(ca, ordered, leaf); + } + + private static bool IsIssuedBy(X509Certificate2 subjectCert, X509Certificate2 issuerCert) + { + return subjectCert.IssuerName.RawData.SequenceEqual(issuerCert.SubjectName.RawData); + } + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpTestUtilities.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpTestUtilities.cs new file mode 100644 index 000000000..bf5d0e70f --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpTestUtilities.cs @@ -0,0 +1,64 @@ +// 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 Xunit; +using Yubico.YubiKey.Piv; + +namespace Yubico.YubiKey.Scp +{ + public static class ScpTestUtilities + { + public static byte[] FipsPin = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 + }; + + public static byte[] FipsPuk = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 + }; + + public static byte[] FipsManagementKey = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x12, + 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x9A + }; + + public static void SetFipsApprovedCredentials(PivSession session) + { + session.ResetApplication(); + session.KeyCollector = new Simple39KeyCollector().Simple39KeyCollectorDelegate; + + session.TryChangePin(Simple39KeyCollector.CollectPin(), FipsPin, out _); + session.TryChangePuk(Simple39KeyCollector.CollectPuk(), FipsPuk, out _); + session.TryChangeManagementKey(Simple39KeyCollector.CollectMgmtKey(), FipsManagementKey, PivTouchPolicy.Always); + Assert.True(session.TryVerifyPin(FipsPin, out _)); + } + + public static void SetFipsApprovedCredentials( + IYubiKeyDevice device, + YubiKeyApplication application, + ScpKeyParameters parameters) + { + if (application == YubiKeyApplication.Piv) + { + using var session = new PivSession(device, parameters); + SetFipsApprovedCredentials(session); + + return; + } + + throw new NotSupportedException(); + } + } +} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs deleted file mode 100644 index 46b3245aa..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2023 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 Xunit; -using Yubico.YubiKey.TestUtilities; - -namespace Yubico.YubiKey.Scp03.Commands -{ - [Trait(TraitTypes.Category, TestCategories.Simple)] - public class DeleteKeyCommandTests - { - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void DeleteKey_One_Succeeds(StandardTestDevice testDeviceType) - { - byte[] key1 = { - 0x33, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, - }; - byte[] key2 = { - 0x33, 0xee, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xff, - }; - byte[] key3 = { - 0x33, 0xdd, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xee, 0xff, 0x11, - }; - var currentKeys = new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 3 - }; - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new DeleteKeyCommand(1, false); - Scp03Response rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) - { - byte[] key1 = { - 0x33, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, - }; - byte[] key2 = { - 0x33, 0xee, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xff, - }; - byte[] key3 = { - 0x33, 0xdd, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xee, 0xff, 0x11, - }; - var currentKeys = new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 3 - }; - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new DeleteKeyCommand(2, false); - Scp03Response rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void DeleteKey_Three_Succeeds(StandardTestDevice testDeviceType) - { - byte[] key1 = { - 0x33, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, - }; - byte[] key2 = { - 0x33, 0xee, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xff, - }; - byte[] key3 = { - 0x33, 0xdd, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xee, 0xff, 0x11, - }; - var currentKeys = new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 3 - }; - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new DeleteKeyCommand(3, true); - Scp03Response rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - } -} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs deleted file mode 100644 index 52c67645c..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2023 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 Xunit; -using Yubico.YubiKey.TestUtilities; - -namespace Yubico.YubiKey.Scp03.Commands -{ - public class PutKeyCommandTests - { - // These may require that DeleteKeyCommandTests have been run first. - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void ChangeDefaultKey_Succeeds(StandardTestDevice testDeviceType) - { - byte[] key1 = { - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff - }; - byte[] key2 = { - 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11 - }; - byte[] key3 = { - 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22 - }; - - var currentKeys = new StaticKeys(); - var newKeys = new StaticKeys(key2, key1, key3); - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new PutKeyCommand(currentKeys, newKeys); - PutKeyResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - ReadOnlyMemory checksum = rsp.GetData(); - bool isEqual = checksum.Span.SequenceEqual(cmd.ExpectedChecksum.Span); - Assert.True(isEqual); - } - - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void AddNewKeySet_Succeeds(StandardTestDevice testDeviceType) - { - byte[] key1 = { - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff - }; - byte[] key2 = { - 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11 - }; - byte[] key3 = { - 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22 - }; - byte[] newKey1 = { - 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, - }; - byte[] newKey2 = { - 0xee, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xff, 0x11 - }; - byte[] newKey3 = { - 0xdd, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xee, 0xff, 0x11, 0x22 - }; - - var currentKeys = new StaticKeys(key2, key1, key3); - var newKeys = new StaticKeys(newKey2, newKey1, newKey3) - { - KeyVersionNumber = 2 - }; - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new PutKeyCommand(currentKeys, newKeys); - PutKeyResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - ReadOnlyMemory checksum = rsp.GetData(); - bool isEqual = checksum.Span.SequenceEqual(cmd.ExpectedChecksum.Span); - Assert.True(isEqual); - } - - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void AddThirdKeySet_Succeeds(StandardTestDevice testDeviceType) - { - byte[] key1 = { - 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, - }; - byte[] key2 = { - 0xee, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xff, 0x11 - }; - byte[] key3 = { - 0xdd, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xee, 0xff, 0x11, 0x22 - }; - byte[] newKey1 = { - 0x33, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, - }; - byte[] newKey2 = { - 0x33, 0xee, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xff, - }; - byte[] newKey3 = { - 0x33, 0xdd, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xee, 0xff, 0x11, - }; - - var currentKeys = new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 2 - }; - var newKeys = new StaticKeys(newKey2, newKey1, newKey3) - { - KeyVersionNumber = 3 - }; - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new PutKeyCommand(currentKeys, newKeys); - PutKeyResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - ReadOnlyMemory checksum = rsp.GetData(); - bool isEqual = checksum.Span.SequenceEqual(cmd.ExpectedChecksum.Span); - Assert.True(isEqual); - } - } -} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs deleted file mode 100644 index 97839d4b3..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2023 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 Xunit; -using Yubico.YubiKey.TestUtilities; - -namespace Yubico.YubiKey.Scp03 -{ - // These may require that DeleteKeyCommandTests have been run first. - [TestCaseOrderer(PriorityOrderer.TypeName, PriorityOrderer.AssembyName)] - public class PutDeleteTests - { - [Fact] - [TestPriority(3)] - public void PutKey_Succeeds() - { - using var staticKeys = new StaticKeys(); - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, - minimumFirmwareVersion: FirmwareVersion.V5_3_0); - - using (var scp03Session = new Scp03Session(device, staticKeys)) - { - using StaticKeys newKeys = GetKeySet(1); - scp03Session.PutKeySet(newKeys); - - using StaticKeys nextKeys = GetKeySet(2); - scp03Session.PutKeySet(nextKeys); - } - - using StaticKeys keySet2 = GetKeySet(2); - - using (var scp03Session = new Scp03Session(device, keySet2)) - { - using StaticKeys keySet3 = GetKeySet(3); - scp03Session.PutKeySet(keySet3); - } - } - - [Fact] - [TestPriority(3)] - public void ReplaceKey_Succeeds() - { - using StaticKeys staticKeys = GetKeySet(2); - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, - FirmwareVersion.V5_3_0); - - using (var scp03Session = new Scp03Session(device, staticKeys)) - { - using StaticKeys newKeys = GetKeySet(1); - newKeys.KeyVersionNumber = 2; - scp03Session.PutKeySet(newKeys); - } - } - - [Fact] - [TestPriority(0)] - public void DeleteKey_Succeeds() - { - using StaticKeys staticKeys = GetKeySet(3); - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, - FirmwareVersion.V5_3_0); - - using var scp03Session = new Scp03Session(device, staticKeys); - scp03Session.DeleteKeySet(1); - scp03Session.DeleteKeySet(2); - - scp03Session.DeleteKeySet(3, true); - } - - // The setNumber is to be 1, 2, or 3 - private StaticKeys GetKeySet(int setNumber) => setNumber switch - { - 1 => GetKeySet1(), - 2 => GetKeySet2(), - _ => GetKeySet3() - }; - - private StaticKeys GetKeySet1() - { - var key1 = new ReadOnlyMemory(new byte[] - { - 0x11, 0x11, 0x11, 0x11, 0x49, 0x2f, 0x4d, 0x09, 0x22, 0xec, 0x3d, 0xb4, 0x6b, 0x20, 0x94, 0x7a - }); - var key2 = new ReadOnlyMemory(new byte[] - { - 0x12, 0x12, 0x12, 0x12, 0x53, 0xB3, 0xE3, 0x78, 0x2A, 0x1D, 0xE5, 0xDC, 0x5A, 0xF4, 0xa6, 0x41 - }); - var key3 = new ReadOnlyMemory(new byte[] - { - 0x13, 0x13, 0x13, 0x13, 0x68, 0xDE, 0x7A, 0xB7, 0x74, 0x19, 0xBB, 0x7F, 0xB0, 0x55, 0x7d, 0x40 - }); - - return new StaticKeys(key2, key1, key3); - } - - private StaticKeys GetKeySet2() - { - var key1 = new ReadOnlyMemory(new byte[] - { - 0x21, 0x21, 0x21, 0x21, 0x20, 0x94, 0x7a, 0x49, 0x2f, 0x4d, 0x09, 0x22, 0xec, 0x3d, 0xb4, 0x6b - }); - var key2 = new ReadOnlyMemory(new byte[] - { - 0x22, 0x22, 0x22, 0x22, 0xDC, 0x5A, 0xF4, 0xa6, 0x41, 0x53, 0xB3, 0xE3, 0x78, 0x2A, 0x1D, 0xE5 - }); - var key3 = new ReadOnlyMemory(new byte[] - { - 0x23, 0x23, 0x23, 0x23, 0x7d, 0x40, 0x68, 0xDE, 0x7A, 0xB7, 0x74, 0x19, 0xBB, 0x7F, 0xB0, 0x55 - }); - - return new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 2 - }; - } - - private StaticKeys GetKeySet3() - { - var key1 = new ReadOnlyMemory(new byte[] - { - 0x21, 0x21, 0x21, 0x21, 0x20, 0xDC, 0x5A, 0xF4, 0xa6, 0x41, 0x94, 0x7a, 0x49, 0x2f, 0x4d, 0x09 - }); - var key2 = new ReadOnlyMemory(new byte[] - { - 0x22, 0x22, 0x22, 0x22, 0x22, 0xec, 0x3d, 0xb4, 0x6b, 0x53, 0xB3, 0xE3, 0x78, 0x2A, 0x1D, 0xE5 - }); - var key3 = new ReadOnlyMemory(new byte[] - { - 0x23, 0x23, 0x23, 0x23, 0x7A, 0xB7, 0x74, 0x19, 0x7d, 0x40, 0x68, 0xDE, 0xBB, 0x7F, 0xB0, 0x55 - }); - - return new StaticKeys(key2, key1, key3) - { - KeyVersionNumber = 3 - }; - } - } -} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs deleted file mode 100644 index 9adcf96df..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -// 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. - -using System; -using Xunit; -using Yubico.YubiKey.Piv; -using Yubico.YubiKey.Piv.Commands; -using Yubico.YubiKey.TestUtilities; - -namespace Yubico.YubiKey.Scp03 -{ - public class SimpleSessionTests - { - private readonly byte[] _pin = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; - - [Theory] - [InlineData(StandardTestDevice.Fw5)] - public void SessionSetupAndUse_Succeeds(StandardTestDevice testDeviceType) - { - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - Assert.True(device.FirmwareVersion >= FirmwareVersion.V5_3_0); - Assert.True(device.HasFeature(YubiKeyFeature.Scp03)); - -#pragma warning disable CS0618 // Specifically testing this soon-to-be-deprecated feature - IYubiKeyDevice scp03Device = (device as YubiKeyDevice)!.WithScp03(new StaticKeys()); -#pragma warning restore CS0618 - - using var piv = new PivSession(scp03Device); - bool result = piv.TryVerifyPin(new ReadOnlyMemory(new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }), out _); - Assert.True(result); - - PivMetadata metadata = piv.GetMetadata(PivSlot.Pin)!; - Assert.Equal(3, metadata.RetryCount); - } - - [Fact] - public void ConnectScp03_Application_Succeeds() - { - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); - - using var scp03Keys = new StaticKeys(); - - using IYubiKeyConnection connection = device.ConnectScp03(YubiKeyApplication.Piv, scp03Keys); - Assert.NotNull(connection); - - var cmd = new VerifyPinCommand(_pin); - VerifyPinResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - - [Fact] - public void ConnectScp03_AlgorithmId_Succeeds() - { - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); - - using var scp03Keys = new StaticKeys(); - - using IYubiKeyConnection connection = device.ConnectScp03( - YubiKeyApplication.Piv.GetIso7816ApplicationId(), scp03Keys); - Assert.NotNull(connection); - - var cmd = new VerifyPinCommand(_pin); - VerifyPinResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - - [Fact] - public void TryConnectScp03_Application_Succeeds() - { - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); - - using var scp03Keys = new StaticKeys(); - - bool isValid = device.TryConnectScp03(YubiKeyApplication.Piv, scp03Keys, out IScp03YubiKeyConnection? connection); - using (connection) - { - Assert.NotNull(connection); - Assert.True(isValid); - var cmd = new VerifyPinCommand(_pin); - VerifyPinResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - } - - [Fact] - public void TryConnectScp03_AlgorithmId_Succeeds() - { - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); - - using var scp03Keys = new StaticKeys(); - - bool isValid = device.TryConnectScp03( - YubiKeyApplication.Piv.GetIso7816ApplicationId(), scp03Keys, out IScp03YubiKeyConnection? connection); - using (connection) - { - Assert.NotNull(connection); - Assert.True(isValid); - var cmd = new VerifyPinCommand(_pin); - VerifyPinResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - } - } -} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/SessionCredentialTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/SessionCredentialTests.cs index 5249cc4e9..451b6767a 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/SessionCredentialTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/SessionCredentialTests.cs @@ -17,22 +17,28 @@ using System.Linq; using System.Security; using Xunit; +using Yubico.YubiKey.Scp; +using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.YubiHsmAuth { public class SessionCredentialTests { #region DeleteCredential - [Fact] - public void AddCredential_DefaultTestCred_AppContainsOneCred() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void AddCredential_DefaultTestCred_AppContainsOneCred(StandardTestDevice selectedDevice, bool useScp = false) { // Preconditions - IYubiKeyDevice testDevice = YhaTestUtilities.GetCleanDevice(); + IYubiKeyDevice testDevice = YhaTestUtilities.GetCleanDevice(selectedDevice); IReadOnlyList credentialList; // Test - using (var yubiHsmAuthSession = new YubiHsmAuthSession(testDevice)) + using (var yubiHsmAuthSession = new YubiHsmAuthSession(testDevice, useScp ? Scp03KeyParameters.DefaultKey : null)) { yubiHsmAuthSession.AddCredential(YhaTestUtilities.DefaultMgmtKey, YhaTestUtilities.DefaultAes128Cred); @@ -173,15 +179,19 @@ public void TryDeleteCredentialKeyCollector_NoKeyCollector_ThrowsInvalidOpEx() #endregion #region AddCredential - [Fact] - public void TryAddCredentialKeyCollector_CorrectMgmtKey_AppContainsNewCred() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + [InlineData(StandardTestDevice.Fw5, true)] + [InlineData(StandardTestDevice.Fw5Fips, true)] + public void TryAddCredentialKeyCollector_CorrectMgmtKey_AppContainsNewCred(StandardTestDevice selectedDevice, bool useScp = false) { // Preconditions - IYubiKeyDevice testDevice = YhaTestUtilities.GetCleanDevice(); + IYubiKeyDevice testDevice = YhaTestUtilities.GetCleanDevice(selectedDevice); IReadOnlyList credentialList; - using (var yubiHsmAuthSession = new YubiHsmAuthSession(testDevice)) + using (var yubiHsmAuthSession = new YubiHsmAuthSession(testDevice, useScp ? Scp03KeyParameters.DefaultKey : null)) { yubiHsmAuthSession.KeyCollector = SimpleKeyCollector.DefaultValueCollectorDelegate; diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/YhaTestUtilities.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/YhaTestUtilities.cs index 7e5a51905..9a3ea6a8d 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/YhaTestUtilities.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiHsmAuth/YhaTestUtilities.cs @@ -90,9 +90,9 @@ public class YhaTestUtilities /// into a known "control" state for performing integration /// tests with the YubiHSM Auth application. /// - public static IYubiKeyDevice GetCleanDevice() + public static IYubiKeyDevice GetCleanDevice(StandardTestDevice testDeviceType = StandardTestDevice.Fw5) { - var testDevice = DeviceReset.EnableAllCapabilities(IntegrationTestDeviceEnumeration.GetTestDevice()); + var testDevice = DeviceReset.EnableAllCapabilities(IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType)); return DeviceReset.ResetYubiHsmAuth(testDevice); } diff --git a/Yubico.YubiKey/tests/sandbox/Plugins/GregPlugin.cs b/Yubico.YubiKey/tests/sandbox/Plugins/GregPlugin.cs index 01505a9b5..c57e77e19 100644 --- a/Yubico.YubiKey/tests/sandbox/Plugins/GregPlugin.cs +++ b/Yubico.YubiKey/tests/sandbox/Plugins/GregPlugin.cs @@ -14,6 +14,9 @@ using System; using System.Linq; +using Microsoft.Extensions.Logging; +using Yubico.Core.Logging; +using Yubico.YubiKey.Piv; namespace Yubico.YubiKey.TestApp.Plugins { @@ -23,6 +26,7 @@ internal class GregPlugin : PluginBase public override string Description => "A place for Greg's test code"; public GregPlugin(IOutput output) : base(output) { } + public static ILogger Logger => Log.GetLogger(); public override bool Execute() { @@ -33,8 +37,13 @@ public override bool Execute() Console.Error.WriteLine($"YubiKey Version: {yubiKey.FirmwareVersion}"); Console.Error.WriteLine("NFC Before Value: " + yubiKey.IsNfcRestricted); - yubiKey.SetIsNfcRestricted(true); - + using (var session = new PivSession(yubiKey)) + { + Logger.LogDebug("Disconnect Yubikey"); + Console.ReadLine(); + } + //Dispose + Console.ReadLine(); return true; } diff --git a/Yubico.YubiKey/tests/sandbox/Plugins/Scp03Plugin.cs b/Yubico.YubiKey/tests/sandbox/Plugins/Scp03Plugin.cs index 1bc529047..f6e091423 100644 --- a/Yubico.YubiKey/tests/sandbox/Plugins/Scp03Plugin.cs +++ b/Yubico.YubiKey/tests/sandbox/Plugins/Scp03Plugin.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.Linq; using Yubico.YubiKey.Piv; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey.TestApp.Plugins @@ -45,10 +46,7 @@ private bool BasicE2ETest() IEnumerable keys = YubiKeyDevice.FindByTransport(Transport.UsbSmartCard); IYubiKeyDevice device = keys.Single()!; -#pragma warning disable CS0618 // Specifically testing this soon-to-be-deprecated feature - IYubiKeyDevice scp03Device = (device as YubiKeyDevice)!.WithScp03(new StaticKeys()); -#pragma warning restore CS0618 - using var piv = new PivSession(scp03Device); + using var piv = new PivSession(device, Scp03KeyParameters.DefaultKey); bool result = piv.TryVerifyPin(new ReadOnlyMemory(new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }), out _); Output.WriteLine($"pin 123456: {result}"); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs index ed654dad4..84ea502d4 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs @@ -56,7 +56,7 @@ public void AesBlockCipher_GivenKeyPlaintext_EncryptsCorrectly() byte[] plaintext = GetPlaintext(); // Act - byte[] result = AesUtilities.BlockCipher(key, plaintext); + var result = AesUtilities.BlockCipher(key, plaintext); // Assert Assert.Equal(result, Hex.HexToBytes("dcc0c378ec111cb23048486ef9d9a6b7")); @@ -116,7 +116,7 @@ public void AesCbcEncrypt_GivenKeyIVPlaintext_EncryptsCorrectly() byte[] iv = GetIV(); // Act - byte[] result = AesUtilities.AesCbcEncrypt(key, iv, plaintext); + ReadOnlyMemory result = AesUtilities.AesCbcEncrypt(key, iv, plaintext); // Assert Assert.Equal(result, Hex.HexToBytes("da19df061b1bcba151d692a4a9e63901")); @@ -175,7 +175,7 @@ public void AesCbcDecrypt_GivenKeyIVCiphertext_EncryptsCorrectly() byte[] iv = GetIV(); // Act - byte[] result = AesUtilities.AesCbcDecrypt(key, iv, ciphertext); + ReadOnlyMemory result = AesUtilities.AesCbcDecrypt(key, iv, ciphertext); // Assert Assert.Equal(result, Hex.HexToBytes("d1af13631ea24595793ddf5bf6f9c42c")); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECParametersExtensionsTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECParametersExtensionsTests.cs new file mode 100644 index 000000000..7ad6a37fb --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECParametersExtensionsTests.cs @@ -0,0 +1,72 @@ +using System.Security.Cryptography; +using Xunit; + +namespace Yubico.YubiKey.Cryptography +{ + public class ECParametersExtensionsTests + { + [Fact] + public void DeepCopy_WithValidParameters_CreatesIndependentCopy() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var original = ecdsa.ExportParameters(true); + + // Act + var copy = original.DeepCopy(); + + // Assert + Assert.Equal(original.Curve.Oid.Value, copy.Curve.Oid.Value); + Assert.Equal(original.Q.X, copy.Q.X); + Assert.Equal(original.Q.Y, copy.Q.Y); + Assert.Equal(original.D, copy.D); + + // Verify independence + Assert.NotSame(original.Q.X, copy.Q.X); + Assert.NotSame(original.Q.Y, copy.Q.Y); + Assert.NotSame(original.D, copy.D); + } + + [Fact] + public void DeepCopy_WithNullArrays_HandlesNullsCorrectly() + { + // Arrange + var original = new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new ECPoint + { + X = null, + Y = null + }, + D = null + }; + + // Act + var copy = original.DeepCopy(); + + // Assert + Assert.Equal(original.Curve.Oid.Value, copy.Curve.Oid.Value); + Assert.Null(copy.Q.X); + Assert.Null(copy.Q.Y); + Assert.Null(copy.D); + } + + [Fact] + public void DeepCopy_ModifyingCopyDoesNotAffectOriginal() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var original = ecdsa.ExportParameters(true); + var copy = original.DeepCopy(); + Assert.NotNull(copy.Q.X); + + // Act + copy.Q.X[0] = (byte)(copy.Q.X[0] + 1); + + // Assert + Assert.NotNull(original.Q.X); + Assert.NotEqual(original.Q.X[0], copy.Q.X[0]); + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECPrivateKeyParametersTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECPrivateKeyParametersTests.cs new file mode 100644 index 000000000..ff151ab21 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECPrivateKeyParametersTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Security.Cryptography; +using Xunit; + +namespace Yubico.YubiKey.Cryptography +{ + public class ECPrivateKeyParametersTests + { + [Fact] + public void Constructor_WithValidECParameters_CreatesInstance() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var parameters = ecdsa.ExportParameters(true); + + // Act + var privateKeyParams = new ECPrivateKeyParameters(parameters); + + // Assert + Assert.NotNull(privateKeyParams.Parameters.D); + Assert.Equal(parameters.D, privateKeyParams.Parameters.D); + Assert.Equal(parameters.Q.X, privateKeyParams.Parameters.Q.X); + Assert.Equal(parameters.Q.Y, privateKeyParams.Parameters.Q.Y); + } + + [Fact] + public void Constructor_WithECDsaObject_CreatesInstance() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + // Act + var privateKeyParams = new ECPrivateKeyParameters(ecdsa); + + // Assert + Assert.NotNull(privateKeyParams.Parameters.D); + Assert.NotNull(privateKeyParams.Parameters.Q.X); + Assert.NotNull(privateKeyParams.Parameters.Q.Y); + } + + [Fact] + public void Constructor_WithNullDValue_ThrowsArgumentException() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var parameters = ecdsa.ExportParameters(true); + parameters.D = null; + + // Act & Assert + var exception = Assert.Throws(() => new ECPrivateKeyParameters(parameters)); + Assert.Equal("parameters", exception.ParamName); + Assert.Contains("D value", exception.Message); + } + + [Fact] + public void Constructor_PerformsDeepCopy() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var originalParams = ecdsa.ExportParameters(true); + var privateKeyParams = new ECPrivateKeyParameters(originalParams); + + Assert.NotNull(originalParams.D); + Assert.NotNull(originalParams.Q.X); + Assert.NotNull(originalParams.Q.Y); + Assert.NotNull(privateKeyParams.Parameters.D); + Assert.NotNull(privateKeyParams.Parameters.Q.X); + Assert.NotNull(privateKeyParams.Parameters.Q.Y); + + // Act - Modify original parameters + originalParams.D[0] = (byte)(originalParams.D[0] + 1); + originalParams.Q.X[0] = (byte)(originalParams.Q.X[0] + 1); + originalParams.Q.Y[0] = (byte)(originalParams.Q.Y[0] + 1); + + // Assert - Verify that the stored parameters weren't affected + Assert.NotEqual(originalParams.D[0], privateKeyParams.Parameters.D[0]); + Assert.NotEqual(originalParams.Q.X[0], privateKeyParams.Parameters.Q.X[0]); + Assert.NotEqual(originalParams.Q.Y[0], privateKeyParams.Parameters.Q.Y[0]); + } + + [Fact] + public void Constructor_WithNullECDsaObject_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new ECPrivateKeyParameters(null!)); + } + + [Theory] + [InlineData("1.2.840.10045.3.1.7")] // NIST P-256 + [InlineData("1.3.132.0.34")] // NIST P-384 + [InlineData("1.3.132.0.35")] // NIST P-512 + public void Constructor_WithDifferentCurves_CreatesValidInstance(string oid) + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.CreateFromOid(Oid.FromOidValue(oid, OidGroup.PublicKeyAlgorithm))); + + // Act + var privateKeyParams = new ECPrivateKeyParameters(ecdsa); + + // Assert + Assert.Equal(oid, privateKeyParams.Parameters.Curve.Oid.Value); + Assert.NotNull(privateKeyParams.Parameters.D); + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECPublicKeyParametersTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECPublicKeyParametersTests.cs new file mode 100644 index 000000000..697c7384a --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/ECPublicKeyParametersTests.cs @@ -0,0 +1,123 @@ +using System; +using System.Security.Cryptography; +using Xunit; + +namespace Yubico.YubiKey.Cryptography +{ + public class ECPublicKeyParametersTests + { + [Fact] + public void Constructor_WithValidPublicParameters_CreatesInstance() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var parameters = ecdsa.ExportParameters(false); + + // Act + var publicKeyParams = new ECPublicKeyParameters(parameters); + + // Assert + Assert.Null(publicKeyParams.Parameters.D); + Assert.NotNull(publicKeyParams.Parameters.Q.X); + Assert.NotNull(publicKeyParams.Parameters.Q.Y); + } + + [Fact] + public void Constructor_WithPrivateKeyData_ThrowsArgumentException() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var parameters = ecdsa.ExportParameters(true); + + // Act & Assert + var exception = Assert.Throws(() => new ECPublicKeyParameters(parameters)); + Assert.Equal("parameters", exception.ParamName); + Assert.Contains("D value", exception.Message); + } + + [Fact] + public void Constructor_WithECDsa_CreatesValidInstance() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + // Act + var publicKeyParams = new ECPublicKeyParameters(ecdsa); + + // Assert + Assert.Null(publicKeyParams.Parameters.D); + Assert.NotNull(publicKeyParams.Parameters.Q.X); + Assert.NotNull(publicKeyParams.Parameters.Q.Y); + } + + [Fact] + public void Constructor_PerformsDeepCopy() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var originalParams = ecdsa.ExportParameters(false); + var publicKeyParams = new ECPublicKeyParameters(originalParams); + + // Act - Modify original parameters + Assert.NotNull(originalParams.Q.X); + Assert.NotNull(originalParams.Q.Y); + originalParams.Q.X[0] = (byte)(originalParams.Q.X[0] + 1); + originalParams.Q.Y[0] = (byte)(originalParams.Q.Y[0] + 1); + + // Assert + Assert.NotNull(publicKeyParams.Parameters.Q.X); + Assert.NotNull(publicKeyParams.Parameters.Q.Y); + Assert.NotEqual(originalParams.Q.X[0], publicKeyParams.Parameters.Q.X[0]); + Assert.NotEqual(originalParams.Q.Y[0], publicKeyParams.Parameters.Q.Y[0]); + } + + [Fact] + public void GetBytes_ReturnsCorrectFormat() + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyParams = new ECPublicKeyParameters(ecdsa); + + // Act + var bytes = publicKeyParams.GetBytes(); + + // Assert + Assert.NotNull(publicKeyParams.Parameters.Q.X); + Assert.NotNull(publicKeyParams.Parameters.Q.Y); + Assert.Equal(1 + publicKeyParams.Parameters.Q.X.Length + publicKeyParams.Parameters.Q.Y.Length, bytes.Length); + Assert.Equal(0x04, bytes.Span[0]); // Check format identifier + + // Verify X and Y coordinates are correctly copied + var xCoord = bytes.Slice(1, publicKeyParams.Parameters.Q.X.Length); + var yCoord = bytes.Slice(1 + publicKeyParams.Parameters.Q.X.Length); + + Assert.True(xCoord.Span.SequenceEqual(publicKeyParams.Parameters.Q.X)); + Assert.True(yCoord.Span.SequenceEqual(publicKeyParams.Parameters.Q.Y)); + } + + [Theory] + [InlineData("1.2.840.10045.3.1.7")] // NIST P-256 + [InlineData("1.3.132.0.34")] // NIST P-384 + [InlineData("1.3.132.0.35")] // NIST P-512 + public void Constructor_WithDifferentCurves_CreatesValidInstance(string oid) + { + // Arrange + using var ecdsa = ECDsa.Create(ECCurve.CreateFromOid(Oid.FromOidValue(oid, OidGroup.PublicKeyAlgorithm))); + + // Act + var publicKeyParams = new ECPublicKeyParameters(ecdsa); + + // Assert + Assert.Equal(oid, publicKeyParams.Parameters.Curve.Oid.Value); + Assert.NotNull(publicKeyParams.Parameters.Q.X); + Assert.NotNull(publicKeyParams.Parameters.Q.Y); + + } + + [Fact] + public void Constructor_WithNullECDsa_ThrowsArgumentNullException() + { + Assert.Throws(() => new ECPublicKeyParameters(null!)); + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs index 5e5ecd4f3..38bac23f1 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs @@ -92,8 +92,6 @@ void GetAuthenticatorInfo_SendsGetInfoCommand() var session = new Fido2Session(mockYubiKey.Object); - //session.AuthenticatorInfo; - mockConnection.Verify(c => c.SendCommand(It.IsAny())); } } diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs deleted file mode 100644 index 287cf0135..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -// 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. - -using System; -using System.Linq; -using System.Security.Cryptography; -using Xunit; -using Yubico.Core.Buffers; -using Yubico.Core.Iso7816; -using Yubico.YubiKey.InterIndustry.Commands; -using Yubico.YubiKey.Piv.Commands; -using Yubico.YubiKey.Scp03; - -namespace Yubico.YubiKey.Pipelines -{ - public class PipelineFixture : IApduTransform - { - public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) - { - if (command is null) - { - throw new ArgumentNullException(nameof(command)); - } - - if (command.AsByteArray().SequenceEqual(new SelectApplicationCommand(YubiKeyApplication.Piv).CreateCommandApdu().AsByteArray())) - { - return new ResponseApdu(Hex.HexToBytes("9000")); - } - else if (command.AsByteArray().SequenceEqual(Hex.HexToBytes("8050FF0008360CB43F4301B894"))) - { - return new ResponseApdu(Hex.HexToBytes("010B001F002500000000FF0360CAAFA4DAC615236ADD5607216F3E115C9000")); - } - else if (command.AsByteArray().SequenceEqual(Hex.HexToBytes("848233001045330AB30BB1A079A8E7F77376DB9F2C"))) - { - return new ResponseApdu(Hex.HexToBytes("9000")); - } - else if (command.AsByteArray().SequenceEqual(Hex.HexToBytes("84FD0000181CE4E3D8F32D986A886DDBC90C8DB22553C2C04391250CCE"))) - { - return new ResponseApdu(Hex.HexToBytes("5F67E9E059DF3C52809DC9F6DDFBEF3E4C45691B2C8CDDD89000")); - } - else - { - string apduHex = Hex.BytesToHex(command.AsByteArray()); - throw new SecureChannelException($"Error: received unexpected APDU {apduHex}"); - // return new ResponseApdu(Hex.HexToBytes("6a80")); - } - } - public void Setup() - { - - } - public void Cleanup() - { - - } - } - - public class RandomNumberGeneratorFixture : RandomNumberGenerator - { - private readonly byte[] bytesToGenerate = Hex.HexToBytes("360CB43F4301B894"); // host challenge - public override void GetBytes(byte[] arr) - { - if (arr is null) - { - throw new ArgumentNullException(nameof(arr)); - } - for (int i = 0; i < bytesToGenerate.Length; i++) - { - arr[i] = bytesToGenerate[i]; - } - } - } - - public class Scp03ApduTransformTests - { - private static IApduTransform GetPipeline() => new PipelineFixture(); - private static StaticKeys GetStaticKeys() => new StaticKeys(); - - [Fact] - public void Constructor_GivenNullPipeline_ThrowsArgumentNullException() - { -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => new Scp03ApduTransform(null, GetStaticKeys())); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void Constructor_GivenNullStaticKeys_ThrowsArgumentNullException() - { -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => new Scp03ApduTransform(GetPipeline(), null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void Invoke_GivenPriorSetup_CorrectlyEncodesCommand() - { - // Arrange - var pipeline = new Scp03ApduTransform(GetPipeline(), GetStaticKeys()); - using var fakeRng = new RandomNumberGeneratorFixture(); - pipeline.Setup(fakeRng); - - // Act - ResponseApdu responseApdu = pipeline.Invoke(new VersionCommand().CreateCommandApdu(), typeof(object), typeof(object)); - var versionResponse = new VersionResponse(responseApdu); - FirmwareVersion fwv = versionResponse.GetData(); - - // Assert - Assert.Equal(5, fwv.Major); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ScpApduTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ScpApduTransformTests.cs new file mode 100644 index 000000000..f3059a43d --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ScpApduTransformTests.cs @@ -0,0 +1,144 @@ +// Copyright 2023 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 Moq; +using Xunit; +using Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp; +using Yubico.YubiKey.InterIndustry.Commands; + +namespace Yubico.YubiKey.Pipelines +{ + public class ScpApduTransformTests + { + private readonly Mock _previous = new Mock(); + private readonly Scp03KeyParameters _scp03KeyParams = Scp03KeyParameters.DefaultKey; + + [Fact] + public void Constructor_NullPipeline_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new ScpApduTransform(null!, _scp03KeyParams)); + } + + [Fact] + public void Constructor_NullKeyParameters_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new ScpApduTransform(_previous.Object, null!)); + } + + [Fact] + public void Constructor_ValidParameters_CreatesInstance() + { + // Act + var transform = new ScpApduTransform(_previous.Object, _scp03KeyParams); + + // Assert + Assert.NotNull(transform); + Assert.Same(_scp03KeyParams, transform.KeyParameters); + } + + [Fact] + public void EncryptDataFunc_BeforeSetup_ThrowsInvalidOperationException() + { + // Arrange + var transform = new ScpApduTransform(_previous.Object, _scp03KeyParams); + + // Act & Assert + Assert.Throws(() => transform.EncryptDataFunc); + } + + [Fact] + public void Invoke_ExemptedCommands_BypassEncoding() + { + // Arrange + var transform = new ScpApduTransform(_previous.Object, _scp03KeyParams); + + var testCases = new[] + { + new CommandTestCase{ + Command = new SelectApplicationCommand(YubiKeyApplication.SecurityDomain), + CommandType = typeof(SelectApplicationCommand), + ResponseType = typeof(ISelectApplicationResponse) + }, + new CommandTestCase{ + Command = new Oath.Commands.SelectOathCommand(), + CommandType = typeof(Oath.Commands.SelectOathCommand), + ResponseType = typeof(Oath.Commands.SelectOathResponse) + }, + new CommandTestCase{ + Command = new Scp.Commands.ResetCommand(0x84, 0x01, 0x01, new byte[] { 0x00 }), + CommandType = typeof(Scp.Commands.ResetCommand), + ResponseType = typeof(YubiKeyResponse) + } + }; + + foreach (var testCase in testCases) + { + _previous.Setup(p => p.Invoke( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ResponseApdu(new byte[] { 0x90, 0x00 })); + + // Act + _ = transform.Invoke( + testCase.Command.CreateCommandApdu(), + testCase.CommandType, + testCase.ResponseType); + + // Assert that the previous pipeline was called (which is the one that doesn't do encoding) + _previous.Verify(p => p.Invoke( + It.IsAny(), + testCase.CommandType, + testCase.ResponseType), Times.Once); + + _previous.Reset(); + } + } + + private struct CommandTestCase + { + public IYubiKeyCommand Command; + public Type CommandType; + public Type ResponseType; + } + + [Fact] + public void Dispose_MultipleCalls_DoesNotThrow() + { + // Arrange + var transform = new ScpApduTransform(_previous.Object, _scp03KeyParams); + + // Act & Assert + transform.Dispose(); + transform.Dispose(); // Should not throw + } + + [Fact] + public void Cleanup_CallsUnderlyingPipelineCleanup() + { + // Arrange + var transform = new ScpApduTransform(_previous.Object, _scp03KeyParams); + + // Act + transform.Cleanup(); + + // Assert + _previous.Verify(p => p.Cleanup(), Times.Once); + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs similarity index 92% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs index 1e4742718..1ce7fd438 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs @@ -15,8 +15,9 @@ using System; using Xunit; using Yubico.Core.Buffers; +using Yubico.YubiKey.Scp.Helpers; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class ChannelEncryptionTests { @@ -46,7 +47,7 @@ public void EncryptData_GivenCorrectKeyPayload_ReturnsCorrectly() int ec = GetEncryptionCounter(); // Act - byte[] output = ChannelEncryption.EncryptData(payload, key, ec); + ReadOnlyMemory output = ChannelEncryption.EncryptData(payload, key, ec); // Assert Assert.Equal(GetCorrectEncryptOutput(), output); @@ -77,7 +78,7 @@ public void DecryptData_GivenCorrectKeyPayload_ReturnsCorrectly() int ec = 1; // Act - byte[] output = ChannelEncryption.DecryptData(payload, key, ec); + ReadOnlyMemory output = ChannelEncryption.DecryptData(payload, key, ec); // Assert Assert.Equal(GetCorrectDecryptedOutput(), output); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs similarity index 98% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs index ad92329f5..7efd3e9ac 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs @@ -16,8 +16,9 @@ using Xunit; using Yubico.Core.Buffers; using Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp.Helpers; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class ChannelMacTests { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/ScpResponseTests.cs similarity index 86% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/ScpResponseTests.cs index 2036ab9b8..4c011c35d 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/ScpResponseTests.cs @@ -15,16 +15,17 @@ using System; using Xunit; using Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp.Commands; -namespace Yubico.YubiKey.Scp03.Commands +namespace Yubico.YubiKey.Scp.Commands { - public class Scp03ResponseTests + public class ScpResponseTests { [Fact] public void Constructor_GivenNullResponseApdu_ThrowsArgumentNullException() { #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => new Scp03Response(null)); + _ = Assert.Throws(() => new ScpResponse(null)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } @@ -35,7 +36,7 @@ public void StatusWord_GivenResponseApdu_EqualsSWField() var responseApdu = new ResponseApdu(new byte[] { 24, 73 }); // Act - var scp03Response = new Scp03Response(responseApdu); + var scp03Response = new ScpResponse(responseApdu); // Assert Assert.Equal(responseApdu.SW, scp03Response.StatusWord); @@ -48,7 +49,7 @@ public void Status_GivenSuccessfulResponseApdu_ReturnsSuccess() var responseApdu = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); // Act - var scp03Response = new Scp03Response(responseApdu); + var scp03Response = new ScpResponse(responseApdu); // Assert Assert.Equal(ResponseStatus.Success, scp03Response.Status); @@ -61,7 +62,7 @@ public void Status_GivenFailedResponseApdu_ReturnsFailed() var responseApdu = new ResponseApdu(new byte[] { SW1Constants.CommandNotAllowed, 0x00 }); // Act - var scp03Response = new Scp03Response(responseApdu); + var scp03Response = new ScpResponse(responseApdu); // Assert Assert.Equal(ResponseStatus.Failed, scp03Response.Status); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs similarity index 92% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs index ed571e511..45aff3055 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs @@ -15,8 +15,10 @@ using System; using Xunit; using Yubico.Core.Buffers; +using Yubico.YubiKey.Scp.Helpers; + +namespace Yubico.YubiKey.Scp -namespace Yubico.YubiKey.Scp03 { public class DerivationTests { @@ -49,7 +51,7 @@ public void Derive_GivenBadKey_ThrowsArgumentException() [Fact] public void Derive_GivenCorrectVals_ReturnsCorrectHostCryptogram() { - byte[] hostCryptogram = Derivation.Derive(Derivation.DDC_HOST_CRYPTOGRAM, 0x40, GetKey(), GetHostChallenge(), GetCardChallenge()); + var hostCryptogram = Derivation.Derive(Derivation.DDC_HOST_CRYPTOGRAM, 0x40, GetKey(), GetHostChallenge(), GetCardChallenge()); Assert.Equal(GetCorrectDeriveOutput(), hostCryptogram); } } diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs new file mode 100644 index 000000000..1d2f6d2a7 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs @@ -0,0 +1,61 @@ +// 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 Xunit; + +namespace Yubico.YubiKey.Scp +{ + public class KeyReferenceTests + { + [Fact] + public void Constructor_ValidParameters_SetsProperties() + { + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + + Assert.Equal(0x13, keyReference.Id); + Assert.Equal(0x1, keyReference.VersionNumber); + } + + [Fact] + public void FactoryMethod_ValidParameters_SetsProperties() + { + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + + Assert.Equal(0x13, keyReference.Id); + Assert.Equal(0x1, keyReference.VersionNumber); + } + + [Fact] + public void FactoryMethod_InvalidParameters_ThrowsException() + { + Assert.Throws(() => KeyReference.Create(ScpKeyIds.Scp11B, 128)); + } + + [Fact] + public void Constructor_0xFF_SetsProperties() + { + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0xFF); + + Assert.Equal(0x13, keyReference.Id); + Assert.Equal(0xFF, keyReference.VersionNumber); + } + + [Fact] + public void FactoryMethod_InvalidKvn_ThrowsException() + { + Assert.Throws(() => KeyReference.Create(ScpKeyIds.Scp11B, 0xFF)); + } + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs similarity index 71% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs index 4cbb95920..77b659f19 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs @@ -15,8 +15,9 @@ using System; using Xunit; using Yubico.Core.Buffers; +using Yubico.YubiKey.Scp.Helpers; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class PaddingTests { @@ -27,22 +28,6 @@ public class PaddingTests private static byte[] Get16BytePayload() => Hex.HexToBytes("000102038005060708090A0B0C0D0E0F"); private static byte[] GetPadded16BytePayload() => Hex.HexToBytes("000102038005060708090A0B0C0D0E0F80000000000000000000000000000000"); - [Fact] - public void PadToBlockSize_GivenNullPayload_ThrowsArgumentNullException() - { -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => Padding.PadToBlockSize(null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void RemovePadding_GivenNullPayload_ThrowsArgumentNullException() - { -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => Padding.RemovePadding(null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - [Fact] public void PadToBlockSize_Given1BytePayload_ReturnsCorrectlyPaddedBlock() { @@ -50,7 +35,7 @@ public void PadToBlockSize_Given1BytePayload_ReturnsCorrectlyPaddedBlock() byte[] payload = Get1BytePayload(); // Act - byte[] paddedPayload = Padding.PadToBlockSize(payload); + Memory paddedPayload = Padding.PadToBlockSize(payload); // Assert Assert.Equal(paddedPayload, GetPadded1BytePayload()); @@ -63,7 +48,7 @@ public void PadToBlockSize_Given8BytePayload_ReturnsCorrectlyPaddedBlock() byte[] payload = Get8BytePayload(); // Act - byte[] paddedPayload = Padding.PadToBlockSize(payload); + Memory paddedPayload = Padding.PadToBlockSize(payload); // Assert Assert.Equal(paddedPayload, GetPadded8BytePayload()); @@ -76,7 +61,7 @@ public void PadToBlockSize_Given16BytePayload_ReturnsCorrectlyPaddedBlock() byte[] payload = Get16BytePayload(); // Act - byte[] paddedPayload = Padding.PadToBlockSize(payload); + Memory paddedPayload = Padding.PadToBlockSize(payload); // Assert Assert.Equal(paddedPayload, GetPadded16BytePayload()); @@ -89,7 +74,7 @@ public void RemovePadding_GivenPadded1BytePayload_ReturnsPayloadWithPaddingRemov byte[] paddedPayload = GetPadded1BytePayload(); // Act - byte[] payload = Padding.RemovePadding(paddedPayload); + Memory payload = Padding.RemovePadding(paddedPayload); // Assert Assert.Equal(payload, Get1BytePayload()); @@ -102,7 +87,7 @@ public void RemovePadding_GivenPadded8BytePayload_ReturnsPayloadWithPaddingRemov byte[] paddedPayload = GetPadded8BytePayload(); // Act - byte[] payload = Padding.RemovePadding(paddedPayload); + Memory payload = Padding.RemovePadding(paddedPayload); // Assert Assert.Equal(payload, Get8BytePayload()); @@ -115,7 +100,7 @@ public void RemovePadding_GivenPadded16BytePayload_ReturnsPayloadWithPaddingRemo byte[] paddedPayload = GetPadded16BytePayload(); // Act - byte[] payload = Padding.RemovePadding(paddedPayload); + Memory payload = Padding.RemovePadding(paddedPayload); // Assert Assert.Equal(payload, Get16BytePayload()); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs new file mode 100644 index 000000000..a10d68d91 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs @@ -0,0 +1,105 @@ +// 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.Linq; +using Xunit; + +namespace Yubico.YubiKey.Scp +{ + public class Scp03KeyParametersTests + { + [Fact] + public void DefaultKey_ReturnsValidInstance() + { + // Act + var keyParams = Scp03KeyParameters.DefaultKey; + + // Assert + Assert.NotNull(keyParams); + Assert.NotNull(keyParams.StaticKeys); + Assert.Equal(ScpKeyIds.Scp03, keyParams.KeyReference.Id); + Assert.Equal(0xFF, keyParams.KeyReference.VersionNumber); + } + + [Fact] + public void FromStaticKeys_ValidKeys_ReturnsValidInstance() + { + // Arrange + var staticKeys = new StaticKeys( + new byte[16], // enc + new byte[16], // mac + new byte[16] // dek + ); + + // Act + var keyParams = Scp03KeyParameters.FromStaticKeys(staticKeys); + + // Assert + Assert.NotNull(keyParams); + Assert.True(staticKeys.AreKeysSame(keyParams.StaticKeys)); + Assert.Equal(ScpKeyIds.Scp03, keyParams.KeyReference.Id); + Assert.Equal(0x01, keyParams.KeyReference.VersionNumber); + } + + [Fact] + public void Constructor_ValidParameters_SetsProperties() + { + // Arrange + var staticKeys = new StaticKeys( + new byte[16], // enc + new byte[16], // mac + new byte[16] // dek + ); + const int keyId = ScpKeyIds.Scp03; + const int kvn = 0x02; + + // Act + var keyParams = new Scp03KeyParameters(keyId, kvn, staticKeys); + + // Assert + Assert.NotNull(keyParams); + Assert.True(staticKeys.AreKeysSame(keyParams.StaticKeys)); + Assert.Equal(keyId, keyParams.KeyReference.Id); + Assert.Equal(kvn, keyParams.KeyReference.VersionNumber); + } + + [Fact] + public void Dispose_DisposesStaticKeys() + { + // Arrange + var staticKeys = new StaticKeys( + new byte[16], // enc + new byte[16], // mac + new byte[16] // dek + ); + + var keyParams = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); + + // Act + keyParams.Dispose(); + Assert.True(keyParams.StaticKeys.DataEncryptionKey.ToArray().All(b => b == 0)); + } + + [Fact] + public void Dispose_MultipleCalls_DoesNotThrow() + { + // Arrange + var keyParams = Scp03KeyParameters.DefaultKey; + + // Act & Assert - Should not throw + keyParams.Dispose(); + keyParams.Dispose(); + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03StateTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03StateTests.cs new file mode 100644 index 000000000..f582f6922 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03StateTests.cs @@ -0,0 +1,146 @@ +using System; +using Moq; +using Xunit; +using Yubico.Core.Iso7816; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp.Commands; +using Yubico.YubiKey.Scp.Helpers; + +namespace Yubico.YubiKey.Scp +{ + public class Scp03StateTests + { + readonly byte[] ResponseData; + + internal Scp03State State { get; set; } + + public Scp03StateTests() + { + var parent = new Mock(); + var keyParams = Scp03KeyParameters.DefaultKey; + var hostChallenge = new byte[8]; + var cardChallenge = new byte[8]; + + ResponseData = GetFakeResponseApduData(hostChallenge, cardChallenge); + + parent.Setup(p => p.Invoke( + It.IsAny(), + typeof(InitializeUpdateCommand), + typeof(InitializeUpdateResponse))) + .Returns(new ResponseApdu(ResponseData)); + + + parent.Setup(p => p.Invoke( + It.IsAny(), + typeof(ExternalAuthenticateCommand), + typeof(ExternalAuthenticateResponse))) + .Returns(new ResponseApdu(ResponseData)); + + // Act + State = Scp03State.CreateScpState(parent.Object, keyParams, hostChallenge); + } + + [Fact] + public void CreateScpState_ValidParameters_InitializesCorrectly() + { + // Arrange + var parent = new Mock(); + var keyParams = Scp03KeyParameters.DefaultKey; + var hostChallenge = new byte[8]; + var cardChallenge = new byte[8]; + + var responseApduData = GetFakeResponseApduData(hostChallenge, cardChallenge); + + parent.Setup(p => p.Invoke( + It.IsAny(), + typeof(InitializeUpdateCommand), + typeof(InitializeUpdateResponse))) + .Returns(new ResponseApdu(responseApduData)); + + + parent.Setup(p => p.Invoke( + It.IsAny(), + typeof(ExternalAuthenticateCommand), + typeof(ExternalAuthenticateResponse))) + .Returns(new ResponseApdu(responseApduData)); + + // Act + var state = Scp03State.CreateScpState(parent.Object, keyParams, hostChallenge); + + // Assert + Assert.NotNull(state); + Assert.NotNull(state.GetDataEncryptor()); + } + + [Fact] + public void CreateScpState_NullPipeline_ThrowsArgumentNullException() + { + // Arrange + var keyParams = Scp03KeyParameters.DefaultKey; + byte[] hostChallenge = new byte[8]; + + // Act & Assert + Assert.Throws(() => + Scp03State.CreateScpState(null!, keyParams, hostChallenge)); + } + + [Fact] + public void CreateScpState_NullKeyParams_ThrowsArgumentNullException() + { + // Arrange + var pipeline = new Mock(); + byte[] hostChallenge = new byte[8]; + + // Act & Assert + Assert.Throws(() => + Scp03State.CreateScpState(pipeline.Object, null!, hostChallenge)); + } + + [Fact] + public void EncodeCommand_ValidCommand_ReturnsEncodedApdu() + { + // Arrange + using var rng = CryptographyProviders.RngCreator(); + Span putKeyData = stackalloc byte[256]; + rng.GetBytes(putKeyData); + var originalCommand = new PutKeyCommand(1, 2, putKeyData.ToArray()).CreateCommandApdu(); + + // Act + var encodedCommand = State.EncodeCommand(originalCommand); + + // Assert + Assert.NotNull(encodedCommand); + Assert.NotEqual(originalCommand.Data, encodedCommand.Data); + } + + private static byte[] GetFakeResponseApduData( + byte[] hostChallenge, + byte[] cardChallenge) + { + Array.Fill(hostChallenge, (byte)1); + Array.Fill(cardChallenge, (byte)1); + + // Derive session keys + var sessionKeys = Derivation.DeriveSessionKeysFromStaticKeys( + Scp03KeyParameters.DefaultKey.StaticKeys, + hostChallenge, + cardChallenge); + + // Check supplied card cryptogram + var calculatedCardCryptogram = Derivation.DeriveCryptogram( + Derivation.DDC_CARD_CRYPTOGRAM, + sessionKeys.MacKey.Span, + hostChallenge, + cardChallenge); + + var responseApduData = new byte[31]; + Array.Fill(responseApduData, (byte)1); + responseApduData[^2] = 0x90; + responseApduData[^1] = 0x00; + + // Add fake Card Crypto response + calculatedCardCryptogram.Span.CopyTo(responseApduData.AsSpan(21..29)); + return responseApduData; + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp11KeyParametersTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp11KeyParametersTests.cs new file mode 100644 index 000000000..3ac6c3a48 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp11KeyParametersTests.cs @@ -0,0 +1,233 @@ +// 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.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Xunit; +using Yubico.YubiKey.Cryptography; + +namespace Yubico.YubiKey.Scp +{ + public class Scp11KeyParametersTests : IDisposable + { + private readonly ECCurve _curve = ECCurve.NamedCurves.nistP256; + private readonly ECParameters _sdPublicKeyParams; + private readonly ECParameters _ocePrivateKeyParams; + private readonly X509Certificate2[] _oceCertificates; + + public Scp11KeyParametersTests() + { + using var sdKeyEcdsa = ECDsa.Create(_curve); + _sdPublicKeyParams = sdKeyEcdsa.ExportParameters(false); + + using var oceKeyEcdsa = ECDsa.Create(_curve); + _ocePrivateKeyParams = oceKeyEcdsa.ExportParameters(true); + + // Create a self-signed cert for testing + var req = new CertificateRequest( + "CN=Test", + oceKeyEcdsa, + HashAlgorithmName.SHA256); + + _oceCertificates = new[] + { + req.CreateSelfSigned( + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddYears(1)) + }; + } + + [Fact] + public void Constructor_Scp11b_ValidParameters_Succeeds() + { + // Arrange + var keyRef = new KeyReference(ScpKeyIds.Scp11B, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + + // Act + var keyParams = new Scp11KeyParameters(keyRef, pkSdEcka); + + // Assert + Assert.NotNull(keyParams); + Assert.Equal(0x13, keyParams.KeyReference.Id); // ScpKeyIds.Scp11B + Assert.Equal(0x01, keyParams.KeyReference.VersionNumber); + Assert.NotNull(keyParams.PkSdEcka); + Assert.Null(keyParams.OceKeyReference); + Assert.Null(keyParams.SkOceEcka); + Assert.Null(keyParams.OceCertificates); + } + + [Theory] + [InlineData(ScpKeyIds.Scp11A)] + [InlineData(ScpKeyIds.Scp11C)] + public void Constructor_Scp11ac_ValidParameters_Succeeds( + byte keyId) + { + // Arrange + var keyRef = new KeyReference(keyId, 0x01); + var oceKeyRef = new KeyReference(0x01, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + var skOceEcka = new ECPrivateKeyParameters(_ocePrivateKeyParams); + + // Act + var keyParams = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _oceCertificates); + + // Assert + Assert.NotNull(keyParams); + Assert.Equal(keyId, keyParams.KeyReference.Id); + Assert.Equal(0x01, keyParams.KeyReference.VersionNumber); + Assert.NotNull(keyParams.PkSdEcka); + Assert.NotNull(keyParams.OceKeyReference); + Assert.NotNull(keyParams.SkOceEcka); + Assert.NotNull(keyParams.OceCertificates); + Assert.Single(keyParams.OceCertificates); + } + + [Theory] + [InlineData(ScpKeyIds.Scp11A)] + [InlineData(ScpKeyIds.Scp11C)] + public void Constructor_Scp11ac_InvalidParameters_ThrowsArgumentException( + byte keyId) + { + // Arrange + var keyRef = new KeyReference(keyId, 0x01); + var oceKeyRef = new KeyReference(0x01, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + var skOceEcka = new ECPrivateKeyParameters(_ocePrivateKeyParams); + + // Act + Assert.Throws(() => + { + _ = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + new X509Certificate2[] { } // Empty list invalid + ); + }); + } + + [Fact] + public void Constructor_Scp11b_WithOptionalParams_ThrowsArgumentException() + { + // Arrange + var keyRef = + new KeyReference(ScpKeyIds.Scp11B, 0x01); // SCP11b and these optional parameters should not work + var oceKeyRef = new KeyReference(0x01, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + var skOceEcka = new ECPrivateKeyParameters(_ocePrivateKeyParams); + + // Act & Assert + Assert.Throws(() => new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _oceCertificates)); + } + + [Theory] + [InlineData(ScpKeyIds.Scp11A)] + [InlineData(ScpKeyIds.Scp11C)] + public void Constructor_Scp11ac_MissingParams_ThrowsArgumentException( + byte keyId) + { + // Arrange + var keyRef = new KeyReference(keyId, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + + // Act & Assert + Assert.Throws(() => new Scp11KeyParameters( + keyRef, + pkSdEcka)); + } + + [Theory] + [InlineData(0x00)] // Invalid key ID + [InlineData(0x10)] // Invalid key ID + [InlineData(0xFF)] // Invalid key ID + public void Constructor_InvalidKeyId_ThrowsArgumentException( + byte keyId) + { + // Arrange + var keyRef = new KeyReference(keyId, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + + // Act & Assert + Assert.Throws(() => new Scp11KeyParameters( + keyRef, + pkSdEcka)); + } + + [Fact] + public void Dispose_ClearsPrivateKey() + { + // Arrange + var keyRef = new KeyReference(ScpKeyIds.Scp11A, 0x01); + var oceKeyRef = new KeyReference(0x01, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + var skOceEcka = new ECPrivateKeyParameters(_ocePrivateKeyParams); + + var keyParams = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _oceCertificates); + + // Act + keyParams.Dispose(); + + // Assert + Assert.Null(keyParams.SkOceEcka); + Assert.Null(keyParams.OceKeyReference); + } + + [Fact] + public void Dispose_MultipleCalls_DoesNotThrow() + { + // Arrange + var keyRef = new KeyReference(ScpKeyIds.Scp11A, 0x01); + var oceKeyRef = new KeyReference(0x01, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdPublicKeyParams); + var skOceEcka = new ECPrivateKeyParameters(_ocePrivateKeyParams); + + var keyParams = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _oceCertificates); + + // Act & Assert - Should not throw + keyParams.Dispose(); + keyParams.Dispose(); + } + + public void Dispose() + { + foreach (var cert in _oceCertificates) + { + cert.Dispose(); + } + } + } +} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/StaticKeysTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/StaticKeysTests.cs similarity index 98% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/StaticKeysTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/StaticKeysTests.cs index 10a4c0b2b..d2d668545 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/StaticKeysTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/StaticKeysTests.cs @@ -15,7 +15,7 @@ using System; using Xunit; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class StaticKeysTests { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs deleted file mode 100644 index 9209b227f..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// 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. - -using System; -using Xunit; -using Yubico.Core.Iso7816; - -namespace Yubico.YubiKey.Scp03.Commands -{ - public class ExternalAuthenticateCommandTests - { - [Fact] - public void CreateCommandApdu_GetClaProperty_ReturnsHex84() - { - Assert.Equal(0x84, GetExternalAuthenticateCommandApdu().Cla); - } - - [Fact] - public void CreateCommandApdu_GetInsProperty_ReturnsHex82() - { - Assert.Equal(0x82, GetExternalAuthenticateCommandApdu().Ins); - } - - [Fact] - public void CreateCommandApdu_GetP1Property_ReturnsHex33() - { - Assert.Equal(0x33, GetExternalAuthenticateCommandApdu().P1); - } - - [Fact] - public void CreateCommandApdu_GetP2Property_ReturnsZero() - { - Assert.Equal(0, GetExternalAuthenticateCommandApdu().P2); - } - - [Fact] - public void CreateCommandApdu_GetData_ReturnsData() - { - ReadOnlyMemory data = GetExternalAuthenticateCommandApdu().Data; - Assert.True(data.Span.SequenceEqual(GetData())); - } - - [Fact] - public void CreateCommandApdu_GetNc_ReturnsDataLength() - { - Assert.Equal(GetData().Length, GetExternalAuthenticateCommandApdu().Nc); - } - - [Fact] - public void CreateCommandApdu_GetNe_ReturnsZero() - { - Assert.Equal(0, GetExternalAuthenticateCommandApdu().Ne); - } - - private static byte[] GetData() - { - return new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; - } - private static ExternalAuthenticateCommand GetExternalAuthenticateCommand() - { - return new ExternalAuthenticateCommand(GetData()); - } - private static CommandApdu GetExternalAuthenticateCommandApdu() - { - return GetExternalAuthenticateCommand().CreateCommandApdu(); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs deleted file mode 100644 index 67d8b5d67..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// 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. - -using System; -using Xunit; -using Yubico.Core.Iso7816; - -namespace Yubico.YubiKey.Scp03.Commands -{ - public class ExternalAuthenticateResponseTests - { - public static ResponseApdu GetResponseApdu() - { - return new ResponseApdu(new byte[] { 0x90, 0x00 }); - } - - [Fact] - public void Constructor_GivenNullResponseApdu_ThrowsArgumentNullExceptionFromBase() - { -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => new ExternalAuthenticateResponse(null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void Constructor_GivenResponseApdu_SetsStatusWordCorrectly() - { - // Arrange - ResponseApdu responseApdu = GetResponseApdu(); - - // Act - var externalAuthenticateResponse = new ExternalAuthenticateResponse(responseApdu); - - // Assert - Assert.Equal(SWConstants.Success, externalAuthenticateResponse.StatusWord); - } - - [Fact] - public void ExternalAuthenticateResponse_GivenResponseApduWithData_ThrowsArgumentException() - { - var badResponseApdu = new ResponseApdu(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x90, 0x00 }); - - _ = Assert.Throws(() => new ExternalAuthenticateResponse(badResponseApdu)); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs deleted file mode 100644 index 04e3b31fc..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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. - -using System; -using Xunit; -using Yubico.Core.Iso7816; - -namespace Yubico.YubiKey.Scp03.Commands -{ - public class InitializeUpdateCommandTests - { - [Fact] - public void CreateCommandApdu_GetClaProperty_ReturnsHex80() - { - Assert.Equal(0x80, GetInitializeUpdateCommandApdu().Cla); - } - - [Fact] - public void CreateCommandApdu_GetInsProperty_ReturnsHex50() - { - Assert.Equal(0x50, GetInitializeUpdateCommandApdu().Ins); - } - - [Fact] - public void CreateCommandApdu_GetP1Property_ReturnsZero() - { - Assert.Equal(0, GetInitializeUpdateCommandApdu().P1); - } - - [Fact] - public void CreateCommandApdu_GetP2Property_ReturnsZero() - { - Assert.Equal(0, GetInitializeUpdateCommandApdu().P2); - } - - [Fact] - public void CreateCommandApdu_GetData_ReturnsChallenge() - { - CommandApdu commandApdu = GetInitializeUpdateCommandApdu(); - byte[] challenge = GetChallenge(); - - Assert.False(commandApdu.Data.IsEmpty); - Assert.True(commandApdu.Data.Span.SequenceEqual(challenge)); - } - - [Fact] - public void CreateCommandApdu_GetNc_Returns8() - { - Assert.Equal(8, GetInitializeUpdateCommandApdu().Nc); - } - - [Fact] - public void CreateCommandApdu_GetNe_ReturnsZero() - { - Assert.Equal(0, GetInitializeUpdateCommandApdu().Ne); - } - - private static byte[] GetChallenge() - { - return new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; - } - private static InitializeUpdateCommand GetInitializeUpdateCommand() - { - return new InitializeUpdateCommand(0, GetChallenge()); - } - private static CommandApdu GetInitializeUpdateCommandApdu() - { - return GetInitializeUpdateCommand().CreateCommandApdu(); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs deleted file mode 100644 index d85a9f18f..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using Xunit; -using Yubico.Core.Iso7816; - -namespace Yubico.YubiKey.Scp03.Commands -{ - public class InitializeUpdateResponseTests - { - public static ResponseApdu GetResponseApdu() - { - return new ResponseApdu(new byte[] { - 1, 2, 3, 4, 5, 6, 7, 8, - 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, - 0x90, 0x00 - }); - } - - private static IReadOnlyCollection GetDiversificationData() => GetResponseApdu().Data.Slice(0, 10).ToArray(); - private static IReadOnlyCollection GetKeyInfo() => GetResponseApdu().Data.Slice(10, 3).ToArray(); - private static IReadOnlyCollection GetCardChallenge() => GetResponseApdu().Data.Slice(13, 8).ToArray(); - private static IReadOnlyCollection GetCardCryptogram() => GetResponseApdu().Data.Slice(21, 8).ToArray(); - - [Fact] - public void Constructor_GivenNullResponseApdu_ThrowsArgumentNullExceptionFromBase() - { -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => new InitializeUpdateResponse(null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void Constructor_GivenResponseApdu_SetsStatusWordCorrectly() - { - // Arrange - ResponseApdu responseApdu = GetResponseApdu(); - - // Act - var initializeUpdateResponse = new InitializeUpdateResponse(responseApdu); - - // Assert - Assert.Equal(SWConstants.Success, initializeUpdateResponse.StatusWord); - } - - [Fact] - public void InitializeUpdateResponse_GivenResponseApdu_DiversificationDataEqualsBytes0To10() - { - // Arrange - ResponseApdu responseApdu = GetResponseApdu(); - - // Act - var initializeUpdateResponse = new InitializeUpdateResponse(responseApdu); - - // Assert - Assert.Equal(GetDiversificationData(), initializeUpdateResponse.DiversificationData); - } - - [Fact] - public void InitializeUpdateResponse_GivenResponseApdu_KeyInfoEqualsBytes10To13() - { - // Arrange - ResponseApdu responseApdu = GetResponseApdu(); - - // Act - var initializeUpdateResponse = new InitializeUpdateResponse(responseApdu); - - // Assert - Assert.Equal(GetKeyInfo(), initializeUpdateResponse.KeyInfo); - } - [Fact] - public void InitializeUpdateResponse_GivenResponseApdu_CardChallengeEqualsBytes13To21() - { - // Arrange - ResponseApdu responseApdu = GetResponseApdu(); - - // Act - var initializeUpdateResponse = new InitializeUpdateResponse(responseApdu); - - // Assert - Assert.Equal(GetCardChallenge(), initializeUpdateResponse.CardChallenge); - } - [Fact] - public void InitializeUpdateResponse_GivenResponseApdu_CardCryptogramEqualsBytes21To29() - { - // Arrange - ResponseApdu responseApdu = GetResponseApdu(); - - // Act - var initializeUpdateResponse = new InitializeUpdateResponse(responseApdu); - - // Assert - Assert.Equal(GetCardCryptogram(), initializeUpdateResponse.CardCryptogram); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs deleted file mode 100644 index 2f131cf47..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// 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. - -using System; -using Xunit; -using Yubico.Core.Buffers; -using Yubico.Core.Iso7816; -using Yubico.YubiKey.Scp03.Commands; - -namespace Yubico.YubiKey.Scp03 -{ - public class SessionTests - { - private static byte[] GetChallenge() => Hex.HexToBytes("360CB43F4301B894"); - private static byte[] GetCorrectInitializeUpdate() => Hex.HexToBytes("8050000008360CB43F4301B894"); - private static byte[] GetInitializeUpdateResponse() => Hex.HexToBytes("010B001F002500000000FF0360CAAFA4DAC615236ADD5607216F3E115C9000"); - private static byte[] GetCorrectExternalAuthenticate() => Hex.HexToBytes("848233001045330AB30BB1A079A8E7F77376DB9F2C"); - - private static StaticKeys GetStaticKeys() - { - return new StaticKeys(); - } - - private static Session GetSession() - { - return new Session(); - } - - [Fact] - public void Constructor_GivenStaticKeys_Succeeds() - { - _ = GetSession(); - } - - [Fact] - public void BuildInitializeUpdate_GivenNullHostChallenge_ThrowsArgumentNullException() - { - Session sess = GetSession(); -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => sess.BuildInitializeUpdate(0, null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void BuildInitializeUpdate_GivenHostChallengeWrongLength_ThrowsArgumentException() - { - byte[] hostChallengeWrongLength = new byte[9]; - Session sess = GetSession(); - _ = Assert.Throws(() => sess.BuildInitializeUpdate(0, hostChallengeWrongLength)); - } - - [Fact] - public void BuildInitializeUpdate_GivenHostChallenge_BuildsCorrectInitializeUpdate() - { - // Arrange - Session sess = GetSession(); - - // Act - InitializeUpdateCommand initializeUpdateCommand = sess.BuildInitializeUpdate(0, GetChallenge()); - byte[] initializeUpdateCommandBytes = initializeUpdateCommand.CreateCommandApdu().AsByteArray(); - - // Assert - Assert.Equal(initializeUpdateCommandBytes, GetCorrectInitializeUpdate()); - } - - [Fact] - public void LoadInitializeUpdate_GivenNullInitializeUpdateResponse_ThrowsArgumentNullException() - { - Session sess = GetSession(); - InitializeUpdateCommand initializeUpdateCommand = sess.BuildInitializeUpdate(0, GetChallenge()); -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => sess.LoadInitializeUpdateResponse(null, GetStaticKeys())); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void LoadInitializeUpdate_GivenNullStaticKeys_ThrowsArgumentNullException() - { - Session sess = GetSession(); - InitializeUpdateCommand initializeUpdateCommand = sess.BuildInitializeUpdate(0, GetChallenge()); - var correctResponse = new InitializeUpdateResponse(new ResponseApdu(GetInitializeUpdateResponse())); -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => sess.LoadInitializeUpdateResponse(correctResponse, null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void LoadInitializeUpdate_CalledBeforeBuildInitializeUpdate_ThrowsInvalidOperationException() - { - Session sess = GetSession(); - var correctResponse = new InitializeUpdateResponse(new ResponseApdu(GetInitializeUpdateResponse())); -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _ = Assert.Throws(() => sess.LoadInitializeUpdateResponse(correctResponse, GetStaticKeys())); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } - - [Fact] - public void Session_GivenInitializeUpdateResponse_BuildsCorrectExternalAuthenticate() - { - // Arrange - Session sess = GetSession(); - InitializeUpdateCommand initializeUpdateCommand = sess.BuildInitializeUpdate(0, GetChallenge()); - sess.LoadInitializeUpdateResponse(initializeUpdateCommand.CreateResponseForApdu(new ResponseApdu(GetInitializeUpdateResponse())), GetStaticKeys()); - - // Act - ExternalAuthenticateCommand externalAuthenticateCommand = sess.BuildExternalAuthenticate(); - byte[] externalAuthenticateCommandBytes = externalAuthenticateCommand.CreateCommandApdu().AsByteArray(); - - // Assert - Assert.Equal(externalAuthenticateCommandBytes, GetCorrectExternalAuthenticate()); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubiKeyDeviceExtensionsTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubiKeyDeviceExtensionsTests.cs deleted file mode 100644 index 544dc9680..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubiKeyDeviceExtensionsTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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. - -using System; -using Moq; -using Xunit; -using Yubico.Core.Devices.SmartCard; -using Yubico.YubiKey.Scp03; - -namespace Yubico.YubiKey -{ -#pragma warning disable CS0618 // Specifically testing this soon-to-be-deprecated feature - public class YubiKeyDeviceExtensionsTests - { - [Fact] - public void WithScp03_WhenDeviceIsNull_Throws() - { - YubiKeyDevice? ykDevice = null; - _ = Assert.Throws(() => ykDevice!.WithScp03(new StaticKeys())); - } - - [Fact] - public void WithScp03_WhenDeviceDoesNotHaveSmartCard_ThrowsException() - { - var ykDeviceInfo = new YubiKeyDeviceInfo() - { - AvailableUsbCapabilities = YubiKeyCapabilities.All, - EnabledUsbCapabilities = YubiKeyCapabilities.All, - }; - - var ykDevice = new YubiKeyDevice(null, null, null, ykDeviceInfo); - _ = Assert.Throws(() => ykDevice.WithScp03(new StaticKeys())); - } - - [Fact] - public void WithScp03_WhenDeviceCorrect_Succeeds() - { - var mockSmartCard = new Mock(); - var ykDeviceInfo = new YubiKeyDeviceInfo() - { - AvailableUsbCapabilities = YubiKeyCapabilities.All, - EnabledUsbCapabilities = YubiKeyCapabilities.All, - }; - - var ykDevice = new YubiKeyDevice(mockSmartCard.Object, null, null, ykDeviceInfo); - IYubiKeyDevice scp03Device = ykDevice.WithScp03(new StaticKeys()); - _ = Assert.IsAssignableFrom(scp03Device); - } - } -#pragma warning restore CS0618 -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs new file mode 100644 index 000000000..f71e42120 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs @@ -0,0 +1,68 @@ +// 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 Xunit; + +namespace Yubico.YubiKey +{ + public class YubiKeyApplicationExtensionsTests + { + [Theory] + [InlineData(YubiKeyApplication.Management, new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 })] + [InlineData(YubiKeyApplication.Otp, new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01 })] + [InlineData(YubiKeyApplication.FidoU2f, new byte[] { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 })] + [InlineData(YubiKeyApplication.Piv, new byte[] { 0xa0, 0x00, 0x00, 0x03, 0x08 })] + public void GetIso7816ApplicationId_ReturnsCorrectId(YubiKeyApplication application, byte[] expectedId) + { + byte[] result = application.GetIso7816ApplicationId(); + Assert.Equal(expectedId, result); + } + + [Fact] + public void GetIso7816ApplicationId_ThrowsForUnsupportedApplication() + { + var unsupportedApp = (YubiKeyApplication)999; + Assert.Throws(() => unsupportedApp.GetIso7816ApplicationId()); + } + + [Fact] + public void ApplicationIds_ContainsAllApplications() + { + var result = YubiKeyApplicationExtensions.Iso7816ApplicationIds; + Assert.Equal(11, result.Count); + Assert.Contains(YubiKeyApplication.Management, result.Keys); + Assert.Contains(YubiKeyApplication.Otp, result.Keys); + Assert.Contains(YubiKeyApplication.FidoU2f, result.Keys); + // Add assertions for other applications + } + + [Theory] + [InlineData(new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 }, YubiKeyApplication.Management)] + [InlineData(new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01 }, YubiKeyApplication.Otp)] + [InlineData(new byte[] { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }, YubiKeyApplication.FidoU2f)] + public void GetById_ReturnsCorrectApplication(byte[] applicationId, YubiKeyApplication expectedApplication) + { + var result = YubiKeyApplicationExtensions.GetYubiKeyApplication(applicationId); + Assert.Equal(expectedApplication, result); + } + + [Fact] + public void GetById_ThrowsForUnknownApplicationId() + { + byte[] unknownId = new byte[] { 0x00, 0x11, 0x22, 0x33 }; + Assert.Throws(() => YubiKeyApplicationExtensions.GetYubiKeyApplication(unknownId)); + } + } +} diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs index 6dfb33f44..e3c8f741e 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs @@ -14,7 +14,8 @@ using System; using Yubico.Core.Devices; -using Yubico.YubiKey.Scp03; +using Yubico.YubiKey.Scp; + namespace Yubico.YubiKey.TestUtilities { @@ -141,9 +142,9 @@ public HollowYubiKeyDevice(bool alwaysAuthenticatePiv = false) EnabledUsbCapabilities = 0; } - public IYubiKeyConnection Connect(YubiKeyApplication yubikeyApplication) + public IYubiKeyConnection Connect(YubiKeyApplication yubiKeyApplication) { - var connection = new HollowConnection(yubikeyApplication, FirmwareVersion) + var connection = new HollowConnection(yubiKeyApplication, FirmwareVersion) { AlwaysAuthenticatePiv = _alwaysAuthenticatePiv, }; @@ -151,57 +152,87 @@ public IYubiKeyConnection Connect(YubiKeyApplication yubikeyApplication) return connection; } - public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys) + public IYubiKeyConnection Connect(byte[] applicationId) { throw new NotImplementedException(); } - public IYubiKeyConnection Connect(byte[] applicationId) + [Obsolete("Obsolete")] + public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) { throw new NotImplementedException(); } - public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) + [Obsolete("Obsolete")] + public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) { throw new NotImplementedException(); } - /// - bool IYubiKeyDevice.HasSameParentDevice(IDevice other) + public IScpYubiKeyConnection Connect(YubiKeyApplication yubikeyApplication, ScpKeyParameters scp03Keys) { - return false; + throw new NotImplementedException(); + } + + public IScpYubiKeyConnection Connect(byte[] applicationId, ScpKeyParameters scp03Keys) + { + throw new NotImplementedException(); } - bool IYubiKeyDevice.TryConnect( + public bool TryConnect( YubiKeyApplication application, out IYubiKeyConnection connection) { throw new NotImplementedException(); } - bool IYubiKeyDevice.TryConnectScp03( + public bool TryConnect( + byte[] applicationId, + out IYubiKeyConnection connection) + { + throw new NotImplementedException(); + } + + [Obsolete("Obsolete")] + public bool TryConnectScp03( YubiKeyApplication application, - StaticKeys scp03Keys, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, out IScp03YubiKeyConnection connection) { throw new NotImplementedException(); } - bool IYubiKeyDevice.TryConnect( + [Obsolete("Obsolete")] + public bool TryConnectScp03( byte[] applicationId, - out IYubiKeyConnection connection) + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, + out IScp03YubiKeyConnection connection) { throw new NotImplementedException(); } - bool IYubiKeyDevice.TryConnectScp03( + public bool TryConnect( + YubiKeyApplication application, + ScpKeyParameters keyParameters, + out IScpYubiKeyConnection connection) + { + throw new NotImplementedException(); + } + + public bool TryConnect( byte[] applicationId, - StaticKeys scp03Keys, - out IScp03YubiKeyConnection connection) + ScpKeyParameters keyParameters, + out IScpYubiKeyConnection connection) { throw new NotImplementedException(); } + /// + bool IYubiKeyDevice.HasSameParentDevice(IDevice other) + { + return false; + } + public void SetEnabledNfcCapabilities(YubiKeyCapabilities yubiKeyCapabilities) => throw new NotImplementedException(); @@ -246,6 +277,16 @@ public void UnlockConfiguration(ReadOnlySpan lockCode) throw new NotImplementedException(); } + public void SetLegacyDeviceConfiguration( + YubiKeyCapabilities yubiKeyInterfaces, + byte challengeResponseTimeout, + bool touchEjectEnabled, + int autoEjectTimeout = 0, + ScpKeyParameters? keyParameters = null) + { + throw new NotImplementedException(); + } + public void SetLegacyDeviceConfiguration( YubiKeyCapabilities yubiKeyInterfaces, byte challengeResponseTimeout, bool touchEjectEnabled, int autoEjectTimeout = 0) diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs index 8da5e7d69..29a070a0d 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs @@ -50,7 +50,8 @@ private static readonly Lazy SingleInstance private const string YubikeyIntegrationtestAllowedKeysName = "YUBIKEY_INTEGRATIONTEST_ALLOWEDKEYS"; private readonly string _allowlistFileName = $"{YubikeyIntegrationtestAllowedKeysName}.txt"; - public IntegrationTestDeviceEnumeration(string? configDirectory = null) + public IntegrationTestDeviceEnumeration( + string? configDirectory = null) { var defaultDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Yubico"); @@ -89,21 +90,24 @@ public IntegrationTestDeviceEnumeration(string? configDirectory = null) /// /// /// - public static IYubiKeyDevice GetBySerial(int serialNumber) + public static IYubiKeyDevice GetBySerial( + int serialNumber) => GetTestDevices().Single(d => d.SerialNumber == serialNumber); /// /// Enumerates all YubiKey test devices on a system. /// /// The allow-list filtered list of available Yubikeys - public static IList GetTestDevices(Transport transport = Transport.All) + public static IList GetTestDevices( + Transport transport = Transport.All) { return YubiKeyDevice .FindByTransport(transport) .Where(IsAllowedKey) .ToList(); - static bool IsAllowedKey(IYubiKeyDevice key) + static bool IsAllowedKey( + IYubiKeyDevice key) => key.SerialNumber == null || Instance.AllowedSerialNumbers.Contains(key.SerialNumber.Value.ToString()); } @@ -113,12 +117,15 @@ static bool IsAllowedKey(IYubiKeyDevice key) /// /// The type of the device. /// The transport the device must support. + /// The earliest version number the + /// caller is willing to accept. Defaults to the minimum version for the given device. /// The allow-list filtered YubiKey that was found. public static IYubiKeyDevice GetTestDevice( StandardTestDevice testDeviceType = StandardTestDevice.Fw5, - Transport transport = Transport.All) + Transport transport = Transport.All, + FirmwareVersion? minimumFirmwareVersion = null) => GetTestDevices(transport) - .SelectByStandardTestDevice(testDeviceType); + .SelectByStandardTestDevice(testDeviceType, minimumFirmwareVersion, transport); /// /// Get YubiKey test device of specified transport and for which the @@ -128,12 +135,14 @@ public static IYubiKeyDevice GetTestDevice( /// The earliest version number the /// caller is willing to accept. /// The allow-list filtered YubiKey that was found. - public static IYubiKeyDevice GetTestDevice(Transport transport, FirmwareVersion minimumFirmwareVersion) + public static IYubiKeyDevice GetTestDevice( + Transport transport, + FirmwareVersion minimumFirmwareVersion) => GetTestDevices(transport) .SelectByMinimumVersion(minimumFirmwareVersion); - - private static void CreateAllowListFileIfMissing(string allowListFilePath) + private static void CreateAllowListFileIfMissing( + string allowListFilePath) { if (File.Exists(allowListFilePath)) { diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/StandardTestDevice.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/StandardTestDevice.cs index c8fb1e939..7253f9e1b 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/StandardTestDevice.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/StandardTestDevice.cs @@ -36,16 +36,6 @@ public enum StandardTestDevice /// Fw5Fips, - /// - /// Major version 5, USB C Lightning, not FIPS - /// - Fw5Ci, - - /// - /// Major version 5, USB C Keychain - /// - Fw5C, - /// /// Major version 5, USB A biometric keychain, not FIPS /// diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs index 09662c0ea..697f71391 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using TestDev = Yubico.YubiKey.TestUtilities.IntegrationTestDeviceEnumeration; @@ -27,15 +28,16 @@ public static class TestDeviceSelection /// /// Thrown if the test device could not be found. /// - public static IYubiKeyDevice RenewDeviceEnumeration(int serialNumber) + public static IYubiKeyDevice RenewDeviceEnumeration( + int serialNumber) { const int maxReconnectAttempts = 40; - const int sleepDuration = 100; //ms + var sleepDurationMs = TimeSpan.FromMilliseconds(200); int reconnectAttempts = 0; do { - System.Threading.Thread.Sleep(sleepDuration); + System.Threading.Thread.Sleep(sleepDurationMs); try { @@ -54,52 +56,85 @@ public static IYubiKeyDevice RenewDeviceEnumeration(int serialNumber) /// Retrieves a single based on test device requirements. /// /// - /// Thrown when is not a recognized value. + /// Thrown when is not a recognized value. /// /// /// Thrown when the input sequence did not contain a valid test device. /// + /// The type of the device. + /// The earliest version number the + /// caller is willing to accept. Defaults to the minimum version for the given device. + /// The list of yubikeys to select from + /// The desired transport + /// The allow-list filtered YubiKey that was found. public static IYubiKeyDevice SelectByStandardTestDevice( this IEnumerable yubiKeys, - StandardTestDevice testDevice) + StandardTestDevice testDeviceType, + FirmwareVersion? minimumFirmwareVersion = null, + Transport transport = Transport.All + ) { var devices = yubiKeys as IYubiKeyDevice[] ?? yubiKeys.ToArray(); if (!devices.Any()) { - throw new InvalidOperationException("Could not find any connected Yubikeys"); + ThrowDeviceNotFoundException($"Could not find any connected Yubikeys (Transport: {transport})", devices); } - return testDevice switch + var devicesVersionFiltered = + devices.Where(d => d.FirmwareVersion >= MatchVersion(testDeviceType, minimumFirmwareVersion)); + + return testDeviceType switch { StandardTestDevice.Fw3 => SelectDevice(3), StandardTestDevice.Fw4Fips => SelectDevice(4, isFipsSeries: true), StandardTestDevice.Fw5 => SelectDevice(5), - StandardTestDevice.Fw5Fips => SelectDevice(5, formFactor: FormFactor.UsbAKeychain, isFipsSeries: true), + StandardTestDevice.Fw5Fips => SelectDevice(5, isFipsSeries: true), StandardTestDevice.Fw5Bio => SelectDevice(5, formFactor: FormFactor.UsbABiometricKeychain), - _ => throw new ArgumentException("Invalid test device value.", nameof(testDevice)), + _ => throw new ArgumentException("Invalid test device value.", nameof(testDeviceType)), }; - IYubiKeyDevice SelectDevice(int majorVersion, FormFactor? formFactor = null, bool isFipsSeries = false) + IYubiKeyDevice SelectDevice( + int majorVersion, + FormFactor? formFactor = null, + bool isFipsSeries = false) { IYubiKeyDevice device = null!; try { - bool MatchingDeviceSelector(IYubiKeyDevice d) => + bool MatchingDeviceSelector( + IYubiKeyDevice d) => d.FirmwareVersion.Major == majorVersion && (formFactor is null || d.FormFactor == formFactor) && d.IsFipsSeries == isFipsSeries; - device = devices.First(MatchingDeviceSelector); + device = devicesVersionFiltered.First(MatchingDeviceSelector); } catch (InvalidOperationException) { - ThrowDeviceNotFoundException($"Target test device not found ({testDevice})", devices); + ThrowDeviceNotFoundException($"Target test device not found ({testDeviceType}, Transport: {transport})", devices); } return device; } } + private static FirmwareVersion MatchVersion( + StandardTestDevice testDeviceType, + FirmwareVersion? minimumFirmwareVersion) + { + if (minimumFirmwareVersion is { }) + { + return minimumFirmwareVersion; + } + + return testDeviceType switch + { + StandardTestDevice.Fw3 => FirmwareVersion.V3_1_0, + StandardTestDevice.Fw4Fips => FirmwareVersion.V4_0_0, + _ => FirmwareVersion.V5_0_0, + }; + } + public static IYubiKeyDevice SelectByMinimumVersion( this IEnumerable yubiKeys, FirmwareVersion minimumFirmwareVersion) @@ -116,16 +151,20 @@ public static IYubiKeyDevice SelectByMinimumVersion( ThrowDeviceNotFoundException("No matching YubiKey found", devices); } - return device!; + return device; } - private static void ThrowDeviceNotFoundException(string errorMessage, IYubiKeyDevice[] devices) + [DoesNotReturn] + private static void ThrowDeviceNotFoundException( + string errorMessage, + IYubiKeyDevice[] devices) { var connectedDevicesText = FormatConnectedDevices(devices); throw new DeviceNotFoundException($"{errorMessage}. {connectedDevicesText}"); } - private static string FormatConnectedDevices(IReadOnlyCollection devices) + private static string FormatConnectedDevices( + IReadOnlyCollection devices) { var deviceText = devices.Select(y => $"{{{y.FirmwareVersion}, {y.FormFactor}, IsFipsSeries: {y.IsFipsSeries}}}"); @@ -139,6 +178,9 @@ private static string FormatConnectedDevices(IReadOnlyCollection // Custom test exception inheriting from InvalidOperationException as some test code depends on InvalidOperationExceptions public class DeviceNotFoundException : InvalidOperationException { - public DeviceNotFoundException(string message) : base(message) { } + public DeviceNotFoundException( + string message) : base(message) + { + } } } diff --git a/build/CompilerSettings.props b/build/CompilerSettings.props index db506cd74..36737af72 100644 --- a/build/CompilerSettings.props +++ b/build/CompilerSettings.props @@ -61,6 +61,8 @@ on things like conditions in this file. --> @(NoWarn);NU5105 + en-US + false - - - en-US - + + false + + + \ No newline at end of file diff --git a/build/ProjectTypes.props b/build/ProjectTypes.props index ea57b8051..75c2e5400 100644 --- a/build/ProjectTypes.props +++ b/build/ProjectTypes.props @@ -44,6 +44,14 @@ limitations under the License. --> true false + + true + false +