From 6e537c78b86540de17f27d634e8be803dc8080ed Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 15 Aug 2024 15:39:26 +0200 Subject: [PATCH 01/53] initial Security Domain implementation feat(scp11): Generate EC Key feat(sd): Reset Security Domain misc: Changed static analysis mode misc: SampleCode projects won't build nuget packages --- .editorconfig | 6 +- Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs | 148 +++++ Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs | 106 ++++ .../tests/Yubico/Core/Tlv/TlvObjectTest.cs | 168 ++++++ .../secure-channel-protocol-3.md | 4 +- .../Resources/ExceptionMessages.Designer.cs | 33 +- .../src/Resources/ExceptionMessages.resx | 15 +- Yubico.YubiKey/src/Yubico.YubiKey.csproj | 3 + .../src/Yubico/YubiKey/ConnectionFactory.cs | 196 ++++++ .../YubiKey/Cryptography/AesUtilities.cs | 57 +- .../Yubico/YubiKey/IScp03YubiKeyConnection.cs | 3 + .../src/Yubico/YubiKey/IYubiKeyDevice.cs | 116 ++-- .../src/Yubico/YubiKey/Oath/OathSession.cs | 44 +- .../src/Yubico/YubiKey/Otp/OtpSession.cs | 10 +- .../Pipelines/CommandChainingTransform.cs | 2 +- .../Pipelines/ResponseChainingTransform.cs | 1 + .../YubiKey/Pipelines/ScpApduTransform.cs | 128 ++++ .../src/Yubico/YubiKey/Piv/PivSession.cs | 50 +- .../Yubico/YubiKey/Scp/ChannelEncryption.cs | 54 ++ .../src/Yubico/YubiKey/Scp/ChannelMac.cs | 108 ++++ .../YubiKey/Scp/Commands/DeleteKeyCommand.cs | 89 +++ .../Commands/ExternalAuthenticateCommand.cs | 65 ++ .../Commands/ExternalAuthenticateResponse.cs | 41 ++ .../Scp/Commands/GenerateEcKeyCommand.cs | 55 ++ .../YubiKey/Scp/Commands/GetDataCommand.cs | 72 +++ .../Scp/Commands/InitializeUpdateCommand.cs | 59 ++ .../Scp/Commands/InitializeUpdateResponse.cs | 54 ++ .../YubiKey/Scp/Commands/PutKeyCommand.cs | 255 ++++++++ .../YubiKey/Scp/Commands/PutKeyResponse.cs | 36 ++ .../YubiKey/Scp/Commands/ResetCommand.cs | 60 ++ .../YubiKey/Scp/Commands/ScpResponse.cs | 47 ++ .../Scp/Commands/SecurityOperationCommand.cs | 54 ++ .../src/Yubico/YubiKey/Scp/Derivation.cs | 122 ++++ .../YubiKey/Scp/IScpYubiKeyConnection.cs | 28 + .../Scp/InternalAuthenticateCommand.cs | 55 ++ .../src/Yubico/YubiKey/Scp/KeyReference.cs | 52 ++ .../src/Yubico/YubiKey/Scp/Padding.cs | 57 ++ .../Yubico/YubiKey/Scp/Scp03KeyParameters.cs | 45 ++ .../src/Yubico/YubiKey/Scp/Scp03State.cs | 129 ++++ .../Yubico/YubiKey/Scp/Scp11KeyParameters.cs | 90 +++ .../src/Yubico/YubiKey/Scp/Scp11State.cs | 301 ++++++++++ .../src/Yubico/YubiKey/Scp/ScpConnection.cs | 104 ++++ .../Yubico/YubiKey/Scp/ScpKeyParameters.cs | 30 + .../src/Yubico/YubiKey/Scp/ScpKid.cs | 24 + .../src/Yubico/YubiKey/Scp/ScpState.cs | 150 +++++ .../Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs | 58 ++ .../YubiKey/Scp/SecureChannelException.cs | 43 ++ .../YubiKey/Scp/SecurityDomainSession.cs | 557 ++++++++++++++++++ .../src/Yubico/YubiKey/Scp/SessionKeys.cs | 94 +++ .../src/Yubico/YubiKey/Scp03/ChannelMac.cs | 2 +- .../Scp03/Commands/InitializeUpdateCommand.cs | 4 +- .../Yubico/YubiKey/Scp03/Scp03Connection.cs | 3 + .../src/Yubico/YubiKey/Scp03/Scp03Session.cs | 5 + .../YubiKey/Scp03/Scp03YubiKeyDevice.cs | 14 +- .../src/Yubico/YubiKey/Scp03/Session.cs | 4 +- .../src/Yubico/YubiKey/Scp03/StaticKeys.cs | 46 +- .../src/Yubico/YubiKey/YubiKeyApplication.cs | 57 +- .../Yubico/YubiKey/YubiKeyDevice.Instance.cs | 469 +++++++-------- .../src/Yubico/YubiKey/YubiKeyDeviceInfo.cs | 4 +- .../Yubico/YubiKey/Oath/OathSessionTests.cs | 5 +- .../Yubico/YubiKey/Piv/GenerateTests.cs | 10 +- .../Yubico/YubiKey/Piv/GetPutDataTests.cs | 8 +- .../Yubico/YubiKey/Piv/SignTests.cs | 4 +- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 336 +++++++++++ .../Yubico/YubiKey/Scp/Scp11Tests.cs | 300 ++++++++++ .../Yubico/YubiKey/Scp/ScpCertificates.cs | 99 ++++ .../Scp03/Commands/DeleteKeyCommandTests.cs | 22 +- .../Scp03/Commands/PutKeyCommandTests.cs | 10 + .../Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs | 6 +- .../YubiKey/Scp03/SimpleSessionTests.cs | 28 +- .../tests/sandbox/Plugins/GregPlugin.cs | 13 +- .../Yubico/YubiKey/YubikeyApplicationTests.cs | 76 +++ .../TestUtilities/HollowYubiKeyDevice.cs | 73 ++- .../IntegrationTestDeviceEnumeration.cs | 1 - build/CompilerSettings.props | 24 +- build/ProjectTypes.props | 8 + 76 files changed, 5226 insertions(+), 483 deletions(-) create mode 100644 Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs create mode 100644 Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs create mode 100644 Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs create mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs create mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs create mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs create mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs 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/Tlv/TlvObject.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs new file mode 100644 index 000000000..1e0408fa8 --- /dev/null +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs @@ -0,0 +1,148 @@ +// 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 + { + private readonly byte[] _bytes; + private readonly int _offset; + + /// + /// Creates a new Tlv given a tag and a value. + /// + public TlvObject(int tag, ReadOnlySpan value) + { + Tag = tag; + byte[] buffer = value.ToArray(); + using var stream = new MemoryStream(); + + byte[] tagBytes = BitConverter.GetBytes(tag).Reverse().SkipWhile(b => b == 0).ToArray(); //TODO check + stream.Write(tagBytes, 0, tagBytes.Length); + + Length = buffer.Length; + if (Length < 0x80) + { + stream.WriteByte((byte)Length); + } + else + { + byte[] lnBytes = BitConverter.GetBytes(Length).Reverse().SkipWhile(b => b == 0).ToArray(); + stream.WriteByte((byte)(0x80 | lnBytes.Length)); + stream.Write(lnBytes, 0, lnBytes.Length); + } + + _offset = (int)stream.Position; + + stream.Write(buffer, 0, Length); + + _bytes = stream.ToArray(); + } + + /// + /// Returns the tag. + /// + public int Tag { get; } + + /// + /// Returns the value. + /// + public ReadOnlyMemory Value => _bytes.Skip(_offset).Take(Length).ToArray(); + + /// + /// Returns the length of the value. + /// + public int Length { get; } + + /// + /// Returns the Tlv as a BER-TLV encoded byte array. + /// + public ReadOnlyMemory GetBytes() => _bytes; + + /// + /// 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); + } + + /// + /// Parse a Tlv from a BER-TLV encoded byte array. + /// + /// A byte array containing the TLV encoded data. + /// The offset in data where the TLV data begins. + /// The length of the TLV encoded data. + /// The parsed Tlv + public static TlvObject Parse(ReadOnlySpan data, int offset, int length) => Parse(data.Slice(offset, length)); + + public static TlvObject ParseFrom(ref ReadOnlySpan buffer) + { + int tag = buffer[0]; + buffer = buffer[1..]; + if ((tag & 0x1F) == 0x1F) // Long form tag + { + do + { + tag = (tag << 8) | buffer[0]; + buffer = buffer[1..]; + } while ((tag & 0x80) == 0x80); + } + + int length = buffer[0]; + buffer = buffer[1..]; + + if (length == 0x80) + { + throw new ArgumentException("Indefinite length not supported"); + } + + 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..]; + + return new TlvObject(tag, value); + } + + 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..9b552aba9 --- /dev/null +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Yubico.Core.Tlv; + +namespace Yubico.YubiKit.Core.Util +{ + /// + /// 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 Tlvs + 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; + } + + public static byte[] EncodeList(IEnumerable list) + { + if (list is null) + { + throw new ArgumentNullException(nameof(list)); + } + + using var stream = new MemoryStream(); + foreach (TlvObject? tlv in list) + { + ReadOnlyMemory bytes = tlv.GetBytes(); +#if NETSTANDARD2_1_OR_GREATER + stream.Write(bytes.Span); +#else + stream.Write(bytes.Span.ToArray(), 0, bytes.Length); +#endif + } + return stream.ToArray(); + } + + public static byte[] EncodeMany(params TlvObject[] tlvs) => EncodeList(tlvs); + + + //Todo keep? + public static byte[] EncodeMap(IReadOnlyDictionary> map) + { + if (map is null) + { + throw new ArgumentNullException(nameof(map)); + } + + using var stream = new MemoryStream(); + foreach (KeyValuePair> entry in map) + { + var tlv = new TlvObject(entry.Key, entry.Value.ToArray()); + ReadOnlyMemory bytes = tlv.GetBytes(); + stream.Write(bytes.ToArray(), 0,bytes.Length);; + } + return stream.ToArray(); + } + + /// + /// 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 ReadOnlyMemory 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; + } + } +} diff --git a/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs b/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs new file mode 100644 index 000000000..71563530b --- /dev/null +++ b/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs @@ -0,0 +1,168 @@ +// 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; +using Yubico.YubiKit.Core.Util; + +namespace Yubico.Core.Tlv.UnitTests +{ + public class TlvObjectTest + { + [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 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 EncodeMap_ValidInput_ReturnsCorrectBytes() + { + var map = new Dictionary> + { + { 0x01, new byte[] { 0xFF } }, + { 0x02, new byte[] { 0xAA, 0xBB } } + }; + + var result = TlvObjects.EncodeMap(map); + Assert.Equal(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x02, 0xAA, 0xBB }, result.ToArray()); + } + + [Fact] + public void EncodeMap_EmptyInput_ReturnsEmptyArray() + { + var result = TlvObjects.EncodeMap(new Dictionary>()); + 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.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 index d1e28e050..91ab0880f 100644 --- 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 @@ -32,7 +32,7 @@ as "SCP03"). This standard prescribes methods to encrypt and authenticate smart (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 +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 @@ -51,7 +51,7 @@ 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 +For example, if you tunnel YubiKey commands and responses 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. diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs index cdc9dd487..6bbad079c 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..8df5141a7 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..dbb191916 100644 --- a/Yubico.YubiKey/src/Yubico.YubiKey.csproj +++ b/Yubico.YubiKey/src/Yubico.YubiKey.csproj @@ -73,6 +73,7 @@ limitations under the License. --> OtpSettings.cs + @@ -106,6 +107,8 @@ limitations under the License. --> Yubico.NET.SDK.snk + + diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs new file mode 100644 index 000000000..0e824403f --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs @@ -0,0 +1,196 @@ +// 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.Threading; +using Microsoft.Extensions.Logging; +using Yubico.Core.Devices.Hid; +using Yubico.Core.Devices.SmartCard; +using Yubico.YubiKey.Scp; +using Yubico.YubiKey.Scp03; + +namespace Yubico.YubiKey +{ + // TODO Consider merging with ConnectionManager + public class ConnectionFactory + { + private readonly ILogger _log; + private readonly YubiKeyDevice _device; + private readonly ISmartCardDevice? _smartCardDevice; + private readonly IHidDevice? _hidKeyboardDevice; + private readonly IHidDevice? _hidFidoDevice; + + public ConnectionFactory( + ILogger log, + YubiKeyDevice device, + ISmartCardDevice? smartCardDevice, + IHidDevice? hidKeyboardDevice, + IHidDevice? hidFidoDevice) + { + _log = log; + _device = device; + _smartCardDevice = smartCardDevice; + _hidKeyboardDevice = hidKeyboardDevice; + _hidFidoDevice = hidFidoDevice; + } + + [Obsolete("Obsolete")] + internal IYubiKeyConnection CreateScpConnection(YubiKeyApplication application, StaticKeys scp03Keys) + { + + if (_smartCardDevice is null) + { + _log.LogError("No smart card interface present. Unable to establish SCP connection to YubiKey."); + throw new InvalidOperationException("TODO"); + } + + _log.LogInformation("Connecting via the SmartCard interface using SCP03."); + WaitForReclaimTimeout(Transport.SmartCard); + + return new Scp03Connection(_smartCardDevice, application, scp03Keys); + } + + public IYubiKeyConnection CreateScpConnection(YubiKeyApplication application, ScpKeyParameters keyParameters) + { + LogConnectionAttempt(application, keyParameters); // No need + + if (_smartCardDevice is null) + { + _log.LogError("No smart card interface present. Unable to establish SCP connection to YubiKey."); + throw new InvalidOperationException("TODO"); + } + + _log.LogInformation("Connecting via the SmartCard interface using SCP03."); + WaitForReclaimTimeout(Transport.SmartCard); + + return new ScpConnection(_smartCardDevice, application, keyParameters); + } + + public IYubiKeyConnection CreateNonScpConnection(YubiKeyApplication application) + { + if (_smartCardDevice != null) + { + _log.LogInformation("Connecting via the SmartCard interface."); + + WaitForReclaimTimeout(Transport.SmartCard); + return new SmartCardConnection(_smartCardDevice, application); + } + + if (application == YubiKeyApplication.Otp && _hidKeyboardDevice != null) + { + _log.LogInformation("Connecting via the Keyboard interface."); + + WaitForReclaimTimeout(Transport.HidKeyboard); + return new KeyboardConnection(_hidKeyboardDevice); + } + + if ((application == YubiKeyApplication.Fido2 || application == YubiKeyApplication.FidoU2f) && _hidFidoDevice != null) + { + _log.LogInformation("Connecting via the FIDO interface."); + + WaitForReclaimTimeout(Transport.HidFido); + return new FidoConnection(_hidFidoDevice); + } + + _log.LogError("No suitable interface present. Unable to establish connection to YubiKey."); + throw new InvalidOperationException("TODO"); + } + + // This function handles waiting for the reclaim timeout on the YubiKey to elapse. The reclaim timeout requires + // the SDK to wait 3 seconds since the last USB message to an interface before switching to a different interface. + // Failure to wait can result in very strange behavior from the USB devices ultimately resulting in communication + // failures (i.e. exceptions). + private void WaitForReclaimTimeout(Transport newTransport) + { + // Newer YubiKeys are able to switch interfaces much, much faster. Maybe this is being paranoid, but we + // should still probably wait a few milliseconds for things to stabilize. But definitely not the full + // three seconds! For older keys, we use a value of 3.01 seconds to give us a little wiggle room as the + // YubiKey's measurement for the reclaim timeout is likely not as accurate as our system clock. + var reclaimTimeout = CanFastReclaim() + ? TimeSpan.FromMilliseconds(100) + : TimeSpan.FromSeconds(3.01); + + // We're only affected by the reclaim timeout if we're switching USB transports. + if (_device.LastActiveTransport == newTransport) + { + _log.LogInformation( + "{Transport} transport is already active. No need to wait for reclaim.", + _device.LastActiveTransport); + + return; + } + + _log.LogInformation( + "Switching USB transports from {OldTransport} to {NewTransport}.", + _device.LastActiveTransport, + newTransport); + + var timeSinceLastActivation = DateTime.Now - GetLastActiveTime(); + + // If we haven't already waited the duration of the reclaim timeout, we need to do so. + // Otherwise, we've already waited and can immediately switch the transport. + if (timeSinceLastActivation < reclaimTimeout) + { + var waitNeeded = reclaimTimeout - timeSinceLastActivation; + + _log.LogInformation( + "Reclaim timeout still active. Need to wait {TimeMS} milliseconds.", + waitNeeded.TotalMilliseconds); + + Thread.Sleep(waitNeeded); + } + + _device.LastActiveTransport = newTransport; + + _log.LogInformation("Reclaim timeout has lapsed. It is safe to switch USB transports."); + } + + private bool CanFastReclaim() + { + if (AppContext.TryGetSwitch(YubiKeyCompatSwitches.UseOldReclaimTimeoutBehavior, out bool useOldBehavior) && + useOldBehavior) + { + return false; + } + + return _device.HasFeature(YubiKeyFeature.FastUsbReclaim); + } + + private DateTime GetLastActiveTime() => + _device.LastActiveTransport switch + { + Transport.SmartCard when _smartCardDevice is { } => _smartCardDevice.LastAccessed, + Transport.HidFido when _hidFidoDevice is { } => _hidFidoDevice.LastAccessed, + Transport.HidKeyboard when _hidKeyboardDevice is { } => _hidKeyboardDevice.LastAccessed, + Transport.None => DateTime.Now, + _ => throw new InvalidOperationException(ExceptionMessages.DeviceTypeNotRecognized) + }; + + private void LogConnectionAttempt( + YubiKeyApplication application, + ScpKeyParameters keyParameters) + { + string applicationName = GetApplicationName(application); + string scpInfo = keyParameters is Scp03KeyParameters + ? "SCP03" + : "SCP11"; //TODO make better + + _log.LogInformation("YubiKey connecting to {Application} application over {ScpInfo}", applicationName, scpInfo); + } + + private static string GetApplicationName(YubiKeyApplication application) => + Enum.GetName(typeof(YubiKeyApplication), application) ?? "Unknown"; + + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs index a4750999b..7aaa4bc7b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs @@ -29,19 +29,19 @@ internal static class AesUtilities /// /// This is not a secure authenticated encryption scheme. /// - /// 16-byte AES128 key + /// 16-byte AES128 key /// 16-byte input block /// The 16-byte AES128 ciphertext - public static byte[] BlockCipher(byte[] key, ReadOnlySpan plaintext) + public static byte[] BlockCipher(byte[] encryptionKey, ReadOnlySpan plaintext) { - if (key is null) + if (encryptionKey is null) { - throw new ArgumentNullException(nameof(key)); + throw new ArgumentNullException(nameof(encryptionKey)); } - if (key.Length != BlockSizeBytes) + if (encryptionKey.Length != BlockSizeBytes) { - throw new ArgumentException(ExceptionMessages.IncorrectAesKeyLength, nameof(key)); + throw new ArgumentException(ExceptionMessages.IncorrectAesKeyLength, nameof(encryptionKey)); } if (plaintext.Length != BlockSizeBytes) @@ -49,31 +49,26 @@ public static byte[] BlockCipher(byte[] key, ReadOnlySpan plaintext) throw new ArgumentException(ExceptionMessages.IncorrectPlaintextLength, nameof(plaintext)); } - byte[] ciphertext; - - using (var aesObj = CryptographyProviders.AesCreator()) - { -#pragma warning disable CA5358 // Allow the usage of cipher mode 'ECB' - aesObj.Mode = CipherMode.ECB; -#pragma warning restore CA5358 - aesObj.KeySize = BlockSizeBits; - aesObj.BlockSize = BlockSizeBits; - aesObj.Key = key; - aesObj.IV = new byte[BlockSizeBytes]; - aesObj.Padding = PaddingMode.None; -#pragma warning disable CA5401 // Justification: Allow the symmetric encryption to use - // a non-default initialization vector - var encryptor = aesObj.CreateEncryptor(); -#pragma warning restore CA5401 - using (var msEncrypt = new MemoryStream()) - { - using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) - { - csEncrypt.Write(plaintext.ToArray(), 0, plaintext.Length); - ciphertext = msEncrypt.ToArray(); - } - } - } + using var aesObj = CryptographyProviders.AesCreator(); + + #pragma warning disable CA5358 // Allow the usage of cipher mode 'ECB' + aesObj.Mode = CipherMode.ECB; + #pragma warning restore CA5358 + aesObj.KeySize = BlockSizeBits; + aesObj.BlockSize = BlockSizeBits; + aesObj.Key = encryptionKey; + aesObj.IV = new byte[BlockSizeBytes]; + aesObj.Padding = PaddingMode.None; + #pragma warning disable CA5401 // Justification: Allow the symmetric encryption to use + // a non-default initialization vector + var encryptor = aesObj.CreateEncryptor(); + #pragma warning restore CA5401 + + using var msEncrypt = new MemoryStream(); + using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); + + csEncrypt.Write(plaintext.ToArray(), 0, plaintext.Length); + byte[] ciphertext = msEncrypt.ToArray(); return ciphertext; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs index 5ca39f358..60a50b59b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey @@ -20,6 +22,7 @@ namespace Yubico.YubiKey /// The connection class that can perform SCP03 operations will implement not /// only , but this interface as well. /// + [Obsolete("Use new scp")] public interface IScp03YubiKeyConnection : IYubiKeyConnection { /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs index f862b9d22..8daef5eab 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs @@ -15,6 +15,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Yubico.Core.Devices; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; using MgmtCmd = Yubico.YubiKey.Management.Commands; @@ -48,14 +49,29 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// Initiate a connection to the specified application on a YubiKey /// device. /// - /// + /// /// The application to reference on the device. /// /// /// An instance of a class that implements the /// interface. /// - IYubiKeyConnection Connect(YubiKeyApplication yubikeyApplication); + IYubiKeyConnection Connect(YubiKeyApplication application); + + /// + /// Initiate a connection to the specified application represented as an + /// applicationId on a YubiKey device. + /// + /// + /// A byte array representing the smart card Application ID (AID) for the + /// application to open. + /// + /// + /// An instance of a class that implements the + /// interface. + /// + [Obsolete("Use corresponding YubiKeyApplication method")] + IYubiKeyConnection Connect(byte[] applicationId); /// /// Initiate a connection to the specified application on a YubiKey @@ -73,7 +89,7 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// . /// /// - /// + /// /// The application to reference on the device. /// /// @@ -83,21 +99,8 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// An instance of a class that implements the /// interface. /// - IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys); - - /// - /// Initiate a connection to the specified application represented as an - /// applicationId on a YubiKey device. - /// - /// - /// A byte array representing the smart card Application ID (AID) for the - /// application to open. - /// - /// - /// An instance of a class that implements the - /// interface. - /// - IYubiKeyConnection Connect(byte[] applicationId); + [Obsolete("Use new Scp")] + IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication application, StaticKeys scp03Keys); /// /// Initiate a connection to the specified application represented as an @@ -126,24 +129,14 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// An instance of a class that implements the /// interface. /// + [Obsolete("Use new Scp")] IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys); + // TODO DOCU + IScpYubiKeyConnection ConnectScp(YubiKeyApplication application, ScpKeyParameters keyParameters); + // TODO DOCU + IScpYubiKeyConnection ConnectScp(byte[] applicationId, ScpKeyParameters keyParameters); - /// - /// Checks whether a IYubiKeyDevice instance contains a particular platform . - /// - /// The device to check. - /// True, if the IYubiKeyDevice contains the platform device. - internal bool Contains(IDevice other); - - /// - /// Checks whether a IYubiKeyDevice instance contains another with the same - /// . - /// - /// The device to check against. - /// True, if the IYubiKeyDevice contains a platform device that shares the same parent. - internal bool HasSameParentDevice(IDevice other); - - /// + /// /// Attempt to connect to the YubiKey device. /// /// The application to reference on the device. @@ -153,6 +146,19 @@ bool TryConnect( YubiKeyApplication application, [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection); + + /// + /// Attempt to connect to the YubiKey device. + /// + /// A byte array representing the smart card Application ID (AID) for the + /// application to open. + /// Out parameter containing the instance. + /// Boolean indicating whether the call was successful. + [Obsolete("Obsolute")] + bool TryConnect( + byte[] applicationId, + [MaybeNullWhen(returnValue: false)] + out IYubiKeyConnection connection); /// /// Attempt to connect to the YubiKey device. The connection will be made @@ -175,24 +181,13 @@ bool TryConnect( /// /// Out parameter containing the instance. /// Boolean indicating whether the call was successful. + [Obsolete("Use new Scp")] bool TryConnectScp03( YubiKeyApplication application, StaticKeys scp03Keys, [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection); - /// - /// Attempt to connect to the YubiKey device. - /// - /// A byte array representing the smart card Application ID (AID) for the - /// application to open. - /// Out parameter containing the instance. - /// Boolean indicating whether the call was successful. - bool TryConnect( - byte[] applicationId, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection); - /// /// Attempt to connect to the YubiKey device. The connection will be made /// over SCP03 (assuming the keys are the ones loaded onto the YubiKey). @@ -214,11 +209,40 @@ bool TryConnect( /// /// Out parameter containing the instance. /// Boolean indicating whether the call was successful. + [Obsolete("Use new Scp")] bool TryConnectScp03( byte[] applicationId, StaticKeys scp03Keys, [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection); + + // TODO DOcumentation + bool TryConnectScp( + YubiKeyApplication application, + ScpKeyParameters keyParameters, + [MaybeNullWhen(returnValue: false)] + out IScpYubiKeyConnection connection); + + bool TryConnectScp( + byte[] applicationId, + ScpKeyParameters keyParameters, + [MaybeNullWhen(returnValue: false)] + out IScpYubiKeyConnection connection); + + /// + /// Checks whether a IYubiKeyDevice instance contains a particular platform . + /// + /// The device to check. + /// True, if the IYubiKeyDevice contains the platform device. + internal bool Contains(IDevice other); + + /// + /// Checks whether a IYubiKeyDevice instance contains another with the same + /// . + /// + /// The device to check against. + /// True, if the IYubiKeyDevice contains a platform device that shares the same parent. + internal bool HasSameParentDevice(IDevice other); /// /// Sets which NFC features are enabled (and disabled). diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs index 97d9c4cdc..6bb2d5a5d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs @@ -14,8 +14,11 @@ using System; using System.Globalization; +using Microsoft.Extensions.Logging; +using Yubico.Core.Logging; using Yubico.YubiKey.InterIndustry.Commands; using Yubico.YubiKey.Oath.Commands; +using Yubico.YubiKey.Scp; namespace Yubico.YubiKey.Oath { @@ -25,6 +28,7 @@ namespace Yubico.YubiKey.Oath public sealed partial class OathSession : IDisposable { private bool _disposed; + private readonly ILogger _log = Log.GetLogger(); private readonly IYubiKeyDevice _yubiKeyDevice; internal OathApplicationData _oathData; @@ -81,34 +85,31 @@ private OathSession() /// /// The object that represents the actual YubiKey which will perform the operations. /// + /// TODO /// /// The yubiKey argument is null. /// /// /// The SelectApplicationData recived from the yubiKey is null. /// - public OathSession(IYubiKeyDevice yubiKey) + public OathSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) { - if (yubiKey is null) - { - throw new ArgumentNullException(nameof(yubiKey)); - } - - _yubiKeyDevice = yubiKey; + _yubiKeyDevice = yubiKey ?? throw new ArgumentNullException(nameof(yubiKey)); - Connection = yubiKey.Connect(YubiKeyApplication.Oath); + Connection = keyParameters is null + ? yubiKey.Connect(YubiKeyApplication.Oath) + : yubiKey.ConnectScp(YubiKeyApplication.Oath, keyParameters); - if (!(Connection.SelectApplicationData is OathApplicationData)) + if (!(Connection.SelectApplicationData is OathApplicationData data)) { throw new InvalidOperationException(nameof(Connection.SelectApplicationData)); } - _oathData = (Connection.SelectApplicationData as OathApplicationData)!; + _oathData = data; _disposed = false; } - /// /// Resets the YubiKey's OATH application back to a factory default state. /// @@ -126,7 +127,7 @@ public void ResetApplication() throw new InvalidOperationException(resetOathResponse.StatusMessage); } - var selectOathResponse = Connection.SendCommand(new SelectOathCommand()); + var selectOathResponse = Connection.SendCommand(new SelectOathCommand()); // TODO throws error: Wrong syntax _oathData = selectOathResponse.GetData(); } @@ -145,6 +146,7 @@ private void EnsureKeyCollector() /// /// When the OathSession object goes out of scope, this method is called. It will close the session. /// + // Note that .NET recommends a Dispose method call Dispose(true) and GC.SuppressFinalize(this). // The actual disposal is in the Dispose(bool) method. // @@ -159,9 +161,25 @@ public void Dispose() // At the moment, there is no "close session" method. So for now, // just connect to the management application. - _ = Connection.SendCommand(new SelectApplicationCommand(YubiKeyApplication.Management)); + // This can fail, possibly resulting in a SCardException (or other), so we wrap it in a try catch-block to complete the disposal of the PivSession + try + { + _ = Connection.SendCommand(new SelectApplicationCommand(YubiKeyApplication.Management)); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + string message = string.Format( + CultureInfo.CurrentCulture, + ExceptionMessages.SessionDisposeUnknownError, e.GetType(), e.Message); + + _log.LogWarning(message); + } + KeyCollector = null; Connection.Dispose(); + _disposed = true; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs index 54fb532aa..4fa49d979 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs @@ -16,6 +16,7 @@ using System.Globalization; using Yubico.YubiKey.Otp.Commands; using Yubico.YubiKey.Otp.Operations; +using Yubico.YubiKey.Scp; namespace Yubico.YubiKey.Otp { @@ -66,12 +67,15 @@ public sealed class OtpSession : IOtpSession /// Constructs a instance for high-level OTP operations. /// /// Instance of class implementing . - public OtpSession(IYubiKeyDevice yubiKey) + /// TODO + public OtpSession(IYubiKeyDevice yubiKey, Scp03KeyParameters? keyParameters = null) { if (yubiKey is null) { throw new ArgumentNullException(nameof(yubiKey)); } YubiKey = yubiKey; - _connection = yubiKey.Connect(YubiKeyApplication.Otp); - _otpStatus = _connection.SendCommand(new ReadStatusCommand()).GetData(); + _connection = keyParameters is null + ? yubiKey.Connect(YubiKeyApplication.Oath) + : yubiKey.ConnectScp(YubiKeyApplication.Oath, keyParameters); + _otpStatus = _connection.SendCommand(new ReadStatusCommand()).GetData(); // fails on read data Incorrect parameters in the command data field. 0x6A80 } #region OTP Operation Object Factory diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs index 077ec08ed..1aec2bbed 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs @@ -67,7 +67,7 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT responseApdu = _pipeline.Invoke(partialApdu, commandType, responseType); } - return responseApdu!; // Covered by Debug.Assert above. + return responseApdu!; // Covered by Debug.Assert above. TODO err?? } public void Setup() => _pipeline.Setup(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs index 8ebb18842..b7685f027 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs @@ -47,6 +47,7 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT var responseApdu = _pipeline.Invoke(command, commandType, responseType); // Unless we see that bytes are available, there's nothing for this transform to do. + // TODO refactor away do while if (responseApdu.SW1 != SW1Constants.BytesAvailable) { return responseApdu; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs new file mode 100644 index 000000000..1d26b947d --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -0,0 +1,128 @@ +// 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 Yubico.Core.Iso7816; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp; + +namespace Yubico.YubiKey.Pipelines +{ + /// + /// Performs SCP encrypt-then-MAC on commands and verify-then-decrypt on responses. + /// + /// + /// Does an SCP Initialize Update / External Authenticate handshake at setup. + /// + /// Commands and responses sent through this pipeline are confidential and authenticated. + /// + /// Requires pre-shared . TODO + /// + // broken into two transforms + internal class ScpApduTransform : IApduTransform, IDisposable + { + public ScpKeyParameters KeyParameters { get; } + + private ScpState ScpState => + _scpState ?? throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); + + private readonly IApduTransform _pipeline; + private ScpState? _scpState; + private bool _disposed; + + /// + /// Constructs a new pipeline from the given one. + /// + /// Underlying pipeline to send and receive encoded APDUs with + /// //todo + public ScpApduTransform(IApduTransform pipeline, ScpKeyParameters keyParameters) + { + _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + KeyParameters = keyParameters ?? throw new ArgumentNullException(nameof(keyParameters)); + } + + /// + /// Performs SCP handshake. Must be called after SELECT. + /// + public void Setup() + { + _pipeline.Setup(); + + if (KeyParameters.GetType() == typeof(Scp03KeyParameters)) + { + InitializeScp03((Scp03KeyParameters)KeyParameters); + } + else if (KeyParameters.GetType() == typeof(Scp11KeyParameters)) + { + InitializeScp11((Scp11KeyParameters)KeyParameters); + } + } + + private void InitializeScp11(Scp11KeyParameters keyParameters) + => _scpState = Scp11State.CreateScpState(_pipeline, keyParameters); + + private void InitializeScp03(Scp03KeyParameters keyParams) + { + // Generate host challenge + using var rng = CryptographyProviders.RngCreator(); + byte[] hostChallenge = new byte[8]; + rng.GetBytes(hostChallenge); + + _scpState = Scp03State.CreateScpState(_pipeline, keyParams, hostChallenge); + } + + public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) + { + // Encode command + var encodedCommand = ScpState.EncodeCommand(command); + + // Pass along the encoded command + var response = _pipeline.Invoke(encodedCommand, commandType, responseType); + + // Special carve out for SelectApplication here, since there will be nothing to decode + if (commandType == typeof(InterIndustry.Commands.SelectApplicationCommand)) + { + return response; + } + + // Decode response and return it + return ScpState.DecodeResponse(response); + } + + // There is a call to cleanup and a call to Dispose. The cleanup only + // needs to call the cleanup on the local APDU Pipeline object. + public void Cleanup() => _pipeline.Cleanup(); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // The Dispose needs to make sure the local disposable fields are + // disposed. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _scpState?.Dispose(); + + _disposed = true; + } + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs index 43c664c2e..c74a847a7 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs @@ -21,6 +21,7 @@ using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.InterIndustry.Commands; using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey.Piv @@ -186,8 +187,21 @@ private PivSession() /// The yubiKey argument is null. /// public PivSession(IYubiKeyDevice yubiKey) - : this(scp03Keys: null, yubiKey) { + if (yubiKey is null) + { + throw new ArgumentNullException(nameof(yubiKey)); + } + + _log.LogInformation("Create a new instance of PivSession."); + + Connection = yubiKey.Connect(YubiKeyApplication.Piv); + + ResetAuthenticationStatus(); + UpdateManagementKey(yubiKey); + + _yubiKeyDevice = yubiKey; + _disposed = false; } /// @@ -227,11 +241,13 @@ public PivSession(IYubiKeyDevice yubiKey) /// /// This exception is thrown when unable to determine the management key type. /// + [Obsolete("Use new Scp")] public PivSession(IYubiKeyDevice yubiKey, StaticKeys scp03Keys) : this(scp03Keys, yubiKey) { } - + + [Obsolete("Use new Scp")] private PivSession(StaticKeys? scp03Keys, IYubiKeyDevice yubiKey) { _log.LogInformation( @@ -254,6 +270,30 @@ private PivSession(StaticKeys? scp03Keys, IYubiKeyDevice yubiKey) _yubiKeyDevice = yubiKey; _disposed = false; } + + public PivSession(IYubiKeyDevice yubiKey, ScpKeyParameters keyParameters) + { + if (yubiKey is null) + { + throw new ArgumentNullException(nameof(yubiKey)); + } + + string scpType = keyParameters switch + { + Scp03KeyParameters _ => "SCP03", + Scp11KeyParameters _ => "SCP11", + _ => string.Empty + }; + + _log.LogInformation($"Create a new instance of PivSession over {scpType}"); + Connection = yubiKey.ConnectScp(YubiKeyApplication.Piv, keyParameters); + + ResetAuthenticationStatus(); + UpdateManagementKey(yubiKey); + + _yubiKeyDevice = yubiKey; + _disposed = false; + } /// /// The object that represents the connection to the YubiKey. Most @@ -319,12 +359,8 @@ public void Dispose() { string message = string.Format( CultureInfo.CurrentCulture, - ExceptionMessages.PivSessionDisposeUnknownError, e.GetType(), e.Message); + ExceptionMessages.SessionDisposeUnknownError, e.GetType(), e.Message); - // Example: - // Exception caught when disposing PivSession: Yubico.PlatformInterop.SCardException, - // Unable to begin a transaction with the given smart card. SCARD_E_SERVICE_STOPPED: The smart card resource manager has shut down. - _log.LogWarning(message); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs new file mode 100644 index 000000000..db8a49407 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs @@ -0,0 +1,54 @@ +// 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.Buffers.Binary; +using Yubico.YubiKey.Cryptography; + +namespace Yubico.YubiKey.Scp +{ + internal static class ChannelEncryption + { + public static byte[] EncryptData(ReadOnlySpan dataToEncrypt, ReadOnlySpan encryptionKey, int encryptionCounter) + { + // NB: Could skip this if the payload is empty (rather than sending a 16-byte encrypted '0x800000...' payload + byte[] countBytes = new byte[sizeof(int)]; + BinaryPrimitives.WriteInt32BigEndian(countBytes, encryptionCounter); + + byte[] ivInput = new byte[16]; + countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block + byte[] iv = AesUtilities.BlockCipher(encryptionKey.ToArray(), ivInput); //todo toarry + + byte[] paddedPayload = Padding.PadToBlockSize(dataToEncrypt.ToArray()); + byte[] encryptedData = AesUtilities.AesCbcEncrypt(encryptionKey.ToArray(), iv, paddedPayload); + + return encryptedData; + } + + public static byte[] DecryptData(byte[] dataToDecrypt, byte[] key, int encryptionCounter) + { + byte[] countBytes = new byte[sizeof(int)]; + BinaryPrimitives.WriteInt32BigEndian(countBytes, encryptionCounter); + + byte[] ivInput = new byte[16]; + countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block + ivInput[0] = 0x80; // to mark as RMAC calculation + byte[] iv = AesUtilities.BlockCipher(key, ivInput); + + byte[] decryptedData = AesUtilities.AesCbcDecrypt(key, iv, dataToDecrypt); + + return Padding.RemovePadding(decryptedData); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs new file mode 100644 index 000000000..3bd2ee570 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs @@ -0,0 +1,108 @@ +// 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.Security.Cryptography; +using Yubico.Core.Cryptography; +using Yubico.Core.Iso7816; +using Yubico.YubiKey; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp; + +internal static class ChannelMac +{ + public static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( + CommandApdu apdu, + ReadOnlySpan macKey, + ReadOnlySpan macChainingValue) + { + if (macChainingValue.Length != 16) + { + throw new ArgumentException(ExceptionMessages.UnknownScpError, nameof(macChainingValue)); + } + + var apduWithLongerLen = AddDataToApdu(apdu, new byte[8]); + byte[] apduBytesWithZeroMac = apduWithLongerLen.AsByteArray(); + + byte[] macInp = new byte[16 + apduBytesWithZeroMac.Length - 8]; + macChainingValue.CopyTo(macInp); + apduBytesWithZeroMac.AsSpan(0, apduBytesWithZeroMac.Length - 8).CopyTo(macInp.AsSpan(16)); + + byte[] newMacChainingValue = new byte[16]; + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + cmacObj.CmacInit(macKey); + cmacObj.CmacUpdate(macInp); + cmacObj.CmacFinal(newMacChainingValue); + + var macdApdu = AddDataToApdu(apdu, newMacChainingValue.AsSpan(0, 8)); + return (macdApdu, newMacChainingValue); + } + + public static void VerifyRmac(ReadOnlySpan response, ReadOnlySpan rmacKey, ReadOnlySpan macChainingValue) + { + if (response.Length < 8) + { + throw new SecureChannelException(ExceptionMessages.InsufficientResponseLengthToVerifyRmac); + } + + if ((response.Length - 8) % 16 != 0) + { + throw new SecureChannelException(ExceptionMessages.IncorrectResponseLengthToDecrypt); + } + + int respDataLen = response.Length - 8; + var recvdRmac = response[^8..]; + + Span macInp = stackalloc byte[16 + respDataLen + 2]; + macChainingValue.CopyTo(macInp); + response[..respDataLen].CopyTo(macInp[16..]); + + macInp[16 + respDataLen] = SW1Constants.Success; + macInp[16 + respDataLen + 1] = SWConstants.Success & 0xFF; + + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + Span cmac = stackalloc byte[16]; + cmacObj.CmacInit(rmacKey); + cmacObj.CmacUpdate(macInp); + cmacObj.CmacFinal(cmac); + + if (!CryptographicOperations.FixedTimeEquals(recvdRmac, cmac.Slice(0, 8))) + { + throw new SecureChannelException(ExceptionMessages.IncorrectRmac); + } + } + + private static CommandApdu AddDataToApdu(CommandApdu apdu, ReadOnlySpan data) + { + var newApdu = new CommandApdu + { + Cla = apdu.Cla, + Ins = apdu.Ins, + P1 = apdu.P1, + P2 = apdu.P2 + }; + + int currentDataLength = apdu.Data.Length; + byte[] newData = new byte[currentDataLength + data.Length]; + + if (!apdu.Data.IsEmpty) + { + apdu.Data.Span.CopyTo(newData); + } + + data.CopyTo(newData.AsSpan(currentDataLength)); + newApdu.Data = newData; + return newApdu; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs new file mode 100644 index 000000000..352d90dc1 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs @@ -0,0 +1,89 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// Use this command to delete one of the SCP03 key sets on the YubiKey. + /// + /// + /// See the User's Manual entry on SCP03. + /// + /// This will execute the Delete Command. That is, there is a general purpose + /// command that can delete various elements, including keys. However, this + /// class can build the general purpose delete command in a way that it will + /// only be able to delete keys. + /// + /// + /// Note that if all three key sets are deleted, then the first key set (the + /// key set with a KeyVersionNumber of 1) will be the default key set. + /// + /// + internal class DeleteKeyCommand : IYubiKeyCommand + { + private const byte GpDeleteKeyCla = 0x84; + private const byte GpDeleteKeyIns = 0xE4; + private const byte GpDeleteKeyP1 = 0; + private const byte GpDeleteKeyP2 = 0; + private const byte GpDeleteLastKeyP2 = 1; + private const byte KvnTag = 0xD2; + private const byte KvnLength = 1; + + private readonly byte[] _data; + private readonly byte _p2Value; + + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + + // The default constructor explicitly defined. We don't want it to be + // used. + private DeleteKeyCommand() + { + throw new NotImplementedException(); + } + + /// + /// Create a new instance of the command. When this command is executed, + /// the key set represented by the given keyVersionNumber will be + /// deleted. If there is no such key set, this command will do nothing + /// and the return Status will be ResponseStatus.NoData. + /// + /// + /// The key set used to make the connection cannot be the key set to be + /// deleted, unless both of the other key sets have been deleted, and you + /// pass true for isLastKey. In this case, the key will be + /// deleted but the SCP03 application on the YubiKey will be reset with + /// the default key. + /// + public DeleteKeyCommand(byte keyVersionNumber, bool isLastKey) + { + _data = new[] { KvnTag, KvnLength, keyVersionNumber }; + _p2Value = isLastKey ? GpDeleteLastKeyP2 : GpDeleteKeyP2; + } + + public CommandApdu CreateCommandApdu() => new CommandApdu() + { + Cla = GpDeleteKeyCla, + Ins = GpDeleteKeyIns, + P1 = GpDeleteKeyP1, + P2 = _p2Value, + Data = _data + }; + + public ScpResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new ScpResponse(responseApdu); + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs new file mode 100644 index 000000000..81f494266 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs @@ -0,0 +1,65 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// Represents the second command in the SCP03 authentication handshake, 'EXTERNAL_AUTHENTICATE' TODO Fix better docu + /// + internal class ExternalAuthenticateCommand : IYubiKeyCommand + { + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + + private const byte GpExternalAuthenticateCla = 0x80; + private const byte GpExternalAuthenticateIns = 0x82; + private const byte GpHighestSecurityLevel = 0x33; + private readonly ReadOnlyMemory _data; + private readonly byte _keyVersionNumber; + private readonly byte _keyId; + + /// + /// Constructs an EXTERNAL_AUTHENTICATE command, containing the provided data. + /// + /// + /// Clients should not generally build this manually. See for more. + /// + /// Data for the command. E.g. a host cryptogram when authenticating with SCP03 + public ExternalAuthenticateCommand(ReadOnlyMemory data) + { + _data = data; + } + + public ExternalAuthenticateCommand(byte keyVersionNumber, byte keyId, byte[] data) + { + _keyVersionNumber = keyVersionNumber; + _keyId = keyId; + _data = data; + } + + public CommandApdu CreateCommandApdu() => new CommandApdu + { + Cla = GpExternalAuthenticateCla, + Ins = GpExternalAuthenticateIns, + P1 = _keyVersionNumber > 0 ? _keyVersionNumber : GpHighestSecurityLevel, + P2 = _keyId > 0 ? _keyId : default, + Data = _data + }; + + public ExternalAuthenticateResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new ExternalAuthenticateResponse(responseApdu); + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs new file mode 100644 index 000000000..5a2456e75 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs @@ -0,0 +1,41 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + internal class ExternalAuthenticateResponse : ScpResponse + { + /// + /// Constructs an ExternalAuthenticateResponse based on a ResponseApdu received from the YubiKey. + /// + /// The ResponseApdu that corresponds to the issuance of + /// this command. + public ExternalAuthenticateResponse(ResponseApdu responseApdu) : + base(responseApdu) + { + if (responseApdu is null) + { + throw new ArgumentNullException(nameof(responseApdu)); + } + + if (responseApdu.Data.Length != 0) + { + throw new ArgumentException(ExceptionMessages.IncorrectExternalAuthenticateData, nameof(responseApdu)); + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs new file mode 100644 index 000000000..e55e3344c --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs @@ -0,0 +1,55 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + internal class GenerateEcKeyCommand : IYubiKeyCommand + { + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + private readonly ReadOnlyMemory _data; + private readonly byte _keyVersionNumber; + private readonly byte _kid; + + public GenerateEcKeyCommand(byte keyVersionNumber, byte kid, ReadOnlyMemory data) + { + _data = data; + _keyVersionNumber = keyVersionNumber; + _kid = kid; + } + + public CommandApdu CreateCommandApdu() => new CommandApdu + { + Cla = 0x80, + Ins = 0xF1, + P1 = _keyVersionNumber, + P2 = _kid, + Data = _data + }; + + public GenerateEcKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new GenerateEcKeyResponse(responseApdu); + } + + internal class GenerateEcKeyResponse : ScpResponse, IYubiKeyResponseWithData> + { + public GenerateEcKeyResponse(ResponseApdu responseApdu) : base(responseApdu) + { + } + + public ReadOnlyMemory GetData() => ResponseApdu.Data; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs new file mode 100644 index 000000000..711ec1b4a --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs @@ -0,0 +1,72 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// Use this command to delete one of the SCP03 key sets on the YubiKey. + /// + /// + /// See the User's Manual entry on SCP03. + /// + /// This will execute the Delete Command. That is, there is a general purpose + /// command that can delete various elements, including keys. However, this + /// class can build the general purpose delete command in a way that it will + /// only be able to delete keys. + /// + /// + /// Note that if all three key sets are deleted, then the first key set (the + /// key set with a KeyVersionNumber of 1) will be the default key set. + /// + /// + internal class GetDataCommand : IYubiKeyCommand + { + private const byte INS_GET_DATA = 0xCA; + private readonly int _tag; + private readonly ReadOnlyMemory _data; + + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + + public GetDataCommand(int tag, ReadOnlyMemory? data = null) + { + _tag = tag; + _data = data ?? ReadOnlyMemory.Empty; + } + + public CommandApdu CreateCommandApdu() => new CommandApdu + { + Cla = 0, + Ins = INS_GET_DATA, + P1 = (byte)(_tag >> 8), + P2 = (byte)(_tag & 0xFF), + Data = _data + }; + + public GetDataCommandResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new GetDataCommandResponse(responseApdu); + } + + internal class GetDataCommandResponse : ScpResponse, IYubiKeyResponseWithData> + { + public GetDataCommandResponse(ResponseApdu responseApdu) : base(responseApdu) + { + } + + public ReadOnlyMemory GetData() => ResponseApdu.Data; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs new file mode 100644 index 000000000..20f10d516 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs @@ -0,0 +1,59 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// Represents the first command in the SCP03 authentication handshake, 'INITIALIZE_UPDATE' + /// + internal class InitializeUpdateCommand : IYubiKeyCommand + { + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + private const byte GpInitializeUpdateCla = 0x84; + private const byte GpInitializeUpdateIns = 0x50; + private readonly ReadOnlyMemory _hostChallenge; + private readonly int _keyVersionNumber; + + /// + /// Constructs an INITIALIZE_UPDATE command, containing the provided data. + /// + /// + /// Clients should not generally build this manually. See for more. + /// + /// Which key set to use. + /// An 8-byte randomly-generated challenge from the host to the device. + public InitializeUpdateCommand(int keyVersionNumber, ReadOnlyMemory hostChallenge) + { + if (hostChallenge.Length != 8) + { + throw new ArgumentException("Invalid size, must be 8 bytes", nameof(_hostChallenge)); //TODO make localised string + } + + _hostChallenge = hostChallenge; + _keyVersionNumber = keyVersionNumber; + } + + public CommandApdu CreateCommandApdu() => new CommandApdu() + { + Cla = GpInitializeUpdateCla, + Ins = GpInitializeUpdateIns, + P1 = (byte)_keyVersionNumber, + Data = _hostChallenge + }; + public InitializeUpdateResponse CreateResponseForApdu(ResponseApdu responseApdu) => new InitializeUpdateResponse(responseApdu); + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs new file mode 100644 index 000000000..ef72bce88 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs @@ -0,0 +1,54 @@ +// 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 System.Collections.ObjectModel; +using Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + internal class InitializeUpdateResponse : ScpResponse + { + public IReadOnlyCollection DiversificationData { get; protected set; } + public IReadOnlyCollection KeyInfo { get; protected set; } + public IReadOnlyCollection CardChallenge { get; protected set; } + public IReadOnlyCollection CardCryptogram { get; protected set; } + + /// + /// Constructs an InitializeUpdateResponse based on a ResponseApdu received from the YubiKey. + /// + /// The ResponseApdu that corresponds to the issuance of + /// this command. + public InitializeUpdateResponse(ResponseApdu responseApdu) : + base(responseApdu) + { + if (responseApdu is null) + { + throw new ArgumentNullException(nameof(responseApdu)); + } + + if (responseApdu.Data.Length != 29) + { + throw new ArgumentException(ExceptionMessages.IncorrectInitializeUpdateResponseData, nameof(responseApdu)); + } + + var responseData = responseApdu.Data.Span; + DiversificationData = new ReadOnlyCollection(responseData[0..10].ToArray()); + KeyInfo = new ReadOnlyCollection(responseData[10..13].ToArray()); + CardChallenge = new ReadOnlyCollection(responseData[13..21].ToArray()); + CardCryptogram = new ReadOnlyCollection(responseData[21..29].ToArray()); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs new file mode 100644 index 000000000..fb627ffbb --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs @@ -0,0 +1,255 @@ +// 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.IO; +using System.Security.Cryptography; +using Yubico.Core.Iso7816; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp03; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// Use this command to put or replace SCP03 keys on the YubiKey. + /// + /// + /// See the User's Manual entry on SCP03. + /// + /// On each YubiKey that supports SCP03, there is space for three sets of + /// keys. Each set contains three keys: "ENC", "MAC", and "DEK" (Channel + /// Encryption, Channel MAC, and Data Encryption). + /// + /// 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. + /// + /// + /// Note that the standard allows changing one key in a key set. However, + /// YubiKeys only allow calling this command with all three keys. That is, + /// with a YubiKey, it is possible only to set or change all three keys of a + /// set with this command. + /// + /// + /// Standard YubiKeys are manufactured with one key set, and each key in that + /// set is the default value. + /// + /// slot 1: ENC(default) MAC(default) DEK(default) + /// slot 2: --empty-- + /// slot 3: --empty-- + /// + /// The default value is 0x40 41 42 ... 4F. + /// + /// + /// The key sets are not specified using a "slot number", rather, each key + /// set is given a Key Version Number (KVN). Each key in the set is given a + /// Key Identifier (KeyId). If the YubiKey contains the default key, the KVN + /// is 255 (0xFF) and the KeyIds are 1, 2, and 3. + /// + /// slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) + /// slot 2: --empty-- + /// slot 3: --empty-- + /// + /// + /// + /// It is possible to use this command to replace or add a key set. However, + /// if the YubiKey contains only the initial, default keys, then it is only + /// possible to replace that set. For example, suppose you have a YubiKey + /// with the default keys and you try to set the keys in slot 2. The YubiKey + /// will not allow that and will return an error. + /// + /// + /// When you replace the initial, default keys, you must specify the KVN of + /// the new keys, and the KeyId of the ENC key. The KeyId(MAC) is, per the + /// standard, KeyId(ENC) + 1, and the KeyId(DEK) is KeyId(ENC) + 2. For the + /// YubiKey, the KVN must be 1. Also, the YubiKey only allows the number 1 as + /// the KeyId of the ENC key. If you supply any other values for the KVN or + /// KeyId, the YubiKey will return an error. Hence, after replacing the + /// initial, default keys, your three sets of keys will be the following: + /// + /// slot 1: KVN=1 KeyId=1:ENC KeyId=2:MAC KeyId=3:DEK + /// slot 2: --empty-- + /// slot 3: --empty-- + /// + /// + /// + /// In order to add or change the keys, you must supply one of the existing + /// key sets in order to build the SCP03 command and to encrypt and + /// authenticate the new keys. When replacing the initial, default keys, you + /// only have the choice to supply the keys with the KVN of 0xFF. + /// + /// + /// Once you have replaced the original key set, you can use that set to add + /// a second set to slot 2. It's KVN must be 2 and the KeyId of the ENC key + /// must be 1. + /// + /// slot 1: KVN=1 KeyId=1:ENC KeyId=2:MAC KeyId=3:DEK + /// slot 2: KVN=2 KeyId=1:ENC KeyId=2:MAC KeyId=3:DEK + /// slot 3: --empty-- + /// + /// + /// + /// You can use either key set to add a set to slot 3. You can use a key set + /// to replace itself. + /// + /// + internal class PutKeyCommand : IYubiKeyCommand + { + private const byte GpPutKeyCla = 0x84; + private const byte GpPutKeyIns = 0xD8; + private const byte KeyIdentifier = 0x81; + + private const int DataLength = 70; + + private const byte KeyType = 0x88; //AES + private const byte BlockSize = 17; + private const byte AesBlockSize = 16; + private const byte KeyCheckSize = 3; + + private const int ChecksumLength = (3 * KeyCheckSize) + 1; + private const int ChecksumOffsetEnc = 1; + private const int ChecksumOffsetMac = ChecksumOffsetEnc + KeyCheckSize; + private const int ChecksumOffsetDek = ChecksumOffsetMac + KeyCheckSize; + + private readonly byte[] _data; + private readonly byte[] _checksum; + private readonly byte _p1Value; + + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + + /// + /// This is the expected result returned by the YubiKey after completing + /// the command. If you want, compare the PutKeyResponse.GetData + /// with this value to verify the command did indeed do what you expected. + /// + public ReadOnlyMemory ExpectedChecksum => _checksum; + + // The default constructor explicitly defined. We don't want it to be + // used. + private PutKeyCommand() + { + throw new NotImplementedException(); + } + + /// + /// Create a new instance of the command. When this command is executed, + /// the newKeys will be installed as the keys in slot specified by + /// newKeys.KeyVersionNumber. The currentKeys contains the + /// keys used to make the connection. This class needs its + /// KeyVersionNumber and its DEK. + /// + public PutKeyCommand(ScpKeyParameters currentKeys, ScpKeyParameters newKeys) //TODO Is this OK? Behaviour of KVN not clear + { + // If the currentKeys' KVN is not the same as the newKeys' KVN, then + // this command is adding a key set and P1 is zero. Otherwise, this + // command is replacing a key, so set P1 to the KVN of the currentKeys. + _p1Value = currentKeys.KeyReference.VersionNumber == newKeys.KeyReference.VersionNumber + ? currentKeys.KeyReference.VersionNumber + : (byte)0; + + // Build the data portion of the APDU + // new kvn || ENC data || MAC data || DEK data + // where the data for each key is + // key type || len of block || block || len of check || check + // The key type is AES, which is the byte 0x88 + // The block is + // keyLen || encrypted key data + // The check is 3 bytes long. + // So the data will be + // 88 || 11 || 10 || <16 bytes> || 03 || <3-byte check> + // |<-- block -->| + // Because the YubiKey supports only AES-128, and because the key is + // a multiple of 16 bytes, no padding is necessary. Because the + // YubiKey only supports putting all three keys, we can know in + // advance all the lengths, and they will be the same each time. We + // also know in advance all the offsets where every element will go. + _data = new byte[DataLength]; + using var memStream = new MemoryStream(_data); + using var binaryWriter = new BinaryWriter(memStream); + binaryWriter.Write(newKeys.KeyReference.VersionNumber); + + _checksum = new byte[ChecksumLength]; + _checksum[0] = newKeys.KeyReference.VersionNumber; + + //TODO Make work with SCp03, Scp11? + var currentKeys03 = currentKeys as Scp03KeyParameters ?? Scp03KeyParameters.DefaultKey; //TODO Right now this ?? is acceptable, because Scp03Params are the only ones being used + var newKeys03 = newKeys as Scp03KeyParameters ?? Scp03KeyParameters.DefaultKey; + + byte[] currentDek = currentKeys03.StaticKeys.DataEncryptionKey.ToArray(); + + try + { + BuildKeyDataField(binaryWriter, newKeys03.StaticKeys.ChannelEncryptionKey, ChecksumOffsetEnc, currentDek); + BuildKeyDataField(binaryWriter, newKeys03.StaticKeys.ChannelMacKey, ChecksumOffsetMac, currentDek); + BuildKeyDataField(binaryWriter, newKeys03.StaticKeys.DataEncryptionKey, ChecksumOffsetDek, currentDek); + } + finally + { + CryptographicOperations.ZeroMemory(currentDek.AsSpan()); + } + } + + public CommandApdu CreateCommandApdu() => new CommandApdu() + { + Cla = GpPutKeyCla, + Ins = GpPutKeyIns, + P1 = _p1Value, + P2 = KeyIdentifier, + Data = _data + }; + + public PutKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new PutKeyResponse(responseApdu); + + // Build the key's data field. Place it into _data beginning at + // dataOffset. + // Use the keyToBlock to encrypt the 16 bytes 0101...01, use the ms 3 + // bytes as the keyCheck + // Use the encryptionKey to encrypt the keyToBlock. + // + // keyType || block len || block || checkLen || check + // 0x88 || 0x17 || 0x10 || 16-byte encData || 0x03 || 3 bytes + private void BuildKeyDataField( + BinaryWriter binaryWriter, + ReadOnlyMemory keyToBlock, + int checksumOffset, + byte[] encryptionKey) + { + byte[] keyData = keyToBlock.ToArray(); + + try + { + byte[] dataToEncrypt = new byte[AesBlockSize] { //Initialization Vector, IV + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + }; + byte[] checkBlock = AesUtilities.BlockCipher(keyData, dataToEncrypt.AsSpan()); + byte[] encryptedKey = AesUtilities.BlockCipher(encryptionKey, keyToBlock.Span); + + binaryWriter.Write(KeyType); + binaryWriter.Write(BlockSize); + binaryWriter.Write(AesBlockSize); + binaryWriter.Write(encryptedKey); + binaryWriter.Write(KeyCheckSize); + binaryWriter.Write(checkBlock, 0, KeyCheckSize); + Array.Copy(checkBlock, 0, _checksum, checksumOffset, KeyCheckSize); + } + finally + { + CryptographicOperations.ZeroMemory(keyData.AsSpan()); + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs new file mode 100644 index 000000000..dc7695674 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs @@ -0,0 +1,36 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// The response to putting or replacing SCP03 keys on the YubiKey. + /// + internal class PutKeyResponse : ScpResponse, IYubiKeyResponseWithData> + { + private readonly byte[] _checksum; + + public PutKeyResponse(ResponseApdu responseApdu) + : base(responseApdu) + { + _checksum = new byte[responseApdu.Data.Length]; + responseApdu.Data.CopyTo(_checksum); + } + + public ReadOnlyMemory GetData() => _checksum; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs new file mode 100644 index 000000000..8a97be087 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs @@ -0,0 +1,60 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + /// + /// TODO + /// + internal class ResetCommand : IYubiKeyCommand + { + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + private readonly byte[] _data; + private readonly byte _keyVersionNumber; + private readonly byte _kid; + private readonly byte _ins; + + /// + /// TODO + /// + /// + /// Clients should not generally build this manually. + /// + /// + /// Which key set to use. + /// + /// + public ResetCommand(byte ins, byte keyVersionNumber, byte kid, byte[] data) + { + _ins = ins; + _data = data; + _keyVersionNumber = keyVersionNumber; + _kid = kid; + } + + public CommandApdu CreateCommandApdu() => new CommandApdu + { + Cla = 0x84, + Ins = _ins, + P1 = _keyVersionNumber, + P2 = _kid, + Data = _data, + Ne = 0 + }; + public YubiKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => new YubiKeyResponse(responseApdu); + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs new file mode 100644 index 000000000..ba3e4e8d7 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs @@ -0,0 +1,47 @@ +// 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.Diagnostics; +using Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + internal class ScpResponse : YubiKeyResponse + { + public ScpResponse(ResponseApdu responseApdu) : + base(responseApdu) + { + + } + + public virtual new ResponseStatus Status => StatusWord switch + { + SWConstants.Success => ResponseStatus.Success, + _ => ResponseStatus.Failed + }; + + public virtual void ThrowIfFailed(string? message = null) + { + switch (StatusWord) + { + case SWConstants.Success: + Debug.Assert(Status == ResponseStatus.Success); + return; + default: + throw new SecureChannelException(message ?? StatusMessage); + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs new file mode 100644 index 000000000..308d2ec1f --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs @@ -0,0 +1,54 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + internal class SecurityOperationCommand : IYubiKeyCommand //todo visibility of classes? + { + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + + private readonly byte _oceRefVersionNumber; + private readonly byte _p2; + private readonly byte[] _certificates; + + public SecurityOperationCommand(byte oceRefVersionNumber, byte p2, byte[] certificates) + { + _oceRefVersionNumber = oceRefVersionNumber; + _p2 = p2; + _certificates = certificates; + } + + public CommandApdu CreateCommandApdu() => + new CommandApdu + { + Cla = 0x80, + Ins = 0x2A, + P1 = _oceRefVersionNumber, + P2 = _p2, + Data = _certificates + }; + + public SecurityOperationResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new SecurityOperationResponse(responseApdu); + } + + internal class SecurityOperationResponse : ScpResponse + { + public SecurityOperationResponse(ResponseApdu responseApdu) : base(responseApdu) + { + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs new file mode 100644 index 000000000..2ea3ef6a1 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs @@ -0,0 +1,122 @@ +// 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.Security.Cryptography; +using Yubico.Core.Cryptography; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp03; + +namespace Yubico.YubiKey.Scp +{ + internal static class Derivation + { + public const byte DDC_SENC = 0x04; + public const byte DDC_SMAC = 0x06; + public const byte DDC_SRMAC = 0x07; + public const byte DDC_CARD_CRYPTOGRAM = 0x00; + public const byte DDC_HOST_CRYPTOGRAM = 0x01; + + // Derive a key from the challenges. + // This method only supports deriving a 64- or 128-bit result based on + // challenges each of which must be 8 bytes. + // The result (output) will be 8 bytes (outputLenBits = 64 bits) + // or 16 bytes (outputLen = 128 bits). + public static Memory Derive( + byte dataDerivationConstant, + byte outputLenBits, + ReadOnlySpan kdfKey, + ReadOnlySpan hostChallenge, + ReadOnlySpan cardChallenge) + { + if (outputLenBits != 64 && outputLenBits != 128) + { + throw new SecureChannelException(ExceptionMessages.IncorrectDerivationLength); + } + if (hostChallenge.Length != 8 || cardChallenge.Length != 8) + { + throw new SecureChannelException(ExceptionMessages.InvalidChallengeLength); + } + + Span macInp = stackalloc byte[32]; + macInp[11] = dataDerivationConstant; + + // This is the output length. + macInp[14] = outputLenBits; + macInp[15] = 1; + hostChallenge.CopyTo(macInp.Slice(16, 8)); + cardChallenge.CopyTo(macInp.Slice(24, 8)); + + Span cmac = stackalloc byte[16]; + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + cmacObj.CmacInit(kdfKey); + cmacObj.CmacUpdate(macInp); + cmacObj.CmacFinal(cmac); + + if (outputLenBits == 128) // Output is a 128 bit key + { + return cmac.ToArray(); + } + + // Output is a cryptogram + byte[] smallerResult = new byte[8]; + cmac[..8].CopyTo(smallerResult); + + CryptographicOperations.ZeroMemory(cmac); + + return smallerResult; + } + + public static Memory DeriveCryptogram( + byte dataDerivationConstant, + ReadOnlySpan key, + ReadOnlySpan hostChallenge, + ReadOnlySpan cardChallenge) => Derive(dataDerivationConstant, 64, key, hostChallenge, cardChallenge); + + public static SessionKeys DeriveSessionKeysFromStaticKeys( + StaticKeys staticKeys, + ReadOnlySpan hostChallenge, + ReadOnlySpan cardChallenge) + { + Span macKey = stackalloc byte[staticKeys.ChannelMacKey.Length]; + Span encKey = stackalloc byte[staticKeys.ChannelEncryptionKey.Length]; + staticKeys.ChannelMacKey.Span.CopyTo(macKey); + staticKeys.ChannelEncryptionKey.Span.CopyTo(encKey); + + try + { + // If these calls succeed, then control of the created buffers + // will be given to the new SessionKeys object created. That is, + // if these succeed, don't overwrite the returned sensitive data. + // The Derive call can throw an exception. Normally, we would + // want to catch that exception just in case at least one call + // succeeded and we have some sensitive data to overwrite. But + // the Derive call fails if either the host or card challenge + // is not exactly 8 bytes. In that case, the first call would + // fail before generating a result, so there will be no data to + // overwrite. + var SMAC = Derive(DDC_SMAC, 128, macKey, hostChallenge, cardChallenge); + var SENC = Derive(DDC_SENC, 128, encKey, hostChallenge, cardChallenge); + var SRMAC = Derive(DDC_SRMAC, 128, macKey, hostChallenge, cardChallenge); + + return new SessionKeys(SMAC, SENC, SRMAC); + } + finally + { + CryptographicOperations.ZeroMemory(macKey); + CryptographicOperations.ZeroMemory(encKey); + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs new file mode 100644 index 000000000..52585a051 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs @@ -0,0 +1,28 @@ +// 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. + +namespace Yubico.YubiKey.Scp +{ + /// + /// The connection class that can perform SCP03 operations will implement not + /// only , but this interface as well. + /// + public interface IScpYubiKeyConnection : IYubiKeyConnection // TODO Why do I need this? + { + /// + /// Return a reference to the SCP key set used to make the connection. + /// + public ScpKeyParameters KeyParameters { get; } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs new file mode 100644 index 000000000..4d1d53bdd --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs @@ -0,0 +1,55 @@ +// 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 Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp.Commands; + +namespace Yubico.YubiKey.Scp +{ + internal class InternalAuthenticateCommand : IYubiKeyCommand + { + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + + private readonly byte _keyReferenceVersionNumber; + private readonly byte _keyReferenceId; + private readonly byte[] _data; + + public InternalAuthenticateCommand(byte keyReferenceVersionNumber, byte keyReferenceId, byte[] data) + { + _keyReferenceVersionNumber = keyReferenceVersionNumber; + _keyReferenceId = keyReferenceId; + _data = data; + } + + public CommandApdu CreateCommandApdu() => + new CommandApdu + { + Cla = 0x80, + Ins = 0x88, + P1 = _keyReferenceVersionNumber, + P2 = _keyReferenceId, + Data = _data + }; + + public InternalAuthenticateResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new InternalAuthenticateResponse(responseApdu); + } + + internal class InternalAuthenticateResponse : ScpResponse + { + public InternalAuthenticateResponse(ResponseApdu responseApdu) : base(responseApdu) + { + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs new file mode 100644 index 000000000..4b707db19 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs @@ -0,0 +1,52 @@ +// 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.Globalization; + +namespace Yubico.YubiKey.Scp +{ + public class KeyReference + { + public byte Id { get; } + public byte VersionNumber { get; } + + public KeyReference( + byte id, + byte versionNumber + ) + { + Id = id; + VersionNumber = versionNumber; + } + + + public ReadOnlySpan GetBytes => new[] { Id, VersionNumber }.AsSpan(); + + public override string ToString() => $"KeyRef[Kid=0x{Id:X2}, Kvn=0x{VersionNumber:X2}"; + + public override bool Equals(object? obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + var other = (KeyReference)obj; + return Id == other.Id && VersionNumber == other.VersionNumber; + } + + public override int GetHashCode() => HashCode.Combine(Id, VersionNumber); + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs new file mode 100644 index 000000000..e375489d1 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs @@ -0,0 +1,57 @@ +// 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; + +namespace Yubico.YubiKey.Scp +{ + internal static class Padding + { + public static byte[] PadToBlockSize(byte[] payload) + { + if (payload is null) + { + throw new ArgumentNullException(nameof(payload)); + } + + int paddedLen = ((payload.Length / 16) + 1) * 16; + byte[] padded = new byte[paddedLen]; + payload.CopyTo(padded, 0); + padded[payload.Length] = 0x80; + return padded; + } + public static byte[] RemovePadding(byte[] paddedPayload) + { + if (paddedPayload is null) + { + throw new ArgumentNullException(nameof(paddedPayload)); + } + + for (int i = paddedPayload.Length - 1; i >= 0; i--) + { + if (paddedPayload[i] == 0x80) + { + return paddedPayload.Take(i).ToArray(); + } + else if (paddedPayload[i] != 0x00) + { + throw new SecureChannelException(ExceptionMessages.InvalidPadding); + } + } + + throw new SecureChannelException(ExceptionMessages.InvalidPadding); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs new file mode 100644 index 000000000..f2fec4dfa --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs @@ -0,0 +1,45 @@ +// 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 Yubico.YubiKey.Scp03; + +namespace Yubico.YubiKey.Scp +{ + public class Scp03KeyParameters : ScpKeyParameters + { + public StaticKeys StaticKeys { get; } + + public Scp03KeyParameters( + KeyReference keyReference, + StaticKeys staticKeys) : base(keyReference) + { + if (keyReference.Id > 3) + { + throw new ArgumentException("Invalid KID for SCP03", nameof(keyReference.Id)); + } + + StaticKeys = staticKeys; + } + + public Scp03KeyParameters( + byte keyId, + byte keyVersionNumber, + StaticKeys staticKeys) : this(new KeyReference(keyId, keyVersionNumber), staticKeys) + { + } + + public static Scp03KeyParameters DefaultKey => new Scp03KeyParameters(ScpKid.Scp03, 0xFF, new StaticKeys()); + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs new file mode 100644 index 000000000..f3e670a4b --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs @@ -0,0 +1,129 @@ +// 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 Yubico.YubiKey.Scp.Commands; + +namespace Yubico.YubiKey.Scp +{ + internal class Scp03State : ScpState + { + private readonly ReadOnlyMemory _hostCryptogram; + + public Scp03State( + SessionKeys sessionKeys, + Memory hostCryptogram) + : base(sessionKeys, new Memory(new byte[16])) + { + _hostCryptogram = hostCryptogram; + } + + internal static Scp03State CreateScpState( + IApduTransform pipeline, + Scp03KeyParameters keyParameters, + ReadOnlyMemory hostChallenge) + { + var (cardChallenge, cardCryptogram) = PerformInitializeUpdate(pipeline, keyParameters, hostChallenge); + var state = CreateScpState(keyParameters, hostChallenge, cardChallenge, cardCryptogram); + state.PerformExternalAuthenticate(pipeline); + + return state; + } + + private static Scp03State CreateScpState( + Scp03KeyParameters keyParameters, + ReadOnlyMemory hostChallenge, + ReadOnlyMemory cardChallenge, + ReadOnlyMemory cardCryptogram) + { + // Derive session keys + var sessionKeys = Derivation.DeriveSessionKeysFromStaticKeys( + keyParameters.StaticKeys, + hostChallenge.Span, + cardChallenge.Span); + + // Check supplied card cryptogram + var calculatedCardCryptogram = Derivation.DeriveCryptogram( + Derivation.DDC_CARD_CRYPTOGRAM, + sessionKeys.MacKey.Span, + hostChallenge.Span, + cardChallenge.Span); + + if (!CryptographicOperations.FixedTimeEquals(cardCryptogram.Span, calculatedCardCryptogram.Span)) + { + throw new SecureChannelException(ExceptionMessages.IncorrectCardCryptogram); + } + + // Calculate host cryptogram + var hostCryptogram = Derivation.DeriveCryptogram( + Derivation.DDC_HOST_CRYPTOGRAM, + sessionKeys.MacKey.Span, + hostChallenge.Span, + cardChallenge.Span); + + return new Scp03State(sessionKeys, hostCryptogram); + } + + private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCryptogram) + PerformInitializeUpdate( + IApduTransform pipeline, + Scp03KeyParameters keyParameters, + ReadOnlyMemory hostChallenge) + { + var initializeUpdateCommand = new InitializeUpdateCommand( + keyParameters.KeyReference.VersionNumber, hostChallenge); + + var initializeUpdateResponseApdu = pipeline.Invoke( + initializeUpdateCommand.CreateCommandApdu(), + typeof(InitializeUpdateCommand), + typeof(InitializeUpdateResponse)); + + var initializeUpdateResponse = initializeUpdateCommand.CreateResponseForApdu(initializeUpdateResponseApdu); + initializeUpdateResponse.ThrowIfFailed($"Error when performing {initializeUpdateCommand.GetType().Name}: {initializeUpdateResponse.StatusMessage}"); + + var cardChallenge = initializeUpdateResponse.CardChallenge.ToArray().AsMemory(); + var cardCryptogram = initializeUpdateResponse.CardCryptogram.ToArray().AsMemory(); + + return (cardChallenge, cardCryptogram); + } + + private void + PerformExternalAuthenticate( + IApduTransform pipeline) + { + // Create a MAC:ed APDU + var unMacedCommand = new ExternalAuthenticateCommand(_hostCryptogram); + (var macdApdu, byte[] newMacChainingValue) = MacApdu( + unMacedCommand.CreateCommandApdu(), + SessionKeys.MacKey.ToArray(), + MacChainingValue.ToArray() + ); + + // Update sessions / states MacChainingValue + MacChainingValue = newMacChainingValue; + + // Send command + var eaCommand = new ExternalAuthenticateCommand(macdApdu.Data.ToArray()); + var externalAuthenticateResponseApdu = pipeline.Invoke( + eaCommand.CreateCommandApdu(), + typeof(ExternalAuthenticateCommand), + typeof(ExternalAuthenticateResponse)); + + var externalAuthenticateResponse = eaCommand.CreateResponseForApdu(externalAuthenticateResponseApdu); + externalAuthenticateResponse.ThrowIfFailed(); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs new file mode 100644 index 000000000..c27adf92a --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs @@ -0,0 +1,90 @@ +// 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 System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Yubico.YubiKey.Scp +{ + /// + /// SCP key parameters for performing SCP11 authentication. + /// For SCP11b only keyRef and pkSdEcka are required. Note that this does not authenticate the off-card entity. + /// For SCP11a and SCP11c the off-card entity CA key reference must be provided, as well as the off-card entity secret key and certificate chain. + /// + public class Scp11KeyParameters : ScpKeyParameters + { + public ECParameters SecurityDomainEllipticCurveKeyAgreementKeyPublicKey { get; } // TODO Add docs + public KeyReference? OffCardEntityKeyReference { get; } + public ECParameters? OffCardEntityEllipticCurveAgreementPrivateKey { get; } + public IReadOnlyList Certificates { get; } + + public Scp11KeyParameters( + KeyReference keyReference, + ECParameters pkSdEcka, + KeyReference? oceKeyReference = null, + ECParameters? skOceEcka = null, + IEnumerable? certificates = null) + : base(keyReference) + { + + SecurityDomainEllipticCurveKeyAgreementKeyPublicKey = pkSdEcka; + OffCardEntityKeyReference = oceKeyReference; + OffCardEntityEllipticCurveAgreementPrivateKey = skOceEcka; + Certificates = certificates?.ToList() ?? new List(); + + ValidateParameters(); + } + + public Scp11KeyParameters(KeyReference keyReference, ECParameters pkSdEcka) + : this(keyReference, pkSdEcka, null, null, null) + { + + } + + private void ValidateParameters() + { + switch (KeyReference.Id) + { + case ScpKid.Scp11b: + if ( + OffCardEntityKeyReference != null || + OffCardEntityEllipticCurveAgreementPrivateKey != null || + Certificates.Count > 0 + ) + { + throw new ArgumentException("Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); + } + + break; + case ScpKid.Scp11a: + case ScpKid.Scp11c: + if ( + OffCardEntityKeyReference == null || + OffCardEntityEllipticCurveAgreementPrivateKey == null || + Certificates.Count == 0 + ) + { + throw new ArgumentException("Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); + } + + break; + default: + throw new ArgumentException("KID must be 0x11, 0x13, or 0x15 for SCP11"); + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs new file mode 100644 index 000000000..6d0946587 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -0,0 +1,301 @@ +// 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 Yubico.Core.Cryptography; +using Yubico.Core.Iso7816; +using Yubico.Core.Tlv; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp.Commands; +using Yubico.YubiKit.Core.Util; + +namespace Yubico.YubiKey.Scp +{ + internal class Scp11State : ScpState + { + private const int ReceiptTag = 0x86; + private const int EckaTag = 0x5F49; + private const int KeyAgreementTag = 0xA6; + + public Scp11State(SessionKeys sessionKeys, Memory receipt) + : base(sessionKeys, receipt) + { + } + + internal static Scp11State CreateScpState( + IApduTransform pipeline, + Scp11KeyParameters keyParameters) + { + // Perform Security Operation, if needed (for Scp11a and Scp11c) + if (keyParameters.KeyReference.Id == ScpKid.Scp11a || keyParameters.KeyReference.Id == ScpKid.Scp11c) + { + PerformSecurityOperation(pipeline, keyParameters); + } + + var securityDomainPublicKey = keyParameters.SecurityDomainEllipticCurveKeyAgreementKeyPublicKey; + var securityDomainPublicKeyCurve = securityDomainPublicKey.Curve; + + // Generate a public and private key using the supplied curve + var ekpOceEcka = CryptographyProviders.EcdhPrimitivesCreator() + .GenerateKeyPair(securityDomainPublicKeyCurve); + + // Create an encoded point of the ephemeral public key to send to the Yubikey + byte[] ephemeralPublicKeyEncodedPointOceEcka = new byte[65]; + ephemeralPublicKeyEncodedPointOceEcka[0] = 0x04; // Coding Identifier Byte + ekpOceEcka.Q.X.CopyTo(ephemeralPublicKeyEncodedPointOceEcka, 1); + ekpOceEcka.Q.Y.CopyTo(ephemeralPublicKeyEncodedPointOceEcka, 33); + + // GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 + byte[] keyUsage = { 0x3C }; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION + byte[] keyType = { 0x88 }; // AES + byte[] keyLen = { 16 }; // 128-bit + byte[] keyIdentifier = { 0x11, GetScpIdentifierByte(keyParameters.KeyReference) }; + + byte[] hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( + new TlvObject( + KeyAgreementTag, TlvObjects.EncodeMany( + new TlvObject(0x90, keyIdentifier), + new TlvObject(0x95, keyUsage), + new TlvObject(0x80, keyType), + new TlvObject(0x81, keyLen) + )), + new TlvObject(EckaTag, ephemeralPublicKeyEncodedPointOceEcka) + ); + + var authenticateCommand = keyParameters.KeyReference.Id == ScpKid.Scp11b + ? new InternalAuthenticateCommand( + keyParameters.KeyReference.VersionNumber, keyParameters.KeyReference.Id, + hostAuthenticateTlvEncodedData) as IYubiKeyCommand + : new ExternalAuthenticateCommand( + keyParameters.KeyReference.VersionNumber, keyParameters.KeyReference.Id, + hostAuthenticateTlvEncodedData) as IYubiKeyCommand; + + var authenticateResponseApdu = pipeline.Invoke( + authenticateCommand.CreateCommandApdu(), authenticateCommand.GetType(), typeof(ScpResponse)); + + var authenticateResponse = authenticateCommand.CreateResponseForApdu(authenticateResponseApdu); + authenticateResponse.ThrowIfFailed( + $"Error when performing {authenticateCommand.GetType().Name}: {authenticateResponse.StatusMessage}"); + + var authenticateResponseTlvs = TlvObjects.DecodeList(authenticateResponseApdu.Data.Span); + + var epkSdEckaTlv = authenticateResponseTlvs[0]; + var epkSdEckaTlvEncodedData = epkSdEckaTlv.GetBytes(); + var sdReceipt = TlvObjects.UnpackValue( + ReceiptTag, + authenticateResponseTlvs[1].GetBytes().Span); // Yubikey X963KDF Receipt to match with our own X963KDF + + var skOceEcka = + keyParameters + .OffCardEntityEllipticCurveAgreementPrivateKey ?? // If set, we will use this for SCP11A and SCP11C. + ekpOceEcka; // Otherwise, just use the newly created ephemeral key for SCP11b. + + var (encryptionKey, macKey, rMacKey, dekKey) + = GetX963KDFKeyAgreementKeys( + skOceEcka.Curve, + securityDomainPublicKey, + ekpOceEcka, + skOceEcka, + sdReceipt, + epkSdEckaTlvEncodedData, + hostAuthenticateTlvEncodedData, + keyUsage, + keyType, + keyLen); + + var sessionKeys = new SessionKeys( + macKey, + encryptionKey, + rMacKey, + dekKey + ); + + return new Scp11State(sessionKeys, sdReceipt.ToArray()); + } + + private static (Memory encryptionKey, Memory macKey, Memory rMacKey, Memory dekKey) + GetX963KDFKeyAgreementKeys( + ECCurve curve, // The curve being used for the key agreement + ECParameters pkSdEcka, // Yubikey Public Key + ECParameters eskOceEcka, // Host Ephemeral Private Key + ECParameters skOceEcka, // Host Private Key + ReadOnlyMemory sdReceipt, // The receipt computed on the Yubikey + ReadOnlyMemory epkSdEckaTlvEncodedData, // Yubikey Ephemeral Public Key as Tlv Raw Data + ReadOnlyMemory hostAuthenticateTlvEncodedData, + ReadOnlyMemory keyUsage, // The shared key usage + ReadOnlyMemory keyType, // The shared key type + ReadOnlyMemory keyLen) // The shared key length + { + bool allKeysAreSameCurve = new[] + { + pkSdEcka.Curve, // Yubikey Public Key + eskOceEcka.Curve, // Host Ephemeral Private Key + skOceEcka.Curve // Host Private Key + }.All(c => c.Oid == curve.Oid); + + if (!allKeysAreSameCurve) + { + throw new ArgumentException("All curves must be the same"); + } + + // Compute key agreement for: + // Yubikey Ephemeral Public Key + Host Ephemeral Private Key + var ecdhObject = CryptographyProviders.EcdhPrimitivesCreator(); + var epkSdEcka = ExtractPublicKeyEcParameters( + epkSdEckaTlvEncodedData, skOceEcka.Curve); // Yubikey Ephemeral Public Key + + byte[] keyAgreementFirst = ecdhObject.ComputeSharedSecret(epkSdEcka, eskOceEcka.D); + + // Compute key agreement for: + // Yubikey Public Key + Host Private Key + byte[] keyAgreementSecond = ecdhObject.ComputeSharedSecret(pkSdEcka, skOceEcka.D); + + byte[] keyMaterial = MergeArrays(keyAgreementFirst, keyAgreementSecond); + byte[] keyAgreementData = MergeArrays(hostAuthenticateTlvEncodedData, epkSdEckaTlvEncodedData); + byte[] sharedInfo = MergeArrays(keyUsage, keyType, keyLen); + + const int keyCount = 4; + var keys = new List(keyCount); + byte counter = 1; + for (int i = 0; i <= keyCount; i++) + { + using var hash = CryptographyProviders.Sha256Creator(); + + _ = hash.TransformBlock(keyMaterial, 0, keyMaterial.Length, null, 0); + _ = hash.TransformBlock(new byte[] { 0, 0, 0, counter }, 0, 4, null, 0); + _ = hash.TransformFinalBlock(sharedInfo, 0, sharedInfo.Length); + + Span digest = hash.Hash; + keys.Add(digest[..16].ToArray()); + keys.Add(digest[16..].ToArray()); + + ++counter; + CryptographicOperations.ZeroMemory(digest); + } + + // Get keys + byte[] encryptionKey = keys[0]; + byte[] macKey = keys[1]; + byte[] rmacKey = keys[2]; + byte[] dekKey = keys[3]; + + // Do AES CMAC + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + Span oceReceipt = stackalloc byte[16]; // Our generated receipt + cmacObj.CmacInit(encryptionKey); + cmacObj.CmacUpdate(keyAgreementData); + cmacObj.CmacFinal(oceReceipt); + + if (!CryptographicOperations.FixedTimeEquals( + oceReceipt, sdReceipt.Span)) // Needs to match with the receipt generated by the Yubikey + { + throw new SecureChannelException(ExceptionMessages.KeyAgreementReceiptMissmatch); + } + + return (encryptionKey, macKey, rmacKey, dekKey); + } + + private static ECParameters ExtractPublicKeyEcParameters(ReadOnlyMemory epkSdEckaTlv, ECCurve curve) + { + var epkSdEckaEncodedPoint = TlvObjects.UnpackValue(EckaTag, epkSdEckaTlv.Span); + var epkSdEcka = new ECParameters + { + Curve = curve, + Q = new ECPoint + { + X = epkSdEckaEncodedPoint.Span[1..33].ToArray(), + Y = epkSdEckaEncodedPoint.Span[33..].ToArray() + } + }; + + return epkSdEcka; + } + + /// + /// Gets the standardized SCP identifier for the given key reference. + /// Global Platform Secure Channel Protocol 11 Card Specification v2.3 – Amendment F § 7.1.1 + /// + private static byte GetScpIdentifierByte(KeyReference keyReference) => + keyReference.Id switch + { + ScpKid.Scp11a => 0b01, + ScpKid.Scp11b => 0b00, + ScpKid.Scp11c => 0b11, + _ => throw new ArgumentException("Invalid SCP11 KID") + }; + + private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyParameters keyParams) + { + // GPC v2.3 Amendment F (SCP11) v1.4 §7.5 + if (keyParams.OffCardEntityEllipticCurveAgreementPrivateKey == null) + { + throw new ArgumentNullException( + nameof(keyParams.OffCardEntityEllipticCurveAgreementPrivateKey), + "SCP11a and SCP11c require a private key"); + } + + int n = keyParams.Certificates.Count - 1; + if (n < 0) + { + throw new ArgumentException( + "SCP11a and SCP11c require a certificate chain", nameof(keyParams.Certificates)); + } + + var oceRef = keyParams.OffCardEntityKeyReference ?? new KeyReference(0, 0); + for (int i = 0; i <= n; i++) + { + byte[] certificates = keyParams.Certificates[i].RawData; + byte oceRefPadded = (byte)(oceRef.Id | (i < n + ? 0b10000000 + : 0x00)); // Is this a good name? + + var securityOperationCommand = new SecurityOperationCommand( + oceRef.VersionNumber, + oceRefPadded, + certificates); + + // Send payload + var responseSecurityOperation = pipeline.Invoke( + securityOperationCommand.CreateCommandApdu(), + typeof(SecurityOperationCommand), + typeof(SecurityOperationResponse)); + + if (responseSecurityOperation.SW != SWConstants.Success) + { + throw new SecureChannelException( + $"Security operation failed. Status: {responseSecurityOperation.SW:X4}"); + } + } + } + + private static byte[] MergeArrays(params ReadOnlyMemory[] values) + { + using var memoryStream = new MemoryStream(); + foreach (var bytes in values) + { +#if NETSTANDARD2_1_OR_GREATER + memoryStream.Write(bytes.Span); +#else + memoryStream.Write(bytes.Span.ToArray(), 0, bytes.Length); +#endif + } + + return memoryStream.ToArray(); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs new file mode 100644 index 000000000..34f559882 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs @@ -0,0 +1,104 @@ +// 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 Yubico.Core.Devices.SmartCard; +using Yubico.YubiKey.Pipelines; + +namespace Yubico.YubiKey.Scp +{ + internal class ScpConnection : SmartCardConnection, IScpYubiKeyConnection + { + private bool _disposed; + private readonly ScpApduTransform _scpApduTransform; + + public ScpConnection( + ISmartCardDevice smartCardDevice, + YubiKeyApplication yubiKeyApplication, + ScpKeyParameters keyParameters) + : base(smartCardDevice, yubiKeyApplication, null) // TODO Consider this, dont use this constructor + { + // _scpApduTransform = SetObject(yubiKeyApplication, scpKeys); TODO Is this method really needed? + + var previousPipeline = GetPipeline(); + var nextPipeline = new ScpApduTransform(previousPipeline, keyParameters); + + // Set parent pipeline + SetPipeline(nextPipeline); + nextPipeline.Setup(); + + _scpApduTransform = nextPipeline; + } + + // public ScpConnection( + // ISmartCardDevice smartCardDevice, + // ReadOnlyMemory applicationId, + // ScpKeyParameters scpKeys) + // : base(smartCardDevice, YubiKeyApplication.Unknown, applicationId.ToArray()) //TODO Consider using the Span + // { + // var application = YubiKeyApplication.Unknown; + // if (applicationId.Span.SequenceEqual(YubiKeyApplication.Fido2.GetIso7816ApplicationId())) + // { + // application = YubiKeyApplication.Fido2; + // } + // else if (applicationId.Span.SequenceEqual(YubiKeyApplication.Otp.GetIso7816ApplicationId())) + // { + // application = YubiKeyApplication.Otp; + // } + // + // _scpApduTransform = SetObject(application, scpKeys); + // } + + public ScpKeyParameters KeyParameters => _scpApduTransform.KeyParameters; + + // private ScpApduTransform SetObject( + // YubiKeyApplication application, + // ScpKeyParameters keyParameters) + // { + // var previousPipeline = GetPipeline(); + // var appendedPipeline = new ScpApduTransform(previousPipeline, keyParameters); + // + // // Is it even possible to connect to Fido2 and Otp with SCP? + // // IApduTransform apduPipeline = application switch + // // { + // // YubiKeyApplication.Fido2 => new FidoErrorTransform(appendedPipeline), + // // YubiKeyApplication.Otp => new OtpErrorTransform(appendedPipeline), + // // _ => appendedPipeline + // // }; + // + // // Set parent pipeline + // // SetPipeline(apduPipeline); + // // apduPipeline.Setup(); + // + // // Set parent pipeline + // SetPipeline(appendedPipeline); + // appendedPipeline.Setup(); + // + // return appendedPipeline; + // } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _scpApduTransform.Dispose(); + _disposed = true; + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs new file mode 100644 index 000000000..ad070bac4 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs @@ -0,0 +1,30 @@ +// 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; + +namespace Yubico.YubiKey.Scp +{ + public abstract class ScpKeyParameters //TODO handle dispose like static keys? Use ReadOnlyMemorySpan? + { + public KeyReference KeyReference { get; protected set; } + + public ReadOnlySpan GetBytes => new ReadOnlySpan(new[] { KeyReference.Id, KeyReference.VersionNumber }); + + protected ScpKeyParameters(KeyReference keyReference) + { + KeyReference = keyReference; + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs new file mode 100644 index 000000000..3e7d39bbe --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs @@ -0,0 +1,24 @@ +// 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. + +namespace Yubico.YubiKey.Scp +{ + public static class ScpKid + { + public const byte Scp03 = 0x01; + public const byte Scp11a = 0x11; + public const byte Scp11b = 0x13; + public const byte Scp11c = 0x15; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs new file mode 100644 index 000000000..a807cf4ac --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs @@ -0,0 +1,150 @@ +using System; +using Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp +{ + internal abstract class ScpState : IDisposable + { + protected readonly SessionKeys SessionKeys; + protected Memory MacChainingValue; + + private int _encryptionCounter = 1; + private bool _disposed; + + /// + /// Initializes the host-side state for an SCP session. + /// + public ScpState(SessionKeys sessionKeys, Memory macChain) + { + MacChainingValue = macChain; + SessionKeys = sessionKeys; + } + + /// + /// Encodes (encrypt then MAC) a command using SCP03. Modifies state, + /// and must be sent in-order. Must be called after LoadInitializeUpdate. + /// + /// + public CommandApdu EncodeCommand(CommandApdu command) + { + if (SessionKeys == null) + { + throw new InvalidOperationException(ExceptionMessages.UnknownScpError); + } + + if (command is null) + { + throw new ArgumentNullException(nameof(command)); + } + + var encodedCommand = new CommandApdu + { + Cla = (byte)(command.Cla | 0x04), //0x04 is for secure-messaging + Ins = command.Ins, + P1 = command.P1, + P2 = command.P2 + }; + + byte[] commandData = command.Data.ToArray(); + byte[] encryptedData = ChannelEncryption.EncryptData( + commandData, SessionKeys.EncKey.Span, _encryptionCounter); + + _encryptionCounter++; + encodedCommand.Data = encryptedData; + + // Create a MAC:ed APDU + (var macdApdu, byte[] newMacChainingValue) = MacApdu( + encodedCommand, + SessionKeys.MacKey.Span, + MacChainingValue.Span); + + // Update the sessions MacChainingValue + MacChainingValue = newMacChainingValue; + + return macdApdu; + } + + /// + /// Decodes (verify RMAC then decrypt) a raw response from the device. + /// + /// + /// + public ResponseApdu DecodeResponse(ResponseApdu response) + { + if (SessionKeys is null) + { + throw new InvalidOperationException(ExceptionMessages.UnknownScpError); + } + + if (response is null) + { + throw new ArgumentNullException(nameof(response)); + } + + // If the response is not Success, just return the response. The + // standard says, "No R-MAC shall be generated and no protection + // shall be applied to a response that includes an error status word: + // in this case only the status word shall be returned in the + // response." + if (response.SW != SWConstants.Success) + { + return response; + } + + // ALWAYS check RMAC before decryption + var responseData = response.Data; + VerifyRmac(responseData.Span, SessionKeys.RmacKey.Span, MacChainingValue.Span); + + byte[] decryptedData = Array.Empty(); + if (responseData.Length > 8) + { + int previousEncryptionCounter = _encryptionCounter - 1; + decryptedData = ChannelEncryption.DecryptData( + + responseData[..^8].ToArray(), + SessionKeys.EncKey.ToArray(), //todo array + previousEncryptionCounter + ); + } + + byte[] fullDecryptedResponse = new byte[decryptedData.Length + 2]; + decryptedData.CopyTo(fullDecryptedResponse, 0); + fullDecryptedResponse[decryptedData.Length] = response.SW1; + fullDecryptedResponse[decryptedData.Length + 1] = response.SW2; + return new ResponseApdu(fullDecryptedResponse); + } + + #pragma warning disable CA1822 // Is being used by subclasses + protected (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( + #pragma warning restore CA1822 + CommandApdu commandApdu, + ReadOnlySpan macKey, + ReadOnlySpan macChainingValue) => + ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); + + protected static void VerifyRmac( + ReadOnlySpan responseData, + ReadOnlySpan rmacKey, + ReadOnlySpan macChainingValue) => + ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + SessionKeys?.Dispose(); + + _disposed = true; + } + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs new file mode 100644 index 000000000..46f184937 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs @@ -0,0 +1,58 @@ +// 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. + +namespace Yubico.YubiKey.Scp +{ + /// + /// TODO DO I EVEN NEED THIS CLASS? + /// + internal class ScpYubiKeyDevice : YubiKeyDevice + { + public ScpKeyParameters KeyParameters { get; private set; } + + public ScpYubiKeyDevice( + YubiKeyDevice device, + ScpKeyParameters keyParameters) + : base(device.GetSmartCardDevice(), + null, + null, + device) + { + KeyParameters = keyParameters; + } + + // internal override IYubiKeyConnection? Connect( + // YubiKeyApplication application, + // ScpKeyParameters scpKeys) + // { + // if (!HasSmartCard) + // { + // return null; + // } + // + // //Scp3 check + // + // if (scpKeys is Scp03KeyParameters scp03) + // { + // if (KeyParameters is Scp03KeyParameters deviceIsScp03 && // TODO Determine or set type of ScpDevice earlier? + // !deviceIsScp03.StaticKeys.AreKeysSame(scp03.StaticKeys)) + // { + // return null; + // } + // } + // + // return new ScpConnection(GetSmartCardDevice(), application, KeyParameters); + // } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs new file mode 100644 index 000000000..107c36cb7 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs @@ -0,0 +1,43 @@ +// 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; + +namespace Yubico.YubiKey.Scp +{ + /// + /// Represents errors that occur during encoding or decoding data for SCP03. + /// +#pragma warning disable CA1064 // Exceptions should be public + internal class SecureChannelException : Exception +#pragma warning restore CA1064 // Exceptions should be public + { + public SecureChannelException() + { + + } + + public SecureChannelException(string message) : + base($"SCP03 CardDataException: {message}") + { + + } + + public SecureChannelException(string message, Exception e) : + base($"SCP03 CardDataException: {message}", e) + { + + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs new file mode 100644 index 000000000..78b64b867 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -0,0 +1,557 @@ +// 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.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; +using Yubico.Core.Iso7816; +using Yubico.Core.Logging; +using Yubico.Core.Tlv; +using Yubico.YubiKey.Scp.Commands; +using Yubico.YubiKit.Core.Util; + +namespace Yubico.YubiKey.Scp +{ + /// + /// Create a session for managing the SCP configuration of a YubiKey. + /// + /// + /// See the User's Manual entry on SCP. + /// + /// Usually, you use SCP "in the background" to secure the communication + /// with another application. For example, when you want to perform PIV + /// operations, but need to send the commands to and get the responses from + /// the YubiKey securely (such as sending commands remotely where + /// authenticity and confidentiality are required), you use SCP. + /// + /// if (YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) + /// { + /// using (var pivSession = new PivSession(scpDevice, scpKeys)) + /// { + /// . . . + /// } + /// } + /// + /// + /// + /// However, there are times you need to manage the configuration of SCP + /// directly, not as simply the security layer for a PIV or other + /// applications. The most common operations are loading and deleting SCP + /// key sets on the YubiKey. + /// + /// + /// For the SCP configuration management operations, use the + /// ScpSession class. + /// + /// + /// Once you have the YubiKey to use, you will build an instance of this + /// ScpSession class to represent the SCP on the hardware. + /// Because this class implements IDisposable, use the using + /// keyword. For example, + /// + /// if (YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) + /// { + /// var scpKeys = new StaticKeys(); + /// using (var scp = new ScpSession(yubiKeyDevice, scpKeys)) + /// { + /// // Perform SCP operations. + /// } + /// } + /// + /// + /// + /// If the YubiKey does not support SCP, the constructor will throw an + /// exception. + /// + /// + /// If the StaticKeys provided are not correct, the constructor will throw an + /// exception. + /// + /// + public sealed class SecurityDomainSession : IDisposable + { + private readonly IYubiKeyDevice _yubiKey; + private readonly ILogger _log = Log.GetLogger(); + private bool _disposed; + + /// + /// The object that represents the connection to the YubiKey. Most + /// applications will ignore this, but it can be used to call Commands + /// directly. + /// + private IScpYubiKeyConnection? Connection { get; } + + // The default constructor explicitly defined. We don't want it to be + // used. + private SecurityDomainSession() + { + throw new NotImplementedException(); + } + + /// + /// Create an instance of , the object that + /// represents SCP on the YubiKey. + /// + /// + /// See the User's Manual entry on SCP. + /// + /// Because this class implements IDisposable, use the using + /// keyword. For example, + /// + /// if (YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) + /// { + /// var staticKeys = new StaticKeys(); + /// // Note that you do not need to call the "WithScp" method when + /// // using the ScpSession class. + /// using (var scp = new ScpSession(yubiKeyDevice, staticKeys)) + /// { + /// // Perform SCP operations. + /// } + /// } + /// + /// + /// + /// + /// The object that represents the actual YubiKey which will perform the + /// operations. + /// + /// + /// The shared secret keys that will be used to authenticate the caller + /// and encrypt the communications. This constructor will make a deep + /// copy of the keys, it will not copy a reference to the object. + /// + /// + /// The yubiKey or scpKeys argument is null. + /// + public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeys) + { + _log.LogInformation("Create a new instance of ScpSession."); + + if (yubiKey is null) + { + throw new ArgumentNullException(nameof(yubiKey)); + } + + if (scpKeys is null) + { + throw new ArgumentNullException(nameof(scpKeys)); + } + + _yubiKey = yubiKey; + Connection = yubiKey.ConnectScp(YubiKeyApplication.SecurityDomain, scpKeys); + } + + /// + /// Create an unauthenticated instance of , the object that + /// represents SCP on the YubiKey. + /// + /// Sessions created from this constructor will not be able to perform operations which require authentication + /// + /// The object that represents the actual YubiKey which will perform the + /// operations. + /// + /// + /// The yubiKey or scpKeys argument is null. + /// + public SecurityDomainSession(IYubiKeyDevice yubiKey) + { + _log.LogInformation("Create a new instance of ScpSession."); + _yubiKey = yubiKey ?? throw new ArgumentNullException(nameof(yubiKey)); + } + + /// + /// Put the given key set onto the YubiKey. + /// + /// + /// See the User's Manual entry on + /// SCP. + /// + /// On each YubiKey that supports SCP, there is space for three sets of + /// keys. Each set contains three keys: "ENC", "MAC", and "DEK" (Channel + /// Encryption, Channel MAC, and Data Encryption). + /// + /// 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. + /// + /// + /// Note that the standard allows changing one key in a key set. However, + /// YubiKeys only allow calling this command with all three keys. That is, + /// with a YubiKey, it is possible only to set or change all three keys of a + /// set. + /// + /// + /// Standard YubiKeys are manufactured with one key set, and each key in that + /// set is the default value. + /// + /// slot 1: ENC(default) MAC(default) DEK(default) + /// slot 2: --empty-- + /// slot 3: --empty-- + /// + /// The default value is 0x40 41 42 ... 4F. + /// + /// + /// The key sets are not specified using a "slot number", rather, each key + /// set is given a Key Version Number (KVN). Each key in the set is given a + /// Key Identifier (KeyId). The YubiKey allows only 1, 2, and 3 as the + /// KeyIds, and SDK users never need to worry about them. If the YubiKey + /// contains the default key, the KVN is 255 (0xFF). + /// + /// slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) + /// slot 2: --empty-- + /// slot 3: --empty-- + /// + /// + /// + /// It is possible to use this method to replace or add a key set. However, + /// if the YubiKey contains only the initial, default keys, then it is only + /// possible to replace that set. For example, suppose you have a YubiKey + /// with the default keys and you try to set the keys in slot 2. The YubiKey + /// will not allow that and will return an error. + /// + /// + /// When you replace the initial, default keys, you must specify the KVN of + /// the new keys. For the YubiKey, in this situation, the KVN must be 1. + /// If you supply any other values for the KVN, the YubiKey will return + /// an error. Hence, after replacing the initial, default keys, your + /// three sets of keys will be the following: + /// + /// slot 1: KVN=1 newENC newMAC newDEK + /// slot 2: --empty-- + /// slot 3: --empty-- + /// + /// + /// + /// In order to add or change any key set, you must supply one of the existing + /// key sets in order to build the SCP command and to encrypt and + /// authenticate the new keys. When replacing the initial, default keys, you + /// only have the choice to supply the keys with the KVN of 0xFF. + /// + /// + /// Once you have replaced the original key set, you can use that set to add + /// a second set to slot 2. It's KVN must be 2. + /// + /// slot 1: KVN=1 ENC MAC DEK + /// slot 2: KVN=2 ENC MAC DEK + /// slot 3: --empty-- + /// + /// + /// + /// You can use either key set to add a set to slot 3. You can use a key set + /// to replace itself. + /// + /// + /// + /// The keys and KeyVersion Number of the set that will be loaded onto + /// the YubiKey. + /// + /// + /// The newKeySet argument is null. + /// + /// + /// The new key set's checksum failed to verify, or some other error + /// described in the exception message. + /// + public void PutKeySet(ScpKeyParameters keyParameters) + { + if (Connection is null) + { + throw new InvalidOperationException("No connection initialized. Use the other constructor"); + } + + _log.LogInformation("Put a new SCP key set onto a YubiKey."); + + if (keyParameters is null) + { + throw new ArgumentNullException(nameof(keyParameters)); + } + + //TODO PutKeyCommand for each, or make a generic one that handles all cases of Scp? + var command = new PutKeyCommand(Connection.KeyParameters, keyParameters); + var response = Connection.SendCommand(command); + if (response.Status != ResponseStatus.Success) + { + throw new SecureChannelException( + string.Format( + CultureInfo.CurrentCulture, + ExceptionMessages.YubiKeyOperationFailed, + response.StatusMessage)); + } + + var checksum = response.GetData(); + if (!CryptographicOperations.FixedTimeEquals(checksum.Span, command.ExpectedChecksum.Span)) + { + throw new SecureChannelException(ExceptionMessages.ChecksumError); + } + } + + /// + /// Delete the key set with the given keyVersionNumber. If the key + /// set to delete is the last SCP key set on the YubiKey, pass + /// true as the isLastKey arg. + /// + /// + /// The key set used to create the SCP session cannot be the key set to + /// be deleted, unless both of the other key sets have been deleted, and + /// you pass true for isLastKey. In this case, the key will + /// be deleted but the SCP application on the YubiKey will be reset + /// with the default key. + /// + /// + /// The number specifying which key set to delete. + /// + /// + /// If this key set is the last SCP key set on the YubiKey, pass + /// true, otherwise, pass false. This arg has a default of + /// false so if no argument is given, it will be false. + /// + public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) + { + if (Connection is null) + { + throw new InvalidOperationException("No connection initialized. Use the other constructor"); + } + + _log.LogInformation("Deleting an SCP key set from a YubiKey."); + + var command = new DeleteKeyCommand(keyVersionNumber, isLastKey); + var response = Connection.SendCommand(command); + if (response.Status != ResponseStatus.Success) + { + throw new SecureChannelException( + string.Format( + CultureInfo.CurrentCulture, + ExceptionMessages.YubiKeyOperationFailed, + response.StatusMessage)); + } + } + + /// + /// Generate a new ECC key pair for the given key reference. + /// + /// + /// GlobalPlatform has no command to generate key pairs on the card itself. This is a + /// Yubico extension that tries to mimic the format of the GPC PUT KEY + /// command. + /// + /// The KID-KVN pair of the key that should be generated. + /// The key version number of the key set that should be replaced, or 0 to generate a new key pair. + /// The parameters of the generated key, including the curve and the public point. + /// + public ECParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) + { + var connection = Connection ?? + throw new InvalidOperationException("No connection initialized. Use the other constructor"); + + _log.LogDebug( + "Generating new key for {KeyRef}{ReplaceMessage}", + keyRef, + replaceKvn == 0 + ? string.Empty + : $", replacing KVN=0x{replaceKvn:X2}"); + + const byte keyTypeEccKeyParamsTag = 0xF0; + var paramsTlv = new TlvObject(keyTypeEccKeyParamsTag, new byte[] { 0 }).GetBytes(); + byte[] commandData = new byte[paramsTlv.Length + 1]; + commandData[0] = keyRef.VersionNumber; + paramsTlv.CopyTo(commandData.AsMemory(1)); + + var command = new GenerateEcKeyCommand(replaceKvn, keyRef.Id, commandData); + var response = connection.SendCommand(command); + if (response.Status != ResponseStatus.Success) + { + throw new SecureChannelException(response.StatusMessage); + } + + const byte keyTypeEccPublicKeyTag = 0xB0; + var tlvReader = new TlvReader(response.GetData()); + var encodedPoint = tlvReader.ReadValue(keyTypeEccPublicKeyTag).Span; + + return new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new ECPoint + { + X = encodedPoint.Slice(1, 32).ToArray(), + Y = encodedPoint.Slice(33, 32).ToArray() + } + }; + } + + /// + /// Perform a factory reset of the Security Domain. + /// This will remove all keys and associated data, as well as restore the default SCP03 static keys, + /// and generate a new (attestable) SCP11b key. + /// + public void Reset() + { + _log.LogDebug("Resetting all SCP keys"); + + var connection = _yubiKey.Connect(YubiKeyApplication.SecurityDomain); + + const byte insInitializeUpdate = 0x50; + const byte insExternalAuthenticate = 0x82; + const byte insInternalAuthenticate = 0x88; + const byte insPerformSecurityOperation = 0x2A; + + var keys = GetKeyInformation().Keys; + foreach (var keyRef in keys) // Reset is done by blocking all available keys + { + byte ins; + var overridenKeyRef = keyRef; + + switch (keyRef.Id) + { + case ScpKid.Scp03: + // SCP03 uses KID=0, we use KVN=0 to allow deleting the default keys + // which have an invalid KVN (0xff). + overridenKeyRef = new KeyReference(0, 0); + ins = insInitializeUpdate; + break; + case 0x02: + case 0x03: + continue; // Skip these as they are deleted by 0x01 + case ScpKid.Scp11a: + case ScpKid.Scp11c: + ins = insExternalAuthenticate; + break; + case ScpKid.Scp11b: + ins = insInternalAuthenticate; + break; + default: // 0x10, 0x20-0x2F + ins = insPerformSecurityOperation; + break; + } + + // Keys have 65 attempts before blocking (and thus removal) + for (int i = 0; i < 65; i++) + { + var result = connection.SendCommand( + new ResetCommand(ins, overridenKeyRef.VersionNumber, overridenKeyRef.Id, new byte[8])); + + switch (result.StatusWord) + { + case SWConstants.AuthenticationMethodBlocked: + case SWConstants.SecurityStatusNotSatisfied: + i = 65; + break; + case SWConstants.InvalidCommandDataParameter: + continue; + default: continue; + } + } + } + + _log.LogInformation("SCP keys reset"); + } + + /// + /// Retrieves the key information stored in the YubiKey and returns it in a dictionary format. + /// + /// A read only dictionary containing the KeyReference as the key and a dictionary of key components as the value. + public IReadOnlyDictionary> GetKeyInformation() + { + const byte tagKeyInformation = 0xE0; + + var keys = new Dictionary>(); + var tlvDataList = TlvObjects.DecodeList(GetData(tagKeyInformation).Span); + foreach (var tlvObject in tlvDataList) + { + var value = TlvObjects.UnpackValue(0xC0, tlvObject.GetBytes().Span); + var keyRef = new KeyReference(value.Span[0], value.Span[1]); + var keyComponents = new Dictionary(); + + while (!(value = value[2..]).IsEmpty) + { + keyComponents.Add(value.Span[0], value.Span[1]); + } + + keys.Add(keyRef, keyComponents); + } + + return new ReadOnlyDictionary>(keys); + } + + /// + /// Retrieves the certificates associated with the given . + /// + /// The key reference for which the certificates should be retrieved. + /// A list of X.509 certificates associated with the key reference. + public IReadOnlyList GetCertificates(KeyReference keyReference) + { + _log.LogInformation("Getting certificates for key={KeyRef}", keyReference); + + const int certificateStoreTag = 0xBF21; + const int controlReferenceTemplateTag = 0xA6; + const int kidKvnTag = 0x83; + + var nestedTlv = new TlvObject( + controlReferenceTemplateTag, + new TlvObject(kidKvnTag, keyReference.GetBytes).GetBytes().Span + ).GetBytes(); + + var certificateTlvData = GetData(certificateStoreTag, nestedTlv); + var certificateTlvList = TlvObjects.DecodeList(certificateTlvData.Span); + + return certificateTlvList + .Select(tlv => new X509Certificate2(tlv.GetBytes().ToArray())) + .ToList(); + } + + /// + /// Gets data from the YubiKey associated with the given tag. + /// + /// The tag of the data to retrieve. + /// Optional data to send with the command. + /// The data retrieved from the YubiKey. + public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) + { + var connection = Connection ?? _yubiKey.Connect(YubiKeyApplication.SecurityDomain); + var response = connection.SendCommand(new GetDataCommand(tag, data)); + + return response.GetData(); + } + + /// + /// When the ScpSession object goes out of scope, this method is called. + /// It will close the session. The most important function of closing a + /// session is to close the connection. + /// + + // Note that .NET recommends a Dispose method call Dispose(true) and + // GC.SuppressFinalize(this). The actual disposal is in the + // Dispose(bool) method. + // However, that does not apply to sealed classes. + // So the Dispose method will simply perform the + // "closing" process, no call to Dispose(bool) or GC. + public void Dispose() + { + if (_disposed) + { + return; + } + + Connection?.Dispose(); + + _disposed = true; + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs new file mode 100644 index 000000000..e75c35f82 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs @@ -0,0 +1,94 @@ +// 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.Security.Cryptography; + +namespace Yubico.YubiKey.Scp +{ + internal class SessionKeys : IDisposable + { + /// + /// Gets the session MAC key. + /// + public ReadOnlyMemory MacKey => _macKey; + /// + /// Gets the session encryption key. + /// + public ReadOnlyMemory EncKey => _encryptionKey; + /// + /// Gets the session RMAC key. + /// + public ReadOnlyMemory RmacKey => _rmacKey; + /// + /// Gets the session data encryption key. + /// This is only set for SCP-11 + /// + public ReadOnlyMemory? DataEncryptionKey => _dataEncryptionKey; + + private readonly Memory _macKey; + private readonly Memory _encryptionKey; + private readonly Memory _rmacKey; + private readonly Memory? _dataEncryptionKey; + private bool _disposed; + + /// + /// Session keys for Secure Channel Protocol (SCP). + /// + /// The session MAC key. + /// The session encryption key. + /// The session RMAC key. + /// The session data encryption key. Optional. + public SessionKeys( + Memory macKey, + Memory encryptionKey, + Memory rmacKey, + Memory? dataEncryptionKey = null) + { + _macKey = macKey; + _encryptionKey = encryptionKey; + _rmacKey = rmacKey; + _dataEncryptionKey = dataEncryptionKey; + + _disposed = false; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // Overwrite the memory of the keys + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (!disposing) + { + return; + } + + CryptographicOperations.ZeroMemory(_macKey.Span); + CryptographicOperations.ZeroMemory(_encryptionKey.Span); + CryptographicOperations.ZeroMemory(_rmacKey.Span); + CryptographicOperations.ZeroMemory(_dataEncryptionKey.GetValueOrDefault().Span); + + _disposed = true; + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs index 384154a78..b38d8e85a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs @@ -28,7 +28,7 @@ public static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu(Command { if (macChainingValue.Length != 16) { - throw new ArgumentException(ExceptionMessages.UnknownScp03Error, nameof(macChainingValue)); + throw new ArgumentException(ExceptionMessages.UnknownScpError, nameof(macChainingValue)); } var apduWithLongerLen = AddDataToApdu(apdu, new byte[8]); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs index c83e74a13..6bdf13dd1 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs @@ -23,8 +23,8 @@ namespace Yubico.YubiKey.Scp03.Commands internal class InitializeUpdateCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; - const byte GpInitializeUpdateCla = 0b1000_0000; - const byte GpInitializeUpdateIns = 0x50; + private const byte GpInitializeUpdateCla = 0x80; + private const byte GpInitializeUpdateIns = 0x50; private readonly byte[] _challenge; private readonly int _keyVersionNumber; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs index 6e2cb1b69..2208f22e8 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Linq; using Yubico.Core.Devices.SmartCard; using Yubico.YubiKey.Pipelines; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey { + [Obsolete("Obsolete")] internal class Scp03Connection : SmartCardConnection, IScp03YubiKeyConnection { private bool _disposed; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs index a29447877..be49667db 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs @@ -87,6 +87,7 @@ public sealed class Scp03Session : IDisposable /// applications will ignore this, but it can be used to call Commands /// directly. /// + [Obsolete("Obsolete")] public IScp03YubiKeyConnection Connection { get; private set; } // The default constructor explicitly defined. We don't want it to be @@ -131,6 +132,7 @@ private Scp03Session() /// /// The yubiKey or scp03Keys argument is null. /// + [Obsolete("Use new Scp")] public Scp03Session(IYubiKeyDevice yubiKey, StaticKeys scp03Keys) { _log.LogInformation("Create a new instance of Scp03Session."); @@ -242,6 +244,7 @@ public Scp03Session(IYubiKeyDevice yubiKey, StaticKeys scp03Keys) /// The new key set's checksum failed to verify, or some other error /// described in the exception message. /// + [Obsolete("Obsolete")] public void PutKeySet(StaticKeys newKeySet) { _log.LogInformation("Put a new SCP03 key set onto a YubiKey."); @@ -289,6 +292,7 @@ public void PutKeySet(StaticKeys newKeySet) /// true, otherwise, pass false. This arg has a default of /// false so if no argument is given, it will be false. /// + [Obsolete("Obsolete")] public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) { _log.LogInformation("Deleting an SCP03 key set from a YubiKey."); @@ -317,6 +321,7 @@ public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) // However, that does not apply to sealed classes. // So the Dispose method will simply perform the // "closing" process, no call to Dispose(bool) or GC. + [Obsolete("Obsolete")] public void Dispose() { if (_disposed) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs index 14b5e0451..548f9c895 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey @@ -20,12 +21,19 @@ internal class Scp03YubiKeyDevice : YubiKeyDevice { public StaticKeys StaticKeys { get; private set; } - public Scp03YubiKeyDevice(YubiKeyDevice device, StaticKeys staticKeys) - : base(device.GetSmartCardDevice(), null, null, device) + public Scp03YubiKeyDevice( + YubiKeyDevice device, + StaticKeys staticKeys) + : base( + device.GetSmartCardDevice(), + null, + null, + device) { StaticKeys = staticKeys.GetCopy(); } + [Obsolete("Obsolete")] internal override IYubiKeyConnection? Connect( YubiKeyApplication? application, byte[]? applicationId, @@ -36,7 +44,7 @@ public Scp03YubiKeyDevice(YubiKeyDevice device, StaticKeys staticKeys) return null; } - if (!(scp03Keys is null) && !StaticKeys.AreKeysSame(scp03Keys)) + if (scp03Keys != null && !StaticKeys.AreKeysSame(scp03Keys)) { return null; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs index 246476971..d7fc55cf6 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs @@ -171,7 +171,7 @@ public CommandApdu EncodeCommand(CommandApdu command) { if (_sessionKeys == null) { - throw new InvalidOperationException(ExceptionMessages.UnknownScp03Error); + throw new InvalidOperationException(ExceptionMessages.UnknownScpError); } if (command is null) @@ -206,7 +206,7 @@ public ResponseApdu DecodeResponse(ResponseApdu response) { if (_sessionKeys is null) { - throw new InvalidOperationException(ExceptionMessages.UnknownScp03Error); + throw new InvalidOperationException(ExceptionMessages.UnknownScpError); } if (response is null) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs index 5f4f9cbe2..ba996ff21 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs @@ -93,7 +93,7 @@ public byte KeyVersionNumber _keyVersionNumber = value; } - } + } //TODO replace this? /// /// AES128 shared secret key used to calculate the Session-MAC key. Also @@ -127,10 +127,9 @@ public byte KeyVersionNumber /// 16-byte AES128 shared secret key /// 16-byte AES128 shared secret key /// 16-byte AES128 shared secret key - public StaticKeys( - ReadOnlyMemory channelMacKey, - ReadOnlyMemory channelEncryptionKey, - ReadOnlyMemory dataEncryptionKey) + public StaticKeys(ReadOnlyMemory channelMacKey, + ReadOnlyMemory channelEncryptionKey, + ReadOnlyMemory dataEncryptionKey) { if (channelMacKey.Length != KeySizeBytes) { @@ -160,9 +159,11 @@ public StaticKeys( /// public StaticKeys() { - var DefaultKey = new ReadOnlyMemory(new byte[] { - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f - }); + var DefaultKey = new ReadOnlyMemory( + new byte[] + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f + }); SetKeys(DefaultKey, DefaultKey, DefaultKey); KeyVersionNumber = 255; @@ -170,10 +171,9 @@ public StaticKeys() _disposed = false; } - private void SetKeys( - ReadOnlyMemory channelMacKey, - ReadOnlyMemory channelEncryptionKey, - ReadOnlyMemory dataEncryptionKey) + private void SetKeys(ReadOnlyMemory channelMacKey, + ReadOnlyMemory channelEncryptionKey, + ReadOnlyMemory dataEncryptionKey) { channelMacKey.CopyTo(_macKey.AsMemory()); channelEncryptionKey.CopyTo(_encKey.AsMemory()); @@ -181,31 +181,27 @@ private void SetKeys( } // Get a copy (deep clone) of this object. - internal StaticKeys GetCopy() - { - return new StaticKeys(ChannelMacKey, ChannelEncryptionKey, DataEncryptionKey) + internal StaticKeys GetCopy() => + new StaticKeys(ChannelMacKey, ChannelEncryptionKey, DataEncryptionKey) { KeyVersionNumber = KeyVersionNumber }; - } /// /// Determine if the contents of each key is the same for both objects. /// If so, this method will return true. /// - public bool AreKeysSame(StaticKeys compareKeys) + public bool AreKeysSame(StaticKeys? compareKeys) { - if (!(compareKeys is null)) + if (compareKeys is null) { - if (ChannelEncryptionKey.Span.SequenceEqual(compareKeys.ChannelEncryptionKey.Span) - && ChannelMacKey.Span.SequenceEqual(compareKeys.ChannelMacKey.Span) - && DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span)) - { - return true; - } + return false; } - return false; + return + ChannelEncryptionKey.Span.SequenceEqual(compareKeys.ChannelEncryptionKey.Span) && + ChannelMacKey.Span.SequenceEqual(compareKeys.ChannelMacKey.Span) && + DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span); } /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs index 7cc409858..4ecab241e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs @@ -13,6 +13,9 @@ // limitations under the License. using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; namespace Yubico.YubiKey { @@ -30,20 +33,22 @@ public enum YubiKeyApplication OtpNdef = 9, YubiHsmAuth = 10, Scp03 = 11, + SecurityDomain = 12 } internal static class YubiKeyApplicationExtensions { - private static readonly byte[] ManagementAppId = new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 }; - private static readonly byte[] OtpAppId = new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01 }; - private static readonly byte[] FidoU2fAppId = new byte[] { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }; - private static readonly byte[] Fido2AppId = new byte[] { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }; - private static readonly byte[] OathAppId = new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01 }; - private static readonly byte[] OpenPgpAppId = new byte[] { 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01 }; - private static readonly byte[] PivAppId = new byte[] { 0xa0, 0x00, 0x00, 0x03, 0x08 }; - private static readonly byte[] OtpNdef = new byte[] { 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; - private static readonly byte[] YubiHsmAuthId = new byte[] { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01 }; - private static readonly byte[] Scp03AuthId = new byte[] { 0xA0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; + private static readonly byte[] ManagementAppId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 }; + private static readonly byte[] OtpAppId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01 }; + private static readonly byte[] FidoU2fAppId = { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }; + private static readonly byte[] Fido2AppId = { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }; + private static readonly byte[] OathAppId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01 }; + private static readonly byte[] OpenPgpAppId = { 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01 }; + private static readonly byte[] PivAppId = { 0xa0, 0x00, 0x00, 0x03, 0x08 }; + private static readonly byte[] OtpNdef = { 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; + private static readonly byte[] YubiHsmAuthId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01 }; + private static readonly byte[] Scp03AuthId = { 0xA0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; + private static readonly byte[] SecurityDomainAppId = { 0xa0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application) => application switch @@ -58,7 +63,39 @@ public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application YubiKeyApplication.OtpNdef => OtpNdef, YubiKeyApplication.YubiHsmAuth => YubiHsmAuthId, YubiKeyApplication.Scp03 => Scp03AuthId, + YubiKeyApplication.SecurityDomain => SecurityDomainAppId, + _ => throw new NotSupportedException(ExceptionMessages.ApplicationIdNotFound), }; + + public static ReadOnlyDictionary> ApplicationIds => + new ReadOnlyDictionary>( + new Dictionary> + { + { YubiKeyApplication.Management, ManagementAppId }, + { YubiKeyApplication.Otp, OtpAppId }, + { YubiKeyApplication.FidoU2f, FidoU2fAppId }, + { YubiKeyApplication.Fido2, Fido2AppId }, + { YubiKeyApplication.Oath, OathAppId }, + { YubiKeyApplication.OpenPgp, OpenPgpAppId }, + { YubiKeyApplication.Piv, PivAppId }, + { YubiKeyApplication.OtpNdef, OtpNdef }, + { YubiKeyApplication.YubiHsmAuth, YubiHsmAuthId }, + { YubiKeyApplication.Scp03, Scp03AuthId }, + { YubiKeyApplication.SecurityDomain, SecurityDomainAppId } + }); + + public static YubiKeyApplication GetById(ReadOnlySpan applicationId) + { + foreach (var kvp in ApplicationIds) + { + if (kvp.Value.Span.SequenceEqual(applicationId)) + { + return kvp.Key; + } + } + + throw new ArgumentException(nameof(applicationId)); //TODO better exp + } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs index 16a952835..4ede8850f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs @@ -15,13 +15,13 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Threading; using Microsoft.Extensions.Logging; using Yubico.Core.Devices; using Yubico.Core.Devices.Hid; using Yubico.Core.Devices.SmartCard; using Yubico.Core.Logging; using Yubico.YubiKey.DeviceExtensions; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.Scp03; using MgmtCmd = Yubico.YubiKey.Management.Commands; @@ -30,6 +30,7 @@ namespace Yubico.YubiKey public partial class YubiKeyDevice : IYubiKeyDevice { #region IYubiKeyDeviceInfo + /// public YubiKeyCapabilities AvailableUsbCapabilities => _yubiKeyInfo.AvailableUsbCapabilities; @@ -92,26 +93,31 @@ public partial class YubiKeyDevice : IYubiKeyDevice /// public bool ConfigurationLocked => _yubiKeyInfo.ConfigurationLocked; + #endregion - private const int _lockCodeLength = MgmtCmd.SetDeviceInfoBaseCommand.LockCodeLength; + private const int LockCodeLength = MgmtCmd.SetDeviceInfoBaseCommand.LockCodeLength; - private static readonly ReadOnlyMemory _lockCodeAllZeros = new byte[_lockCodeLength]; + private static readonly ReadOnlyMemory _lockCodeAllZeros = new byte[LockCodeLength]; internal bool HasSmartCard => !(_smartCardDevice is null); internal bool HasHidFido => !(_hidFidoDevice is null); internal bool HasHidKeyboard => !(_hidKeyboardDevice is null); internal bool IsNfcDevice { get; private set; } - + internal Transport LastActiveTransport; + internal ISmartCardDevice GetSmartCardDevice() => _smartCardDevice!; + private ISmartCardDevice? _smartCardDevice; private IHidDevice? _hidFidoDevice; private IHidDevice? _hidKeyboardDevice; private IYubiKeyDeviceInfo _yubiKeyInfo; - private Transport _lastActiveTransport; + + private ConnectionFactory ConnectionFactory => + new ConnectionFactory( + Log.GetLogger(), this, _smartCardDevice, _hidKeyboardDevice, _hidFidoDevice); private readonly ILogger _log = Log.GetLogger(); - internal ISmartCardDevice GetSmartCardDevice() => _smartCardDevice!; /// public Transport AvailableTransports @@ -132,7 +138,9 @@ public Transport AvailableTransports if (HasSmartCard) { - transports |= IsNfcDevice ? Transport.NfcSmartCard : Transport.UsbSmartCard; + transports |= IsNfcDevice + ? Transport.NfcSmartCard + : Transport.UsbSmartCard; } return transports; @@ -162,11 +170,13 @@ public YubiKeyDevice(IDevice device, IYubiKeyDeviceInfo info) throw new ArgumentException(ExceptionMessages.DeviceTypeNotRecognized, nameof(device)); } - _log.LogInformation("Created a YubiKeyDevice based on the {Transport} transport.", _lastActiveTransport); + _log.LogInformation("Created a YubiKeyDevice based on the {Transport} transport.", LastActiveTransport); _yubiKeyInfo = info; IsNfcDevice = _smartCardDevice?.IsNfcTransport() ?? false; - _lastActiveTransport = GetTransportIfOnlyDevice(); + LastActiveTransport = GetTransportIfOnlyDevice(); + + //TODO consolidate constructors.. } /// @@ -187,7 +197,7 @@ public YubiKeyDevice( _hidKeyboardDevice = hidKeyboardDevice; _yubiKeyInfo = yubiKeyDeviceInfo; IsNfcDevice = smartCardDevice?.IsNfcTransport() ?? false; - _lastActiveTransport = GetTransportIfOnlyDevice(); // Must be after setting the three device fields. + LastActiveTransport = GetTransportIfOnlyDevice(); // Must be after setting the three device fields. } /// @@ -200,7 +210,7 @@ public YubiKeyDevice( /// The device does not have the same ParentDeviceId, or /// The device is not of a recognizable type. /// - public void Merge(IDevice device) + public void Merge(IDevice device) // TODO Consider INTERNAL { if (!((IYubiKeyDevice)this).HasSameParentDevice(device)) { @@ -215,7 +225,7 @@ public void Merge(IDevice device) /// /// /// - public void Merge(IDevice device, IYubiKeyDeviceInfo info) + public void Merge(IDevice device, IYubiKeyDeviceInfo info) // TODO Consider INTERNAL { // First merge the devices MergeDevice(device); @@ -231,222 +241,156 @@ public void Merge(IDevice device, IYubiKeyDeviceInfo info) } } - private void MergeDevice(IDevice device) - { - switch (device) - { - case ISmartCardDevice scardDevice: - _smartCardDevice = scardDevice; - IsNfcDevice = scardDevice.IsNfcTransport(); - break; - case IHidDevice hidDevice when hidDevice.IsKeyboard(): - _hidKeyboardDevice = hidDevice; - break; - case IHidDevice hidDevice when hidDevice.IsFido(): - _hidFidoDevice = hidDevice; - break; - default: - throw new ArgumentException(ExceptionMessages.DeviceTypeNotRecognized, nameof(device)); - } - - _lastActiveTransport = GetTransportIfOnlyDevice(); - } - - bool IYubiKeyDevice.HasSameParentDevice(IDevice device) => HasSameParentDevice(device); - - internal protected bool HasSameParentDevice(IDevice device) + #region obsoletewip + /// + [Obsolete("Use new Scp")] + public IYubiKeyConnection Connect(byte[] applicationId) { - if (device is null) - { - throw new ArgumentNullException(nameof(device)); - } - - // Never match on a missing parent ID - if (device.ParentDeviceId is null) - { - return false; - } - - return _smartCardDevice?.ParentDeviceId == device.ParentDeviceId - || _hidFidoDevice?.ParentDeviceId == device.ParentDeviceId - || _hidKeyboardDevice?.ParentDeviceId == device.ParentDeviceId; + var application = YubiKeyApplicationExtensions.GetById(applicationId); + return ConnectionFactory.CreateNonScpConnection(application); } /// - public IYubiKeyConnection Connect(YubiKeyApplication yubikeyApplication) - { - _ = TryConnect(yubikeyApplication, null, true, out var returnValue); - return returnValue!; - } + [Obsolete("Use new Scp")] + public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication application, StaticKeys scp03Keys) + => (IScp03YubiKeyConnection)ConnectionFactory.CreateScpConnection(application, scp03Keys); /// - public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys) + [Obsolete("Use new Scp")] + public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) { - _ = TryConnectScp03(yubikeyApplication, null, scp03Keys, true, out var returnValue); - return returnValue!; + var application = YubiKeyApplicationExtensions.GetById(applicationId); + return (IScp03YubiKeyConnection)ConnectionFactory.CreateScpConnection(application, scp03Keys); } - /// - public IYubiKeyConnection Connect(byte[] applicationId) + [Obsolete("Use new Scp")] + internal virtual IYubiKeyConnection? Connect( + YubiKeyApplication? application, + byte[]? applicationId, + StaticKeys scp03Keys) // TODO Test that this works { - _ = TryConnect(null, applicationId, true, out var returnValue); - return returnValue!; + application ??= YubiKeyApplicationExtensions.GetById(applicationId); + // return Connect((YubiKeyApplication)application, scp03Keys); + return ConnectionFactory.CreateScpConnection((YubiKeyApplication)application, scp03Keys); } + + [Obsolete("Obsolete")] + public virtual IYubiKeyConnection Connect(YubiKeyApplication application, StaticKeys scp03Keys) + => ConnectionFactory.CreateScpConnection(application, scp03Keys); + + #endregion /// - public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) - { - _ = TryConnectScp03(null, applicationId, scp03Keys, true, out var returnValue); - return returnValue!; - } + public virtual IYubiKeyConnection Connect(YubiKeyApplication application) + => ConnectionFactory.CreateNonScpConnection(application); /// - public bool TryConnect( - YubiKeyApplication application, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection) => - TryConnect(application, null, false, out connection); + public virtual IYubiKeyConnection Connect(YubiKeyApplication application, ScpKeyParameters keyParameters) + => ConnectionFactory.CreateScpConnection(application, keyParameters); /// - public bool TryConnectScp03( - YubiKeyApplication application, - StaticKeys scp03Keys, - [MaybeNullWhen(returnValue: false)] - out IScp03YubiKeyConnection connection) => - TryConnectScp03(application, null, scp03Keys, false, out connection); + public IScpYubiKeyConnection ConnectScp(YubiKeyApplication application, ScpKeyParameters keyParameters) + => (IScpYubiKeyConnection)ConnectionFactory.CreateScpConnection(application, keyParameters); //TODO is safe? /// - public bool TryConnect( - byte[] applicationId, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection) => - TryConnect(null, applicationId, false, out connection); + public IScpYubiKeyConnection ConnectScp(byte[] applicationId, ScpKeyParameters keyParameters) //TODO Decide if to keep or not + { + var application = YubiKeyApplicationExtensions.GetById(applicationId); + return (IScpYubiKeyConnection)ConnectionFactory.CreateScpConnection(application, keyParameters); //TODO safe? + } + #region obsoletewip /// + [Obsolete("Use new Scp")] public bool TryConnectScp03( - byte[] applicationId, + YubiKeyApplication application, StaticKeys scp03Keys, - [MaybeNullWhen(returnValue: false)] - out IScp03YubiKeyConnection connection) => - TryConnectScp03(null, applicationId, scp03Keys, false, out connection); - - // Calls the common code and throws an exception if there is a problem - // and throwOnFail is true. - private bool TryConnect( - YubiKeyApplication? application, - byte[]? applicationId, - bool throwOnFail, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection) + [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection) { - var returnValue = Connect(application, applicationId, null); - if (!(returnValue is null) || !throwOnFail) + var attemptedConnection = ConnectionFactory.CreateScpConnection(application, scp03Keys); + if (attemptedConnection is IScp03YubiKeyConnection scp03Connection) { - connection = returnValue; - return !(returnValue is null); + connection = scp03Connection; + return true; } - throw new NotSupportedException(ExceptionMessages.NoInterfaceAvailable); + connection = null; + return false; } - private bool TryConnectScp03( - YubiKeyApplication? application, - byte[]? applicationId, + /// + [Obsolete("Use new Scp")] + public bool TryConnectScp03( + byte[] applicationId, StaticKeys scp03Keys, - bool throwOnFail, - [MaybeNullWhen(returnValue: false)] - out IScp03YubiKeyConnection connection) + [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection) { - var returnValue = Connect(application, applicationId, scp03Keys); - if (!(returnValue is null) && returnValue is IScp03YubiKeyConnection scp03Connection) + var application = YubiKeyApplicationExtensions.GetById(applicationId); + var attemptedConnection = ConnectionFactory.CreateScpConnection(application, scp03Keys); + if (attemptedConnection is IScp03YubiKeyConnection scp03Connection) { connection = scp03Connection; return true; } - if (!throwOnFail) - { - connection = null; - return false; - } - - throw new NotSupportedException(ExceptionMessages.NoInterfaceAvailable); + connection = null; + return false; } + #endregion + + /// - // Common to all Connect methods. - // If application is given, use it. If not, use applicationId. - // If scp03Keys are given, make an SCP03 connection. If not, normal - // connection. - // If a connection is made, return it, if there's an error, return null. - internal virtual IYubiKeyConnection? Connect( - YubiKeyApplication? application, - byte[]? applicationId, - StaticKeys? scp03Keys) + [Obsolete("Use corresponding YubiKeyApplication method")] + public bool TryConnect( + byte[] applicationId, + [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection) { - _log.LogInformation( - "YubiKey {Serial} connecting to {Application} application" + (scp03Keys is null ? "." : " over SCP03."), - SerialNumber, - application is null ? applicationId is null ? "Unknown" : applicationId.ToString() - : Enum.GetName(typeof(YubiKeyApplication), application)); - - if (application is null) - { - if (!(applicationId is null) && HasSmartCard && !(_smartCardDevice is null)) - { - _log.LogInformation("Connecting via the SmartCard interface."); - WaitForReclaimTimeout(Transport.SmartCard); - return scp03Keys is null - ? new SmartCardConnection(_smartCardDevice, applicationId) - : new Scp03Connection(_smartCardDevice, applicationId, scp03Keys); - } - - _log.LogInformation( - (applicationId is null ? "No application given." : "No smart card interface present.") + - "Unable to establish connection to YubiKey."); - - return null; - } - - if (!(scp03Keys is null)) - { - if (HasSmartCard && !(_smartCardDevice is null)) - { - _log.LogInformation("Connecting via the SmartCard interface."); - WaitForReclaimTimeout(Transport.SmartCard); - return new Scp03Connection(_smartCardDevice, (YubiKeyApplication)application, scp03Keys); - } - - return null; - } - - // OTP application should prefer the HIDKeyboard transport, but fall back on smart card - // if unavailable. - if (application == YubiKeyApplication.Otp && HasHidKeyboard) - { - _log.LogInformation("Connecting via the Keyboard interface."); - WaitForReclaimTimeout(Transport.HidKeyboard); - return new KeyboardConnection(_hidKeyboardDevice!); - } + var application = YubiKeyApplicationExtensions.GetById(applicationId); + connection = ConnectionFactory.CreateNonScpConnection(application); + return true; //TODO is this safe? will it throw? + } - // FIDO applications should prefer the HIDFido transport, but fall back on smart card - // if unavailable. - if ((application == YubiKeyApplication.Fido2 || application == YubiKeyApplication.FidoU2f) - && HasHidFido) + /// + public bool TryConnect( + YubiKeyApplication application, + [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection) // TODO Consider making nullable again + { + connection = ConnectionFactory.CreateNonScpConnection(application); + return true; //TODO is this safe? will it throw? + } + + /// + public bool TryConnectScp( + YubiKeyApplication application, + ScpKeyParameters keyParameters, + [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection) + { + var attemptedConnection = ConnectionFactory.CreateScpConnection(application, keyParameters); + if (attemptedConnection is IScpYubiKeyConnection scpConnection) { - _log.LogInformation("Connecting via the FIDO interface."); - WaitForReclaimTimeout(Transport.HidFido); - return new FidoConnection(_hidFidoDevice!); + connection = scpConnection; + return true; } - if (HasSmartCard && !(_smartCardDevice is null)) + connection = null; + return false; + } + + public bool TryConnectScp( + byte[] applicationId, + ScpKeyParameters keyParameters, + [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection) + { + var application = YubiKeyApplicationExtensions.GetById(applicationId); + var attemptedConnection = ConnectionFactory.CreateScpConnection(application, keyParameters); + if (attemptedConnection is IScpYubiKeyConnection scpConnection) { - _log.LogInformation("Connecting via the SmartCard interface."); - WaitForReclaimTimeout(Transport.SmartCard); - return new SmartCardConnection(_smartCardDevice, (YubiKeyApplication)application); + connection = scpConnection; + return true; } - _log.LogInformation("No smart card interface present. Unable to establish connection to YubiKey."); - return null; + connection = null; + return false; } /// @@ -565,15 +509,15 @@ public void SetDeviceFlags(DeviceFlags deviceFlags) /// public void LockConfiguration(ReadOnlySpan lockCode) { - if (lockCode.Length != _lockCodeLength) + if (lockCode.Length != LockCodeLength) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, ExceptionMessages.LockCodeWrongLength, - _lockCodeLength, + LockCodeLength, lockCode.Length), - nameof(lockCode)); + nameof(lockCode)); } if (lockCode.SequenceEqual(_lockCodeAllZeros.Span)) @@ -596,15 +540,15 @@ public void LockConfiguration(ReadOnlySpan lockCode) /// public void UnlockConfiguration(ReadOnlySpan lockCode) { - if (lockCode.Length != _lockCodeLength) + if (lockCode.Length != LockCodeLength) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, ExceptionMessages.LockCodeWrongLength, - _lockCodeLength, + LockCodeLength, lockCode.Length), - nameof(lockCode)); + nameof(lockCode)); } var command = new MgmtCmd.SetDeviceInfoCommand(); @@ -627,6 +571,7 @@ public void SetLegacyDeviceConfiguration( int autoEjectTimeout) { #region argument checks + // Keep only flags related to interfaces. This makes the operation easier for users // who may be doing bitwise operations on [Available/Enabled]UsbCapabilities. yubiKeyInterfaces &= @@ -665,6 +610,7 @@ public void SetLegacyDeviceConfiguration( nameof(autoEjectTimeout)); } } + #endregion IYubiKeyResponse response; @@ -674,8 +620,8 @@ public void SetLegacyDeviceConfiguration( { var deviceFlags = touchEjectEnabled - ? DeviceFlags | DeviceFlags.TouchEject - : DeviceFlags & ~DeviceFlags.TouchEject; + ? DeviceFlags | DeviceFlags.TouchEject + : DeviceFlags & ~DeviceFlags.TouchEject; var setDeviceInfoCommand = new MgmtCmd.SetDeviceInfoCommand { @@ -758,6 +704,7 @@ public void DeviceReset() CultureInfo.CurrentCulture, ExceptionMessages.NotSupportedByYubiKeyVersion)); } + IYubiKeyConnection? connection = null; try { @@ -780,6 +727,8 @@ public void DeviceReset() connection?.Dispose(); } } + + /////////////////////////////////////////// PRIVATE ////////////////////////////////////////////////////////////////// private IYubiKeyResponse SendConfiguration(MgmtCmd.SetDeviceInfoBaseCommand baseCommand) { @@ -813,8 +762,7 @@ private IYubiKeyResponse SendConfiguration(MgmtCmd.SetDeviceInfoBaseCommand base } } - private IYubiKeyResponse SendConfiguration( - MgmtCmd.SetLegacyDeviceConfigBase baseCommand) + private IYubiKeyResponse SendConfiguration(MgmtCmd.SetLegacyDeviceConfigBase baseCommand) { IYubiKeyConnection? connection = null; try @@ -841,8 +789,50 @@ private IYubiKeyResponse SendConfiguration( connection?.Dispose(); } } + + private void MergeDevice(IDevice device) + { + switch (device) + { + case ISmartCardDevice scardDevice: + _smartCardDevice = scardDevice; + IsNfcDevice = scardDevice.IsNfcTransport(); + break; + case IHidDevice hidDevice when hidDevice.IsKeyboard(): + _hidKeyboardDevice = hidDevice; + break; + case IHidDevice hidDevice when hidDevice.IsFido(): + _hidFidoDevice = hidDevice; + break; + default: + throw new ArgumentException(ExceptionMessages.DeviceTypeNotRecognized, nameof(device)); + } + + LastActiveTransport = GetTransportIfOnlyDevice(); + } + + bool IYubiKeyDevice.HasSameParentDevice(IDevice device) => HasSameParentDevice(device); + + internal protected bool HasSameParentDevice(IDevice device) + { + if (device is null) + { + throw new ArgumentNullException(nameof(device)); + } + + // Never match on a missing parent ID + if (device.ParentDeviceId is null) + { + return false; + } + + return _smartCardDevice?.ParentDeviceId == device.ParentDeviceId + || _hidFidoDevice?.ParentDeviceId == device.ParentDeviceId + || _hidKeyboardDevice?.ParentDeviceId == device.ParentDeviceId; + } #region IEquatable and IComparable + /// public override bool Equals(object obj) { @@ -903,7 +893,7 @@ internal protected bool Contains(IDevice other) => { ISmartCardDevice scDevice => scDevice.Path == _smartCardDevice?.Path, IHidDevice hidDevice => hidDevice.Path == _hidKeyboardDevice?.Path || - hidDevice.Path == _hidFidoDevice?.Path, + hidDevice.Path == _hidFidoDevice?.Path, _ => false }; @@ -929,7 +919,9 @@ public int CompareTo(IYubiKeyDevice other) if (HasSmartCard) { - int delta = string.Compare(_smartCardDevice!.Path, concreteKey._smartCardDevice!.Path, StringComparison.Ordinal); + int delta = string.Compare( + _smartCardDevice!.Path, concreteKey._smartCardDevice!.Path, StringComparison.Ordinal); + if (delta != 0) { return delta; @@ -942,7 +934,9 @@ public int CompareTo(IYubiKeyDevice other) if (HasHidFido) { - int delta = string.Compare(_hidFidoDevice!.Path, concreteKey._hidFidoDevice!.Path, StringComparison.Ordinal); + int delta = string.Compare( + _hidFidoDevice!.Path, concreteKey._hidFidoDevice!.Path, StringComparison.Ordinal); + if (delta != 0) { return delta; @@ -955,7 +949,9 @@ public int CompareTo(IYubiKeyDevice other) if (HasHidKeyboard) { - int delta = string.Compare(_hidKeyboardDevice!.Path, concreteKey._hidKeyboardDevice!.Path, StringComparison.Ordinal); + int delta = string.Compare( + _hidKeyboardDevice!.Path, concreteKey._hidKeyboardDevice!.Path, StringComparison.Ordinal); + if (delta != 0) { return delta; @@ -998,7 +994,9 @@ public int CompareTo(IYubiKeyDevice other) /// public static bool operator <(YubiKeyDevice left, YubiKeyDevice right) => - left is null ? right is object : left.CompareTo(right) < 0; + left is null + ? right is object + : left.CompareTo(right) < 0; /// public static bool operator <=(YubiKeyDevice left, YubiKeyDevice right) => @@ -1010,10 +1008,14 @@ public int CompareTo(IYubiKeyDevice other) /// public static bool operator >=(YubiKeyDevice left, YubiKeyDevice right) => - left is null ? right is null : left.CompareTo(right) >= 0; + left is null + ? right is null + : left.CompareTo(right) >= 0; + #endregion #region System.Object overrides + /// public override int GetHashCode() { @@ -1026,6 +1028,7 @@ public override int GetHashCode() } private static readonly string EOL = Environment.NewLine; + /// public override string ToString() { @@ -1041,19 +1044,11 @@ public override string ToString() + "- Available NFC Capabilities: " + AvailableNfcCapabilities + EOL + "- Enabled USB Capabilities: " + EnabledUsbCapabilities + EOL + "- Enabled NFC Capabilities: " + EnabledNfcCapabilities + EOL; + return res; } - #endregion - private DateTime GetLastActiveTime() => - _lastActiveTransport switch - { - Transport.SmartCard when _smartCardDevice is { } => _smartCardDevice.LastAccessed, - Transport.HidFido when _hidFidoDevice is { } => _hidFidoDevice.LastAccessed, - Transport.HidKeyboard when _hidKeyboardDevice is { } => _hidKeyboardDevice.LastAccessed, - Transport.None => DateTime.Now, - _ => throw new InvalidOperationException(ExceptionMessages.DeviceTypeNotRecognized) - }; + #endregion // If this YubiKey only has a single active USB transport attached to it, we do not really need to worry about // the reclaim timeout. This can help speed up the first access of the device as we know for a fact that there @@ -1079,63 +1074,5 @@ private Transport GetTransportIfOnlyDevice() return Transport.None; } - - // This function handles waiting for the reclaim timeout on the YubiKey to elapse. The reclaim timeout requires - // the SDK to wait 3 seconds since the last USB message to an interface before switching to a different interface. - // Failure to wait can result in very strange behavior from the USB devices ultimately resulting in communication - // failures (i.e. exceptions). - private void WaitForReclaimTimeout(Transport newTransport) - { - // Newer YubiKeys are able to switch interfaces much, much faster. Maybe this is being paranoid, but we - // should still probably wait a few milliseconds for things to stabilize. But definitely not the full - // three seconds! For older keys, we use a value of 3.01 seconds to give us a little wiggle room as the - // YubiKey's measurement for the reclaim timeout is likely not as accurate as our system clock. - var reclaimTimeout = CanFastReclaim() ? TimeSpan.FromMilliseconds(100) : TimeSpan.FromSeconds(3.01); - - // We're only affected by the reclaim timeout if we're switching USB transports. - if (_lastActiveTransport == newTransport) - { - _log.LogInformation( - "{Transport} transport is already active. No need to wait for reclaim.", - _lastActiveTransport); - - return; - } - - _log.LogInformation( - "Switching USB transports from {OldTransport} to {NewTransport}.", - _lastActiveTransport, - newTransport); - - var timeSinceLastActivation = DateTime.Now - GetLastActiveTime(); - - // If we haven't already waited the duration of the reclaim timeout, we need to do so. - // Otherwise, we've already waited and can immediately switch the transport. - if (timeSinceLastActivation < reclaimTimeout) - { - var waitNeeded = reclaimTimeout - timeSinceLastActivation; - - _log.LogInformation( - "Reclaim timeout still active. Need to wait {TimeMS} milliseconds.", - waitNeeded.TotalMilliseconds); - - Thread.Sleep(waitNeeded); - } - - _lastActiveTransport = newTransport; - - _log.LogInformation("Reclaim timeout has lapsed. It is safe to switch USB transports."); - } - - private bool CanFastReclaim() - { - if (AppContext.TryGetSwitch(YubiKeyCompatSwitches.UseOldReclaimTimeoutBehavior, out bool useOldBehavior) && - useOldBehavior) - { - return false; - } - - return this.HasFeature(YubiKeyFeature.FastUsbReclaim); - } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs index 2ed341faf..20376bfd4 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs @@ -27,8 +27,8 @@ namespace Yubico.YubiKey /// public class YubiKeyDeviceInfo : IYubiKeyDeviceInfo { - private const byte FipsMask = 0b1000_0000; - private const byte SkyMask = 0b0100_0000; + private const byte FipsMask = 0x80; + private const byte SkyMask = 0x40; private const byte FormFactorMask = unchecked((byte)~(FipsMask | SkyMask)); private static readonly FirmwareVersion _fipsInclusiveLowerBound = diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs index 01a4149f9..6e18aca18 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using Xunit; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.Oath @@ -26,9 +27,9 @@ public void ResetOathApplication(StandardTestDevice testDeviceType) { IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice)) + using (var oathSession = new OathSession(testDevice, Scp03KeyParameters.DefaultKey)) //TODO follow Dains advice to reset the session { - oathSession.ResetApplication(); + oathSession.ResetApplication(); // IList data = oathSession.GetCredentials(); Assert.True(oathSession._oathData.Challenge.IsEmpty); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs index cf3c6e7b0..ee651db39 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,16 +38,17 @@ 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, new StaticKeys()) : new PivSession(testDevice); + using var pivSession = useScp03 + ? new PivSession(testDevice, Scp03KeyParameters.DefaultKey) + : new PivSession(testDevice); + var collectorObj = new Simple39KeyCollector(); pivSession.KeyCollector = collectorObj.Simple39KeyCollectorDelegate; var result = pivSession.GenerateKeyPair(PivSlot.Retired12, expectedAlgorithm); - Assert.Equal(expectedAlgorithm, result.Algorithm); } - + [SkippableTheory(typeof(NotSupportedException))] [InlineData(PivAlgorithm.Rsa1024)] [InlineData(PivAlgorithm.Rsa2048)] diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs index 9ff52ba1a..0c9fc3086 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs @@ -115,8 +115,10 @@ public void Chuid_Auth_Req(StandardTestDevice testDeviceType) }; IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - +// TODO +#pragma warning disable CS0618 // Type or member is obsolete using (var pivSession = new PivSession(testDevice, new StaticKeys())) +#pragma warning restore CS0618 // Type or member is obsolete { pivSession.ResetApplication(); @@ -304,8 +306,10 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); } - +//TODO +#pragma warning disable CS0618 // Type or member is obsolete using (var pivSession = new PivSession(testDevice, newKeys)) +#pragma warning restore CS0618 // Type or member is obsolete { // Verify the PIN pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs index cded8cb9f..de9b95a0d 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/SignTests.cs @@ -47,9 +47,11 @@ 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)); - +//TODO using var pivSession = useScp03 +#pragma warning disable CS0618 // Type or member is obsolete ? new PivSession(testDevice, new StaticKeys()) +#pragma warning restore CS0618 // Type or member is obsolete : 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..36f41b540 --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -0,0 +1,336 @@ +// 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.Linq; +using Xunit; +using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp03; +using Yubico.YubiKey.TestUtilities; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Yubico.YubiKey.Scp +{ + public class Scp03Tests + { + private readonly ReadOnlyMemory _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; + private IYubiKeyDevice Device { get; set; } + + public Scp03Tests() + { + Device = IntegrationTestDeviceEnumeration.GetTestDevice( + Transport.SmartCard, + minimumFirmwareVersion: FirmwareVersion.V5_3_0); + + using var session = new SecurityDomainSession(Device); + session.Reset(); + } + + + [Fact] + public void TestImportKey() + { + byte[] sk = + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + }; + + var newKeyParams = new Scp03KeyParameters(ScpKid.Scp03, 0x01, new StaticKeys( + sk, + sk, + sk)); + + // assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + session.PutKeySet(newKeyParams); + } + + using (_ = new SecurityDomainSession(Device, newKeyParams)) + { + // Authentication with new key should succeed + } + + Assert.Throws(() => //TODO Is this the correct exception to throw? + { + using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + // Default key should not work now and throw an exception + } + }); + } + + [Fact] + public void TestDeleteKey() + { + var keyRef1 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); + var keyRef2 = new Scp03KeyParameters(ScpKid.Scp03, 0x55, RandomStaticKeys()); + + // Auth with default key, then replace default key + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + session.PutKeySet(keyRef1); + } + + // Authenticate with key1, then add additional key, keyref2 + using (var session = new SecurityDomainSession(Device, keyRef1)) + { + session.PutKeySet(keyRef2); + } + + // Authenticate with key2, delete key 1 + using (var session = new SecurityDomainSession(Device, keyRef2)) + { + session.DeleteKeySet(keyRef1.KeyReference.VersionNumber); + } + + // Authenticate with key 1, + // Should throw because we just deleted it + Assert.Throws(() => + { + using (_ = new SecurityDomainSession(Device, keyRef1)) + { + } + }); + + using (var session = new SecurityDomainSession(Device, keyRef2)) + { + session.DeleteKeySet(keyRef2.KeyReference.VersionNumber, true); + } + + // Try to authenticate with key 2, + // Should throw because we just deleted the last key + Assert.Throws(() => + { + using (_ = new SecurityDomainSession(Device, keyRef2)) + { + } + }); + } + + [Fact] + public void TestReplaceKey() + { + var keyRef1 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); + var keyRef2 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); + + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + session.PutKeySet(keyRef1); + } + + using (var session = new SecurityDomainSession(Device, keyRef1)) + { + session.PutKeySet(keyRef2); + } + + using (_ = new SecurityDomainSession(Device, keyRef2)) + { + // Authentication with new key should succeed + } + + Assert.Throws( + () => // TODO SecureChannelException this time for some reason, but ArgumentException if I try with the DefaultKey, check google Keep + { + using (_ = new SecurityDomainSession(Device, keyRef1)) + { + } + }); + + using (_ = new SecurityDomainSession(Device, keyRef2)) + { + // Authentication with new key should succeed + } + } + + [Fact] + public void AuthenticateWithWrongKey_Should_ThrowException() + { + var incorrectKeys = RandomStaticKeys(); + var keyRef = new Scp03KeyParameters(ScpKid.Scp03, 0x01, incorrectKeys); + + Assert.Throws(() => new SecurityDomainSession(Device, keyRef)); + + using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + // Authentication with default key should succeed + } + } + + [Fact] + public void GetInformation_WithDefaultKey_Returns_DefaultKey() + { + using var session = new SecurityDomainSession(Device); + + var result = session.GetKeyInformation(); + Assert.NotEmpty(result); + Assert.Equal(4, result.Count); + Assert.Equal(0xFF, result.Keys.First().VersionNumber); + } + + [Fact] + public void Connect_GetInformation_WithDefaultKey_Returns_DefaultKey() + { + using var connection = Device.Connect(YubiKeyApplication.SecurityDomain); + const byte TAG_KEY_INFORMATION = 0xE0; + var response = connection.SendCommand(new GetDataCommand(TAG_KEY_INFORMATION)); + var res = response.GetData(); + + // var result = session.GetKeyInformation(); + // Assert.NotEmpty(result); + // Assert.Equal(4, result.Count); + // Assert.Equal(0xFF, result.Keys.First().VersionNumber); + } + + [Fact] + public void TestGetCertificateBundle() + { + Skip.IfNot(Device.FirmwareVersion >= FirmwareVersion.V5_7_2); + + using var session = new SecurityDomainSession(Device); + + var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + var certificateList = session.GetCertificates(keyReference); + + Assert.NotEmpty(certificateList); + } + + [Fact] + public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() + { + byte[] sk = + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + }; + + var newKeyParams = new Scp03KeyParameters( + ScpKid.Scp03, + 0x01, + new StaticKeys(sk, sk, sk)); + + // TODO assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + session.PutKeySet(newKeyParams); + } + + using (var session = new SecurityDomainSession(Device, newKeyParams)) + { + // Authentication with new key should succeed + session.GetKeyInformation(); + } + + Assert.Throws(() => //TODO Is this the correct exception to throw? + { + using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + // Default key should not work now and throw an exception + } + }); + + using (var session = new SecurityDomainSession(Device)) + { + session.Reset(); + } + + // Successful authentication with default key means key has been restored to factory settings + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + _ = session.GetKeyInformation(); + } + } + + [Fact] + public void TryConnectScp_WithApplicationId_Succeeds() + { + var isValid = + Device.TryConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); + using (connection) + { + Assert.True(isValid); + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(_defaultPin); + var rsp = connection.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + } + + [Fact] + public void TryConnectScp_WithApplication_Succeeds() + { + var isValid = + Device.TryConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); + using (connection) + { + Assert.NotNull(connection); + Assert.True(isValid); + var cmd = new VerifyPinCommand(_defaultPin); + var rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + } + + [Fact] + public void ConnectScp_WithApplication_Succeeds() + { + using IYubiKeyConnection connection = + Device.ConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey); + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(_defaultPin); + var rsp = connection.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + + [Fact] + public void ConnectScp_WithApplicationId_Succeeds() + { + using IYubiKeyConnection connection = Device.ConnectScp( + YubiKeyApplication.Piv.GetIso7816ApplicationId(), + Scp03KeyParameters.DefaultKey); + + Assert.NotNull(connection); + + var cmd = new VerifyPinCommand(_defaultPin); + var rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + + #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/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs new file mode 100644 index 000000000..2cdc23faa --- /dev/null +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -0,0 +1,300 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Xunit; +using Yubico.YubiKey.TestUtilities; +// ReSharper disable UnusedVariable + +namespace Yubico.YubiKey.Scp +{ + [SuppressMessage("Style", "IDE0059:Unnecessary assignment of a value")] + public class Scp11Tests + { + public Scp11Tests() + { + Device = IntegrationTestDeviceEnumeration.GetTestDevice( + Transport.SmartCard, + minimumFirmwareVersion: FirmwareVersion.V5_7_2); + + using var session = new SecurityDomainSession(Device); + session.Reset(); + } + + [Fact] + public void Scp11b_Authenticate_Succeeds() // Works + { + IReadOnlyCollection certificateList; + var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + using (var session = new SecurityDomainSession(Device)) + { + certificateList = session.GetCertificates(keyReference); + } + + var leaf = certificateList.Last(); + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); + var keyParams = new Scp11KeyParameters(keyReference, ecDsaPublicKey); + + using (var session = new SecurityDomainSession(Device, keyParams)) + { + session.GetKeyInformation(); + } + } + + [Fact] + public void Scp11b_Import_Succeeds() //todo + { + + using (var session = new SecurityDomainSession(Device)) + { + var ecDsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var keyReference = new KeyReference(ScpKid.Scp11b, 0x2); + // session.PutKeySet(keyReference); + session.GetKeyInformation(); + } + } + + + [Fact] + public void TestGetCertificateBundle() //Works + { + using var session = new SecurityDomainSession(Device); + + var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + var certificateList = session.GetCertificates(keyReference); + + Assert.NotEmpty(certificateList); + } + + + [Fact] + public void GenerateEcKey_Succeeds() // Works + { + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + + var keyReference = new KeyReference(ScpKid.Scp11a, 0x3); + + // Generate a new EC key + ECParameters generatedKey = session.GenerateEcKey(keyReference, 0); + + // Verify the generated key + Assert.NotNull(generatedKey.Q.X); + Assert.NotNull(generatedKey.Q.Y); + Assert.Equal(32, generatedKey.Q.X.Length); // P-256 curve should have 32-byte X and Y coordinates + Assert.Equal(32, generatedKey.Q.Y.Length); + Assert.Equal(ECCurve.NamedCurves.nistP256.Oid.Value, generatedKey.Curve.Oid.Value); + + using ECDsa ecdsa = ECDsa.Create(generatedKey); + Assert.NotNull(ecdsa); + } + + // [Fact] + // public void Scp11a_Authenticate_Succeeds() + // { + // + // byte kvn = 0x03; + // var keyReference = new KeyReference(ScpKid.Scp11a, kvn); + // + // Scp11KeyParameters keyParams; + // using (var session = new SecurityDomainSession(Device)) + // { + // keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + // } + // + // using (var session = new SecurityDomainSession(Device, keyParams)) + // { + // session.DeleteKeySet(keyReference.VersionNumber, false); + // } + // } + + + // private Scp11KeyParameters LoadKeys(SecurityDomainSession session, byte scpKid, byte kvn) + // { + // var sessionRef = new KeyReference(scpKid, kvn); + // var oceRef = new KeyReference(OceKid, kvn); + + // PublicKeyValues publicKeyValues = session.GenerateEcKey(sessionRef, 0); + + // var oceCerts = GetOceCertificates(OceCerts); + // if (oceCerts.Ca == null) + // { + // throw new InvalidOperationException("Missing CA certificate"); + // } + // session.PutKey(oceRef, PublicKeyValues.FromPublicKey(oceCerts.Ca.GetPublicKey()), 0); + + // byte[] ski = GetSki(oceCerts.Ca); + // if (ski == null) + // { + // throw new InvalidOperationException("CA certificate missing Subject Key Identifier"); + // } + // session.StoreCaIssuer(oceRef, ski); + + // using (var keyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + // { + // keyStore.Open(OpenFlags.ReadOnly); + + // // Assuming the certificate and private key are installed in the certificate store + // var cert = keyStore.Certificates.Find(X509FindType.FindBySubjectName, "YourCertSubjectName", false)[0]; + // var privateKey = (RSACng)cert.PrivateKey; + + // var certChain = new List { cert }; + // // Add any intermediate certificates to the chain if necessary + + // return new Scp11KeyParameters( + // sessionRef, + // publicKeyValues.ToPublicKey(), + // oceRef, + // privateKey, + // certChain + // ); + // } + // } + + // private ScpCertificates GetOceCertificates(byte[] pem) + // { + // try + // { + // var certificates = new List(); + + // // Convert PEM to a string + // string pemString = System.Text.Encoding.UTF8.GetString(pem); + + // // Split the PEM string into individual certificates + // string[] pemCerts = pemString.Split( + // new[] { "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----" }, + // StringSplitOptions.RemoveEmptyEntries + // ); + + // foreach (string certString in pemCerts) + // { + // if (!string.IsNullOrWhiteSpace(certString)) + // { + // // Remove any whitespace and convert to byte array + // byte[] 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 byte[] GetSki(X509Certificate2 certificate) + { + var extension = certificate.Extensions["2.5.29.14"]; + if (!(extension is X509SubjectKeyIdentifierExtension skiExtension)) + { + throw new InvalidOperationException("Invalid Subject Key Identifier extension"); + } + + byte[] rawData = skiExtension.RawData; + if (rawData == null || rawData.Length == 0) + { + throw new InvalidOperationException("Missing Subject Key Identifier"); + } + + // The raw data is already in the format we need, so we can return it directly + return rawData; + } + + // private readonly static byte OCE_KID = 0x010; + private IYubiKeyDevice Device { get; set; } + } + + + + + + + class Scp11TestData + { + private readonly static ReadOnlyMemory OCE_CERTS = 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 + static readonly Memory OCE = Convert.FromBase64String("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCMEgggfMIIIGz" + + "CCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqGS" + + "Ib3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIHdoQx_USA3j" + + "mRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4G4sboIRw1v" + + "DH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3s0Yx5yMm_x" + + "zw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEYAn0F3LoMET" + + "QytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PBCpwzbFfVCL" + + "a6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8PFClFZJLQld" + + "u5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67joWydWAMp_" + + "lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAKlqCeAfvDvA" + + "3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixVmlwJ8ZbVxD" + + "6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnryAChdBsGdm" + + "StpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpewiLL7C22ler" + + "UT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRaPhOZEESZEw" + + "LKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcohWoO9EMX_bL" + + "K9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO55LN3rNpcD9" + + "-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_ieUBbebPz0" + + "Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXPfZ3jWxspKI" + + "REHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4CwmKu-12R6iY" + + "Q_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_FemmZyNl0qI1Mj" + + "9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0DD55FDIGCy0" + + "FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScfCsefRl7-o_" + + "9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4pfQqMWcPpqV" + + "p4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6cVrgt3EJWLe" + + "y5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDYxBFFtnd6hq" + + "4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJvQl8-Wp4MG" + + "Bsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYrDg7DAg_-Qc" + + "Oi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3uGZbeJEpU1" + + "hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0CAlJVOTF9uE" + + "pLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zCB7DBXBgkqh" + + "kiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUD" + + "BAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxDY7yIoTVQq7" + + "sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzTl5MLFAwn3N" + + "E49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvcNAQkVMRYEF" + + "JBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k23PH-qUXUGP" + + "EuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA==").AsMemory(); + + static readonly ReadOnlyMemory OCE_PASSWORD = "password".AsMemory(); + } +} 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..54f203f17 --- /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 = new List(certificates); +// 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 ?? Array.Empty(); +// if (keyUsage.Length > 4 && keyUsage[4]) +// { +// 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/Scp03/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs index 46b3245aa..a512230b5 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using Xunit; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.Scp03.Commands @@ -23,6 +25,7 @@ public class DeleteKeyCommandTests [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5Fips)] [InlineData(StandardTestDevice.Fw5)] + [Obsolete("Obsolete")] public void DeleteKey_One_Succeeds(StandardTestDevice testDeviceType) { byte[] key1 = { @@ -41,12 +44,11 @@ public void DeleteKey_One_Succeeds(StandardTestDevice testDeviceType) 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); + var cmd = new Scp03.Commands.DeleteKeyCommand(1, false); + Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } @@ -70,13 +72,16 @@ public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) }; IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + //TODO +#pragma warning disable CS0618 // Type or member is obsolete var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +#pragma warning restore CS0618 // Type or member is obsolete Assert.True(isValid); Assert.NotNull(connection); - var cmd = new DeleteKeyCommand(2, false); - Scp03Response rsp = connection!.SendCommand(cmd); + var cmd = new Scp03.Commands.DeleteKeyCommand(2, false); + Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } @@ -100,13 +105,16 @@ public void DeleteKey_Three_Succeeds(StandardTestDevice testDeviceType) }; IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + //TODO +#pragma warning disable CS0618 // Type or member is obsolete var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +#pragma warning restore CS0618 // Type or member is obsolete Assert.True(isValid); Assert.NotNull(connection); - var cmd = new DeleteKeyCommand(3, true); - Scp03Response rsp = connection!.SendCommand(cmd); + var cmd = new Scp03.Commands.DeleteKeyCommand(3, true); + Scp03.Commands.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 index 52c67645c..dd838cb3c 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs @@ -40,7 +40,11 @@ public void ChangeDefaultKey_Succeeds(StandardTestDevice testDeviceType) var newKeys = new StaticKeys(key2, key1, key3); IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + //TODO + +#pragma warning disable CS0618 // Type or member is obsolete var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +#pragma warning restore CS0618 // Type or member is obsolete Assert.True(isValid); Assert.NotNull(connection); @@ -84,7 +88,10 @@ public void AddNewKeySet_Succeeds(StandardTestDevice testDeviceType) }; IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + //TODO +#pragma warning disable CS0618 // Type or member is obsolete var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +#pragma warning restore CS0618 // Type or member is obsolete Assert.True(isValid); Assert.NotNull(connection); @@ -131,7 +138,10 @@ public void AddThirdKeySet_Succeeds(StandardTestDevice testDeviceType) }; IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + //TODO +#pragma warning disable CS0618 // Type or member is obsolete var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +#pragma warning restore CS0618 // Type or member is obsolete Assert.True(isValid); Assert.NotNull(connection); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs index 97839d4b3..a917c5594 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs @@ -15,6 +15,7 @@ using System; using Xunit; using Yubico.YubiKey.TestUtilities; +#pragma warning disable CS0618 // Type or member is obsolete namespace Yubico.YubiKey.Scp03 { @@ -24,6 +25,7 @@ public class PutDeleteTests { [Fact] [TestPriority(3)] + [Obsolete("Obsolete")] public void PutKey_Succeeds() { using var staticKeys = new StaticKeys(); @@ -41,7 +43,6 @@ public void PutKey_Succeeds() } using StaticKeys keySet2 = GetKeySet(2); - using (var scp03Session = new Scp03Session(device, keySet2)) { using StaticKeys keySet3 = GetKeySet(3); @@ -51,6 +52,7 @@ public void PutKey_Succeeds() [Fact] [TestPriority(3)] + [Obsolete("Obsolete")] public void ReplaceKey_Succeeds() { using StaticKeys staticKeys = GetKeySet(2); @@ -68,12 +70,14 @@ public void ReplaceKey_Succeeds() [Fact] [TestPriority(0)] + [Obsolete("Obsolete")] public void DeleteKey_Succeeds() { using StaticKeys staticKeys = GetKeySet(3); IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( Transport.SmartCard, FirmwareVersion.V5_3_0); + //TODO using var scp03Session = new Scp03Session(device, staticKeys); scp03Session.DeleteKeySet(1); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs index 9adcf96df..e313bb03f 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs @@ -16,10 +16,12 @@ using Xunit; using Yubico.YubiKey.Piv; using Yubico.YubiKey.Piv.Commands; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.Scp03 { + [Trait(TraitTypes.Category, TestCategories.Simple)] public class SimpleSessionTests { private readonly byte[] _pin = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; @@ -45,6 +47,7 @@ public void SessionSetupAndUse_Succeeds(StandardTestDevice testDeviceType) } [Fact] + [Obsolete("Obsolete")] public void ConnectScp03_Application_Succeeds() { IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( @@ -61,6 +64,7 @@ public void ConnectScp03_Application_Succeeds() } [Fact] + [Obsolete("Obsolete")] public void ConnectScp03_AlgorithmId_Succeeds() { IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( @@ -78,6 +82,7 @@ public void ConnectScp03_AlgorithmId_Succeeds() } [Fact] + [Obsolete("Obsolete")] public void TryConnectScp03_Application_Succeeds() { IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( @@ -97,23 +102,38 @@ public void TryConnectScp03_Application_Succeeds() } [Fact] + [Obsolete("Obsolete")] public void TryConnectScp03_AlgorithmId_Succeeds() { - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( + var 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); + bool isValid = device.TryConnectScp03(YubiKeyApplication.Piv, scp03Keys, out IScp03YubiKeyConnection? connection); using (connection) { - Assert.NotNull(connection); Assert.True(isValid); + Assert.NotNull(connection); var cmd = new VerifyPinCommand(_pin); VerifyPinResponse rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } } + + [Fact] + public void TryConnectScp_AlgorithmId_Succeeds() + { + var device = IntegrationTestDeviceEnumeration.GetTestDevice(Transport.SmartCard, FirmwareVersion.V5_3_0); + var isValid = device.TryConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); + using (connection) + { + Assert.True(isValid); + Assert.NotNull(connection); + var cmd = new VerifyPinCommand(_pin); + var rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + } } } 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/unit/Yubico/YubiKey/YubikeyApplicationTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs new file mode 100644 index 000000000..9d49e0516 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs @@ -0,0 +1,76 @@ +// 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.ObjectModel; +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_ReturnsReadOnlyDictionary() + { + var result = YubiKeyApplicationExtensions.ApplicationIds; + Assert.IsType>>(result); + } + + [Fact] + public void ApplicationIds_ContainsAllApplications() + { + var result = YubiKeyApplicationExtensions.ApplicationIds; + 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.GetById(applicationId); + Assert.Equal(expectedApplication, result); + } + + [Fact] + public void GetById_ThrowsForUnknownApplicationId() + { + byte[] unknownId = new byte[] { 0x00, 0x11, 0x22, 0x33 }; + Assert.Throws(() => YubiKeyApplicationExtensions.GetById(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..db3e97cc9 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; +using StaticKeys = Yubico.YubiKey.Scp03.StaticKeys; namespace Yubico.YubiKey.TestUtilities { @@ -141,45 +142,59 @@ 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, }; return connection; } - + + public IYubiKeyConnection Connect(byte[] applicationId) + { + throw new NotImplementedException(); + } + + [Obsolete("Obsolete")] public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys) { throw new NotImplementedException(); } - public IYubiKeyConnection Connect(byte[] applicationId) + [Obsolete("Obsolete")] + public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) { throw new NotImplementedException(); } - public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) + public IScpYubiKeyConnection ConnectScp(YubiKeyApplication yubikeyApplication, ScpKeyParameters scp03Keys) { throw new NotImplementedException(); } - /// - bool IYubiKeyDevice.HasSameParentDevice(IDevice other) + public IScpYubiKeyConnection ConnectScp(byte[] applicationId, ScpKeyParameters scp03Keys) { - return false; + 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, out IScp03YubiKeyConnection connection) @@ -187,21 +202,47 @@ bool IYubiKeyDevice.TryConnectScp03( throw new NotImplementedException(); } - bool IYubiKeyDevice.TryConnect( + [Obsolete("Obsolete")] + public bool TryConnectScp03( byte[] applicationId, - out IYubiKeyConnection connection) + StaticKeys scp03Keys, + out IScp03YubiKeyConnection connection) { throw new NotImplementedException(); } - bool IYubiKeyDevice.TryConnectScp03( + public bool TryConnectScp( + YubiKeyApplication application, + ScpKeyParameters keyParameters, + out IScpYubiKeyConnection connection) + { + throw new NotImplementedException(); + } + + public bool TryConnectScp( byte[] applicationId, - StaticKeys scp03Keys, - out IScp03YubiKeyConnection connection) + ScpKeyParameters keyParameters, + out IScpYubiKeyConnection connection) + { + throw new NotImplementedException(); + } + + public IScpYubiKeyConnection ConnectScp(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys) + { + throw new NotImplementedException(); + } + + public IScpYubiKeyConnection ConnectScp(byte[] applicationId, StaticKeys scp03Keys) { throw new NotImplementedException(); } + /// + bool IYubiKeyDevice.HasSameParentDevice(IDevice other) + { + return false; + } + public void SetEnabledNfcCapabilities(YubiKeyCapabilities yubiKeyCapabilities) => throw new NotImplementedException(); diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs index 8da5e7d69..1f11eb966 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs @@ -132,7 +132,6 @@ public static IYubiKeyDevice GetTestDevice(Transport transport, FirmwareVersion => GetTestDevices(transport) .SelectByMinimumVersion(minimumFirmwareVersion); - private static void CreateAllowListFileIfMissing(string allowListFilePath) { if (File.Exists(allowListFilePath)) 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 + | - // Because the YubiKey supports only AES-128, and because the key is - // a multiple of 16 bytes, no padding is necessary. Because the - // YubiKey only supports putting all three keys, we can know in - // advance all the lengths, and they will be the same each time. We - // also know in advance all the offsets where every element will go. - _data = new byte[DataLength]; - using var memStream = new MemoryStream(_data); - using var binaryWriter = new BinaryWriter(memStream); - binaryWriter.Write(newKeys.KeyReference.VersionNumber); - - _checksum = new byte[ChecksumLength]; - _checksum[0] = newKeys.KeyReference.VersionNumber; - - //TODO Make work with SCp03, Scp11? - var currentKeys03 = currentKeys as Scp03KeyParameters ?? Scp03KeyParameters.DefaultKey; //TODO Right now this ?? is acceptable, because Scp03Params are the only ones being used - var newKeys03 = newKeys as Scp03KeyParameters ?? Scp03KeyParameters.DefaultKey; - - byte[] currentDek = currentKeys03.StaticKeys.DataEncryptionKey.ToArray(); - - try + public CommandApdu CreateCommandApdu() => + new CommandApdu { - BuildKeyDataField(binaryWriter, newKeys03.StaticKeys.ChannelEncryptionKey, ChecksumOffsetEnc, currentDek); - BuildKeyDataField(binaryWriter, newKeys03.StaticKeys.ChannelMacKey, ChecksumOffsetMac, currentDek); - BuildKeyDataField(binaryWriter, newKeys03.StaticKeys.DataEncryptionKey, ChecksumOffsetDek, currentDek); - } - finally - { - CryptographicOperations.ZeroMemory(currentDek.AsSpan()); - } - } - - public CommandApdu CreateCommandApdu() => new CommandApdu() - { - Cla = GpPutKeyCla, - Ins = GpPutKeyIns, - P1 = _p1Value, - P2 = KeyIdentifier, - Data = _data - }; - - public PutKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => - new PutKeyResponse(responseApdu); - - // Build the key's data field. Place it into _data beginning at - // dataOffset. - // Use the keyToBlock to encrypt the 16 bytes 0101...01, use the ms 3 - // bytes as the keyCheck - // Use the encryptionKey to encrypt the keyToBlock. - // - // keyType || block len || block || checkLen || check - // 0x88 || 0x17 || 0x10 || 16-byte encData || 0x03 || 3 bytes - private void BuildKeyDataField( - BinaryWriter binaryWriter, - ReadOnlyMemory keyToBlock, - int checksumOffset, - byte[] encryptionKey) - { - byte[] keyData = keyToBlock.ToArray(); - - try - { - byte[] dataToEncrypt = new byte[AesBlockSize] { //Initialization Vector, IV - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 - }; - byte[] checkBlock = AesUtilities.BlockCipher(keyData, dataToEncrypt.AsSpan()); - byte[] encryptedKey = AesUtilities.BlockCipher(encryptionKey, keyToBlock.Span); - - binaryWriter.Write(KeyType); - binaryWriter.Write(BlockSize); - binaryWriter.Write(AesBlockSize); - binaryWriter.Write(encryptedKey); - binaryWriter.Write(KeyCheckSize); - binaryWriter.Write(checkBlock, 0, KeyCheckSize); - Array.Copy(checkBlock, 0, _checksum, checksumOffset, KeyCheckSize); - } - finally - { - CryptographicOperations.ZeroMemory(keyData.AsSpan()); - } - } + Cla = GpPutKeyCla, + Ins = GpPutKeyIns, + P1 = _p1, + P2 = _p2, + Data = _data + }; + + public PutKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => new PutKeyResponse(responseApdu); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand2.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand2.cs deleted file mode 100644 index b2372cce1..000000000 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand2.cs +++ /dev/null @@ -1,135 +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 Yubico.Core.Iso7816; - -namespace Yubico.YubiKey.Scp.Commands -{ - /// - /// Use this command to put or replace SCP03 keys on the YubiKey. - /// - /// - /// See the User's Manual entry on SCP03. - /// - /// On each YubiKey that supports SCP03, there is space for three sets of - /// keys. Each set contains three keys: "ENC", "MAC", and "DEK" (Channel - /// Encryption, Channel MAC, and Data Encryption). - /// - /// 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. - /// - /// - /// Note that the standard allows changing one key in a key set. However, - /// YubiKeys only allow calling this command with all three keys. That is, - /// with a YubiKey, it is possible only to set or change all three keys of a - /// set with this command. - /// - /// - /// Standard YubiKeys are manufactured with one key set, and each key in that - /// set is the default value. - /// - /// slot 1: ENC(default) MAC(default) DEK(default) - /// slot 2: --empty-- - /// slot 3: --empty-- - /// - /// The default value is 0x40 41 42 ... 4F. - /// - /// - /// The key sets are not specified using a "slot number", rather, each key - /// set is given a Key Version Number (KVN). Each key in the set is given a - /// Key Identifier (KeyId). If the YubiKey contains the default key, the KVN - /// is 255 (0xFF) and the KeyIds are 1, 2, and 3. - /// - /// slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) - /// slot 2: --empty-- - /// slot 3: --empty-- - /// - /// - /// - /// It is possible to use this command to replace or add a key set. However, - /// if the YubiKey contains only the initial, default keys, then it is only - /// possible to replace that set. For example, suppose you have a YubiKey - /// with the default keys and you try to set the keys in slot 2. The YubiKey - /// will not allow that and will return an error. - /// - /// - /// When you replace the initial, default keys, you must specify the KVN of - /// the new keys, and the KeyId of the ENC key. The KeyId(MAC) is, per the - /// standard, KeyId(ENC) + 1, and the KeyId(DEK) is KeyId(ENC) + 2. For the - /// YubiKey, the KVN must be 1. Also, the YubiKey only allows the number 1 as - /// the KeyId of the ENC key. If you supply any other values for the KVN or - /// KeyId, the YubiKey will return an error. Hence, after replacing the - /// initial, default keys, your three sets of keys will be the following: - /// - /// slot 1: KVN=1 KeyId=1:ENC KeyId=2:MAC KeyId=3:DEK - /// slot 2: --empty-- - /// slot 3: --empty-- - /// - /// - /// - /// In order to add or change the keys, you must supply one of the existing - /// key sets in order to build the SCP03 command and to encrypt and - /// authenticate the new keys. When replacing the initial, default keys, you - /// only have the choice to supply the keys with the KVN of 0xFF. - /// - /// - /// Once you have replaced the original key set, you can use that set to add - /// a second set to slot 2. It's KVN must be 2 and the KeyId of the ENC key - /// must be 1. - /// - /// slot 1: KVN=1 KeyId=1:ENC KeyId=2:MAC KeyId=3:DEK - /// slot 2: KVN=2 KeyId=1:ENC KeyId=2:MAC KeyId=3:DEK - /// slot 3: --empty-- - /// - /// - /// - /// You can use either key set to add a set to slot 3. You can use a key set - /// to replace itself. - /// - /// - internal class PutKeyCommand2 : IYubiKeyCommand - { - private const byte GpPutKeyCla = 0x80; - private const byte GpPutKeyIns = 0xD8; - - private readonly byte[] _data; - private readonly byte _p1; - private readonly byte _p2; - - public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; - - public PutKeyCommand2(byte p1, byte p2, ReadOnlyMemory data) - { - _p1 = p1; - _p2 = p2; - _data = data.ToArray(); - } - - public CommandApdu CreateCommandApdu() => - new CommandApdu - { - Cla = GpPutKeyCla, - Ins = GpPutKeyIns, - P1 = _p1, - P2 = _p2, - Data = _data - }; - - public PutKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => new PutKeyResponse(responseApdu); - } -} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs index e375489d1..4653dce6f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs @@ -19,22 +19,37 @@ namespace Yubico.YubiKey.Scp { internal static class Padding { - public static byte[] PadToBlockSize(byte[] payload) + /// + /// Pad the given to the next multiple of 16 bytes by adding a 0x80 byte followed by zero bytes. + /// + /// The payload to pad. + /// The padded payload. + public static Memory PadToBlockSize(ReadOnlySpan payload) { - if (payload is null) + if(payload == null) { throw new ArgumentNullException(nameof(payload)); } - + int paddedLen = ((payload.Length / 16) + 1) * 16; - byte[] padded = new byte[paddedLen]; - payload.CopyTo(padded, 0); + + Span padded = stackalloc byte[paddedLen]; + payload.CopyTo(padded); padded[payload.Length] = 0x80; - return padded; + + return padded.ToArray(); } - public static byte[] RemovePadding(byte[] paddedPayload) + + /// + /// Remove the padding from the given . + /// + /// The padded payload. + /// The unpadded payload. + /// is null. + /// The padding is invalid. + public static Memory RemovePadding(ReadOnlySpan paddedPayload) { - if (paddedPayload is null) + if (paddedPayload == null) { throw new ArgumentNullException(nameof(paddedPayload)); } @@ -43,9 +58,10 @@ public static byte[] RemovePadding(byte[] paddedPayload) { if (paddedPayload[i] == 0x80) { - return paddedPayload.Take(i).ToArray(); + return paddedPayload[..i].ToArray(); } - else if (paddedPayload[i] != 0x00) + + if (paddedPayload[i] != 0x00) { throw new SecureChannelException(ExceptionMessages.InvalidPadding); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index 6d0946587..c42f2f37b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -22,7 +22,6 @@ using Yubico.Core.Tlv; using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.Scp.Commands; -using Yubico.YubiKit.Core.Util; namespace Yubico.YubiKey.Scp { @@ -66,9 +65,10 @@ internal static Scp11State CreateScpState( byte[] keyLen = { 16 }; // 128-bit byte[] keyIdentifier = { 0x11, GetScpIdentifierByte(keyParameters.KeyReference) }; - byte[] hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( + var hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( new TlvObject( - KeyAgreementTag, TlvObjects.EncodeMany( + KeyAgreementTag, + TlvObjects.EncodeMany( new TlvObject(0x90, keyIdentifier), new TlvObject(0x95, keyUsage), new TlvObject(0x80, keyType), @@ -115,7 +115,7 @@ internal static Scp11State CreateScpState( epkSdEckaTlvEncodedData, hostAuthenticateTlvEncodedData, keyUsage, - keyType, + keyType, keyLen); var sessionKeys = new SessionKeys( @@ -160,7 +160,7 @@ private static (Memory encryptionKey, Memory macKey, Memory rM epkSdEckaTlvEncodedData, skOceEcka.Curve); // Yubikey Ephemeral Public Key byte[] keyAgreementFirst = ecdhObject.ComputeSharedSecret(epkSdEcka, eskOceEcka.D); - + // Compute key agreement for: // Yubikey Public Key + Host Private Key byte[] keyAgreementSecond = ecdhObject.ComputeSharedSecret(pkSdEcka, skOceEcka.D); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs index 41701cf89..4fbc714cd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs @@ -47,7 +47,7 @@ public CommandApdu EncodeCommand(CommandApdu command) }; byte[] commandData = command.Data.ToArray(); - byte[] encryptedData = ChannelEncryption.EncryptData( + var encryptedData = ChannelEncryption.EncryptData( commandData, SessionKeys.EncKey.Span, _encryptionCounter); _encryptionCounter++; @@ -96,7 +96,7 @@ public ResponseApdu DecodeResponse(ResponseApdu response) var responseData = response.Data; VerifyRmac(responseData.Span, SessionKeys.RmacKey.Span, MacChainingValue.Span); - byte[] decryptedData = Array.Empty(); + Memory decryptedData = Array.Empty(); if (responseData.Length > 8) { int previousEncryptionCounter = _encryptionCounter - 1; @@ -108,7 +108,7 @@ public ResponseApdu DecodeResponse(ResponseApdu response) } byte[] fullDecryptedResponse = new byte[decryptedData.Length + 2]; - decryptedData.CopyTo(fullDecryptedResponse, 0); + decryptedData.CopyTo(fullDecryptedResponse); fullDecryptedResponse[decryptedData.Length] = response.SW1; fullDecryptedResponse[decryptedData.Length + 1] = response.SW2; return new ResponseApdu(fullDecryptedResponse); @@ -121,10 +121,10 @@ public DataEncryptor GetDataEncryptor() throw new InvalidOperationException(ExceptionMessages.UnknownScpError); //todo set correct message } - return data => AesUtilities.AesCbcEncrypt( + return plainText => AesUtilities.AesCbcEncrypt( SessionKeys.DataEncryptionKey.Value.ToArray(), new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, - data.Span); + plainText.Span); } #pragma warning disable CA1822 // Is being used by subclasses diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index dd341c32b..1250d6414 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -24,9 +24,9 @@ using Yubico.Core.Iso7816; using Yubico.Core.Logging; using Yubico.Core.Tlv; +using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.Scp.Commands; using Yubico.YubiKey.Scp03; -using Yubico.YubiKit.Core.Util; namespace Yubico.YubiKey.Scp { @@ -91,7 +91,8 @@ public sealed class SecurityDomainSession : IDisposable private const byte KeyTypeEccPrivateKeyTag = 0xB1; private const byte KeyTypeEccKeyParamsTag = 0xF0; private const byte KeyTypeEccPublicKeyTag = 0xB0; - + private const byte KeyTypeAesTag = 0x88; + private readonly IYubiKeyDevice _yubiKey; private readonly ILogger _log = Log.GetLogger(); private bool _disposed; @@ -182,133 +183,88 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey) } /// - /// Put the given key set onto the YubiKey. + /// Puts an SCP03 key set onto the YubiKey using the Security Domain. /// - /// - /// See the User's Manual entry on - /// SCP. - /// - /// On each YubiKey that supports SCP, there is space for three sets of - /// keys. Each set contains three keys: "ENC", "MAC", and "DEK" (Channel - /// Encryption, Channel MAC, and Data Encryption). - /// - /// 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. - /// - /// - /// Note that the standard allows changing one key in a key set. However, - /// YubiKeys only allow calling this command with all three keys. That is, - /// with a YubiKey, it is possible only to set or change all three keys of a - /// set. - /// - /// - /// Standard YubiKeys are manufactured with one key set, and each key in that - /// set is the default value. - /// - /// slot 1: ENC(default) MAC(default) DEK(default) - /// slot 2: --empty-- - /// slot 3: --empty-- - /// - /// The default value is 0x40 41 42 ... 4F. - /// - /// - /// The key sets are not specified using a "slot number", rather, each key - /// set is given a Key Version Number (KVN). Each key in the set is given a - /// Key Identifier (KeyId). The YubiKey allows only 1, 2, and 3 as the - /// KeyIds, and SDK users never need to worry about them. If the YubiKey - /// contains the default key, the KVN is 255 (0xFF). - /// - /// slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) - /// slot 2: --empty-- - /// slot 3: --empty-- - /// - /// - /// - /// It is possible to use this method to replace or add a key set. However, - /// if the YubiKey contains only the initial, default keys, then it is only - /// possible to replace that set. For example, suppose you have a YubiKey - /// with the default keys and you try to set the keys in slot 2. The YubiKey - /// will not allow that and will return an error. - /// - /// - /// When you replace the initial, default keys, you must specify the KVN of - /// the new keys. For the YubiKey, in this situation, the KVN must be 1. - /// If you supply any other values for the KVN, the YubiKey will return - /// an error. Hence, after replacing the initial, default keys, your - /// three sets of keys will be the following: - /// - /// slot 1: KVN=1 newENC newMAC newDEK - /// slot 2: --empty-- - /// slot 3: --empty-- - /// - /// - /// - /// In order to add or change any key set, you must supply one of the existing - /// key sets in order to build the SCP command and to encrypt and - /// authenticate the new keys. When replacing the initial, default keys, you - /// only have the choice to supply the keys with the KVN of 0xFF. - /// - /// - /// Once you have replaced the original key set, you can use that set to add - /// a second set to slot 2. It's KVN must be 2. - /// - /// slot 1: KVN=1 ENC MAC DEK - /// slot 2: KVN=2 ENC MAC DEK - /// slot 3: --empty-- - /// - /// - /// - /// You can use either key set to add a set to slot 3. You can use a key set - /// to replace itself. - /// - /// - /// - /// The keys and KeyVersion Number of the set that will be loaded onto - /// the YubiKey. - /// - /// - /// The newKeySet argument is null. - /// - /// - /// The new key set's checksum failed to verify, or some other error - /// described in the exception message. - /// - public void PutKeySet(Scp03KeyParameters keyParameters) // Works for SCP3 but needs to be remade. + /// The key reference identifying where to store the key. + /// The new SCP03 key set to store. + /// The key version number to replace, or 0 for a new key. + /// Thrown when the KID is not 0x01 for SCP03 key sets. + /// Thrown when the new key set's checksum failed to verify, or some other error + /// described in the exception message. + public void PutKeySet(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) { - if (Connection is null) + _log.LogInformation("Importing SCP03 key set into KeyRef {KeyRef}", keyRef); + + if (keyRef.Id != ScpKid.Scp03) { - throw new InvalidOperationException("No connection initialized. Use the other constructor"); + throw new ArgumentException("KID must be 0x01 for SCP03 key sets"); } - - _log.LogInformation("Put a new SCP key set onto a YubiKey."); - - if (keyParameters is null) + + var connection = Connection ?? throw new InvalidOperationException("No connection initialized"); + var encryptor = connection.DataEncryptor ?? throw new InvalidOperationException("No session DEK available"); + + using var dataStream = new MemoryStream(); + using var dataWriter = new BinaryWriter(dataStream); + using var expectedKcvStream = new MemoryStream(); + using var expectedKcvWriter = new BinaryWriter(expectedKcvStream); + + // Write KVN + dataWriter.Write(keyRef.VersionNumber); + expectedKcvWriter.Write(keyRef.VersionNumber); + + Span kcvInput = stackalloc byte[16] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; + ReadOnlySpan kvcZeroIv = stackalloc byte[16]; + + // Process all keys + foreach (var key in new[] + { + newKeySet.ChannelEncryptionKey, + newKeySet.ChannelMacKey, + newKeySet.DataEncryptionKey + }) { - throw new ArgumentNullException(nameof(keyParameters)); + // Key check value (KCV) is first 3 bytes of encrypted test vector + var kcv = AesUtilities.AesCbcEncrypt( + key.Span, + kvcZeroIv, + kcvInput)[..3]; + + // Encrypt the key using session encryptor + var encryptedKey = encryptor(key); + + // Write key structure + var tlvData = new TlvObject(KeyTypeAesTag, encryptedKey.Span.ToArray()).GetBytes(); + dataWriter.Write(tlvData.ToArray()); + + // Write KCV + byte[] kcvData = kcv.ToArray(); + dataWriter.Write((byte)kcvData.Length); + dataWriter.Write(kcvData); + + // Add KCV to expected response + expectedKcvWriter.Write(kcvData); } - - var command = new PutKeyCommand(Connection.KeyParameters, keyParameters); - var response = Connection.SendCommand(command); + + ReadOnlyMemory commandData = dataStream.ToArray().AsMemory(); + byte p2 = (byte)(0x80 | keyRef.Id); // OR with 0x80 indicates that we're sending multiple keys + + var command = new PutKeyCommand((byte)replaceKvn, p2, commandData); + var response = connection.SendCommand(command); if (response.Status != ResponseStatus.Success) { throw new SecureChannelException( string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.YubiKeyOperationFailed, - response.StatusMessage)); + CultureInfo.CurrentCulture, ExceptionMessages.YubiKeyOperationFailed, response.StatusMessage)); } - - var checksum = response.GetData(); - if (!CryptographicOperations.FixedTimeEquals(checksum.Span, command.ExpectedChecksum.Span)) + + var responseKcvData = response.GetData().Span; + ReadOnlySpan expectedKcvData = expectedKcvStream.ToArray().AsSpan(); + if (!CryptographicOperations.FixedTimeEquals(responseKcvData, expectedKcvData)) { throw new SecureChannelException(ExceptionMessages.ChecksumError); } } - - + /// /// Puts an ECC private key onto the YubiKey using the Security Domain. /// @@ -331,7 +287,7 @@ public void PutKeySet(KeyReference keyRef, ECParameters secretKey, int replaceKv { throw new ArgumentException("Private key must be of type SECP256R1"); } - + try { // Prepare the command data @@ -361,7 +317,7 @@ public void PutKeySet(KeyReference keyRef, ECParameters secretKey, int replaceKv dataWriter.Write((byte)0); // Create and send the command - var command = new PutKeyCommand2((byte)replaceKvn, keyRef.Id, dataStream.ToArray()); + var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, dataStream.ToArray()); var response = connection.SendCommand(command); if (response.Status != ResponseStatus.Success) { @@ -558,7 +514,8 @@ public IReadOnlyDictionary> GetKeyInformati const byte tagKeyInformation = 0xE0; var keys = new Dictionary>(); - var tlvDataList = TlvObjects.DecodeList(GetData(tagKeyInformation).Span); + var getDataResult = GetData(tagKeyInformation).Span; + var tlvDataList = TlvObjects.DecodeList(getDataResult); foreach (var tlvObject in tlvDataList) { var value = TlvObjects.UnpackValue(0xC0, tlvObject.GetBytes().Span); @@ -607,10 +564,10 @@ public IReadOnlyList GetCertificates(KeyReference keyReference /// /// The tag of the data to retrieve. /// Optional data to send with the command. - /// The data retrieved from the YubiKey. + /// The encoded tlv data retrieved from the YubiKey. This will have to be decoded public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) { - var connection = Connection ?? _yubiKey.Connect(YubiKeyApplication.SecurityDomain); // todo this could be default behaviour + var connection = Connection ?? _yubiKey.Connect(YubiKeyApplication.SecurityDomain); var response = connection.SendCommand(new GetDataCommand(tag, data)); return response.GetData(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs index b4b9d9a6d..79ee9cfcd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Buffers.Binary; +using System.Linq; using Yubico.YubiKey.Cryptography; namespace Yubico.YubiKey.Scp03 { + [Obsolete("Use new ChannelEncryption instead")] internal static class ChannelEncryption { public static byte[] EncryptData(byte[] payload, byte[] key, int encryptionCounter) @@ -30,9 +33,9 @@ public static byte[] EncryptData(byte[] payload, byte[] key, int encryptionCount byte[] iv = AesUtilities.BlockCipher(key, ivInput); byte[] paddedPayload = Padding.PadToBlockSize(payload); - byte[] encryptedData = AesUtilities.AesCbcEncrypt(key, iv, paddedPayload); + var encryptedData = AesUtilities.AesCbcEncrypt(key.AsSpan(), iv.AsSpan(), paddedPayload.AsSpan()); - return encryptedData; + return encryptedData.ToArray(); } public static byte[] DecryptData(byte[] payload, byte[] key, int encryptionCounter) @@ -45,9 +48,9 @@ public static byte[] DecryptData(byte[] payload, byte[] key, int encryptionCount ivInput[0] = 0x80; // to mark as RMAC calculation byte[] iv = AesUtilities.BlockCipher(key, ivInput); - byte[] decryptedData = AesUtilities.AesCbcDecrypt(key, iv, payload); + var decryptedData = AesUtilities.AesCbcDecrypt(key.AsSpan(), iv.AsSpan(), payload); - return Padding.RemovePadding(decryptedData); + return Padding.RemovePadding(decryptedData.ToArray()).ToArray(); } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs index b38d8e85a..a048389d6 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs @@ -22,6 +22,7 @@ namespace Yubico.YubiKey.Scp03 { + [Obsolete("Use new ChannelMac instead")] internal static class ChannelMac { public static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu(CommandApdu apdu, byte[] macKey, byte[] macChainingValue) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommand.cs index 4aca9fdad..af552cc2a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommand.cs @@ -33,6 +33,7 @@ namespace Yubico.YubiKey.Scp03.Commands /// key set with a KeyVersionNumber of 1) will be the default key set. /// /// + [Obsolete("Use new DeleteKeyCommand instead")] internal class DeleteKeyCommand : IYubiKeyCommand { private const byte GpDeleteKeyCla = 0x84; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommand.cs index 5b7a63576..8a18c5545 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommand.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp03.Commands @@ -19,6 +20,7 @@ namespace Yubico.YubiKey.Scp03.Commands /// /// Represents the second command in the SCP03 authentication handshake, 'EXTERNAL_AUTHENTICATE' /// + [Obsolete("Use new ExternalAuthenticateCommand instead")] internal class ExternalAuthenticateCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponse.cs index 270b1d811..2ab1421a5 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponse.cs @@ -17,6 +17,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("Use new ExternalAuthenticateResponse instead")] internal class ExternalAuthenticateResponse : Scp03Response { /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs index 6bdf13dd1..0decc4f95 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommand.cs @@ -20,6 +20,7 @@ namespace Yubico.YubiKey.Scp03.Commands /// /// Represents the first command in the SCP03 authentication handshake, 'INITIALIZE_UPDATE' /// + [Obsolete("Use new InitializeUpdateCommand instead")] internal class InitializeUpdateCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs index bc62e79eb..19ee5e979 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs @@ -19,6 +19,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("Use new InitializeUpdateResponse instead")] internal class InitializeUpdateResponse : Scp03Response { public IReadOnlyCollection DiversificationData { get; protected set; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs index b94fdd92b..22efb2a34 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs @@ -105,6 +105,7 @@ namespace Yubico.YubiKey.Scp03.Commands /// to replace itself. /// /// + [Obsolete("Use new PutKeyCommand instead")] internal class PutKeyCommand : IYubiKeyCommand { private const byte GpPutKeyCla = 0x84; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyResponse.cs index 00b3b2244..e75243c30 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyResponse.cs @@ -20,6 +20,7 @@ namespace Yubico.YubiKey.Scp03.Commands /// /// The response to putting or replacing SCP03 keys on the YubiKey. /// + [Obsolete("Use new PutKeyResponse instead")] internal class PutKeyResponse : Scp03Response, IYubiKeyResponseWithData> { private readonly byte[] _checksum; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/Scp03Response.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/Scp03Response.cs index e6073e18c..88f6beae5 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/Scp03Response.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/Scp03Response.cs @@ -18,6 +18,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("Use new ScpResponse instead")] internal class Scp03Response : YubiKeyResponse { public Scp03Response(ResponseApdu responseApdu) : diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Padding.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Padding.cs index 00136ed79..dbb62b3de 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Padding.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Padding.cs @@ -17,6 +17,7 @@ namespace Yubico.YubiKey.Scp03 { + [Obsolete("Use new Padding class instead")] internal static class Padding { public static byte[] PadToBlockSize(byte[] payload) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs index 6ce5e8c9f..9f4317f68 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs @@ -20,6 +20,7 @@ namespace Yubico.YubiKey.Scp03 /// Represents errors that occur during encoding or decoding data for SCP03. /// #pragma warning disable CA1064 // Exceptions should be public +[Obsolete("Use new ChannelEncryption instead")] internal class SecureChannelException : Exception #pragma warning restore CA1064 // Exceptions should be public { diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index 36f41b540..f7b1b1850 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -18,6 +18,7 @@ using Yubico.YubiKey.Piv.Commands; using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; +using GetDataCommand = Yubico.YubiKey.Scp.Commands.GetDataCommand; #pragma warning disable CS0618 // Type or member is obsolete @@ -58,7 +59,7 @@ public void TestImportKey() using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(newKeyParams); + session.PutKeySet(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); } using (_ = new SecurityDomainSession(Device, newKeyParams)) @@ -84,13 +85,13 @@ public void TestDeleteKey() // Auth with default key, then replace default key using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(keyRef1); + session.PutKeySet(keyRef1.KeyReference, keyRef1.StaticKeys, 0); } // Authenticate with key1, then add additional key, keyref2 using (var session = new SecurityDomainSession(Device, keyRef1)) { - session.PutKeySet(keyRef2); + session.PutKeySet(keyRef2.KeyReference, keyRef2.StaticKeys, 0); } // Authenticate with key2, delete key 1 @@ -131,12 +132,12 @@ public void TestReplaceKey() using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(keyRef1); + session.PutKeySet(keyRef1.KeyReference, keyRef1.StaticKeys, 0); } using (var session = new SecurityDomainSession(Device, keyRef1)) { - session.PutKeySet(keyRef2); + session.PutKeySet(keyRef2.KeyReference, keyRef2.StaticKeys, keyRef1.KeyReference.VersionNumber); } using (_ = new SecurityDomainSession(Device, keyRef2)) @@ -189,12 +190,11 @@ public void Connect_GetInformation_WithDefaultKey_Returns_DefaultKey() using var connection = Device.Connect(YubiKeyApplication.SecurityDomain); const byte TAG_KEY_INFORMATION = 0xE0; var response = connection.SendCommand(new GetDataCommand(TAG_KEY_INFORMATION)); - var res = response.GetData(); + var result = response.GetData(); - // var result = session.GetKeyInformation(); - // Assert.NotEmpty(result); - // Assert.Equal(4, result.Count); - // Assert.Equal(0xFF, result.Keys.First().VersionNumber); + Assert.NotEmpty(result.ToArray()); + // Assert.Equal(4, result.Length); + // Assert.Equal(0xFF, result.ToArray().First()); } [Fact] @@ -229,7 +229,7 @@ public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(newKeyParams); + session.PutKeySet(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); } using (var session = new SecurityDomainSession(Device, newKeyParams)) diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs index a512230b5..a6a3237ab 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs @@ -15,9 +15,10 @@ using System; using Xunit; using Yubico.YubiKey.Scp; +using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; -namespace Yubico.YubiKey.Scp03.Commands +namespace Yubico.YubiKey.Scp.Commands { [Trait(TraitTypes.Category, TestCategories.Simple)] public class DeleteKeyCommandTests @@ -80,8 +81,8 @@ public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) Assert.True(isValid); Assert.NotNull(connection); - var cmd = new Scp03.Commands.DeleteKeyCommand(2, false); - Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); + var cmd = new Scp.Commands.DeleteKeyCommand(2, false); + var rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } @@ -113,8 +114,8 @@ public void DeleteKey_Three_Succeeds(StandardTestDevice testDeviceType) Assert.True(isValid); Assert.NotNull(connection); - var cmd = new Scp03.Commands.DeleteKeyCommand(3, true); - Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); + var cmd = new DeleteKeyCommand(3, true); + var rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } } diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs index ed654dad4..76e477b2d 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs @@ -116,7 +116,7 @@ public void AesCbcEncrypt_GivenKeyIVPlaintext_EncryptsCorrectly() byte[] iv = GetIV(); // Act - byte[] result = AesUtilities.AesCbcEncrypt(key, iv, plaintext); + Memory 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); + Memory result = AesUtilities.AesCbcDecrypt(key, iv, ciphertext); // Assert Assert.Equal(result, Hex.HexToBytes("d1af13631ea24595793ddf5bf6f9c42c")); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs index 287cf0135..52cc1f711 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs @@ -24,6 +24,7 @@ namespace Yubico.YubiKey.Pipelines { + [Obsolete("This class is obsolete and will be removed in a future release.")] public class PipelineFixture : IApduTransform { public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) @@ -82,6 +83,7 @@ public override void GetBytes(byte[] arr) } } + [Obsolete("Class is replaced by ScpApduTransform.")] public class Scp03ApduTransformTests { private static IApduTransform GetPipeline() => new PipelineFixture(); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs index 1e4742718..afff39bc5 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs @@ -16,7 +16,7 @@ using Xunit; using Yubico.Core.Buffers; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class ChannelEncryptionTests { @@ -46,7 +46,7 @@ public void EncryptData_GivenCorrectKeyPayload_ReturnsCorrectly() int ec = GetEncryptionCounter(); // Act - byte[] output = ChannelEncryption.EncryptData(payload, key, ec); + Memory output = ChannelEncryption.EncryptData(payload, key, ec); // Assert Assert.Equal(GetCorrectEncryptOutput(), output); @@ -77,7 +77,7 @@ public void DecryptData_GivenCorrectKeyPayload_ReturnsCorrectly() int ec = 1; // Act - byte[] output = ChannelEncryption.DecryptData(payload, key, ec); + Memory 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/Scp03/ChannelMacTests.cs index ad92329f5..8f0f2013a 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs @@ -17,7 +17,7 @@ using Yubico.Core.Buffers; using Yubico.Core.Iso7816; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class ChannelMacTests { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs index 9209b227f..5f0025e16 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs @@ -18,6 +18,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("This class is obsolete and will be removed in a future release.")] public class ExternalAuthenticateCommandTests { [Fact] diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs index 67d8b5d67..7b3cca4b6 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs @@ -18,6 +18,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("This class is obsolete and will be removed in a future release.")] public class ExternalAuthenticateResponseTests { public static ResponseApdu GetResponseApdu() diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs index 04e3b31fc..ea6507a43 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs @@ -18,6 +18,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("This class is obsolete and will be removed in a future release.")] public class InitializeUpdateCommandTests { [Fact] diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs index d85a9f18f..bec58a375 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs @@ -19,6 +19,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("This class is obsolete and will be removed in a future release.")] public class InitializeUpdateResponseTests { public static ResponseApdu GetResponseApdu() diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs index 2036ab9b8..616595bef 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs @@ -18,6 +18,7 @@ namespace Yubico.YubiKey.Scp03.Commands { + [Obsolete("This class is obsolete and will be removed in a future release.")] public class Scp03ResponseTests { [Fact] diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs index 4cbb95920..c52aa217c 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs @@ -16,7 +16,7 @@ using Xunit; using Yubico.Core.Buffers; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { public class PaddingTests { @@ -50,7 +50,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 +63,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 +76,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 +89,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 +102,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 +115,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/Scp03/SessionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs index 2f131cf47..61ad49384 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs @@ -20,6 +20,7 @@ namespace Yubico.YubiKey.Scp03 { + [Obsolete("Replaced by SecurityDomainSession")] public class SessionTests { private static byte[] GetChallenge() => Hex.HexToBytes("360CB43F4301B894"); From 5e9ec88dd13d377ccf01e1a87feacebe263dcd95 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Tue, 5 Nov 2024 16:31:18 +0100 Subject: [PATCH 04/53] feat(scp11ab): Initialize SCP Connection --- .../YubiKey/Cryptography/ECKeyParameters.cs | 31 ++ .../Cryptography/ECParametersExtensions.cs | 71 +++ .../Cryptography/ECPrivateKeyParameters.cs | 35 ++ .../Cryptography/ECPublicKeyParameters.cs | 50 ++ .../YubiKey/Pipelines/ScpApduTransform.cs | 2 +- .../Commands/ExternalAuthenticateResponse.cs | 2 +- .../YubiKey/Scp/Commands/ResetCommand.cs | 2 +- .../YubiKey/Scp/Commands/StoreDataCommand.cs | 68 +++ .../src/Yubico/YubiKey/Scp/Scp11State.cs | 22 +- .../YubiKey/Scp/SecurityDomainSession.cs | 452 +++++++++++----- .../src/Yubico/YubiKey/Scp03/Derivation.cs | 1 + .../src/Yubico/YubiKey/Scp03/Session.cs | 1 + .../Yubico.YubiKey.IntegrationTests.csproj | 1 + .../Yubico/YubiKey/Scp/Scp03Tests.cs | 16 +- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 505 ++++++++++++------ .../Yubico/YubiKey/Scp/ScpCertificates.cs | 168 +++--- .../Scp03/Commands/DeleteKeyCommandTests.cs | 66 +-- .../Scp03/Commands/PutKeyCommandTests.cs | 315 +++++------ .../Yubico/YubiKey/Scp03/DerivationTests.cs | 4 +- .../unit/Yubico/YubiKey/Scp03/SessionTests.cs | 3 +- 20 files changed, 1228 insertions(+), 587 deletions(-) create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs new file mode 100644 index 000000000..4ce2ef6d1 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs @@ -0,0 +1,31 @@ +// 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.Security.Cryptography; + +namespace Yubico.YubiKey.Cryptography +{ + /// + /// Base class for EC key parameters + /// + public abstract class ECKeyParameters + { + public ECParameters Parameters { get; } + + protected ECKeyParameters(ECParameters parameters) + { + Parameters = parameters.DeepCopy(); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs new file mode 100644 index 000000000..11f139316 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs @@ -0,0 +1,71 @@ +// 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.Linq; +using System.Security.Cryptography; + +namespace Yubico.YubiKey.Cryptography +{ + + /// + /// Helper extensions for parameter copying + /// + internal static class ECParametersExtensions + { + public static ECParameters DeepCopy(this ECParameters parameters) + { + var copy = new ECParameters + { + Curve = parameters.Curve, + Q = new ECPoint + { + X = parameters.Q.X?.ToArray(), + Y = parameters.Q.Y?.ToArray() + } + }; + + if (parameters.D != null) + { + copy.D = parameters.D.ToArray(); + } + + return copy; + } + + /// + /// Creates an instance from a byte array. + /// + /// + /// The byte array is expected to be in the format 0x04 || X || Y + /// where X and Y are the uncompressed coordinates of the point. + /// + /// The byte array. + /// An instance of EccPrivateKeyParameters with the nistP256 curve. + public static ECPublicKeyParameters CreateEcPublicKeyFromBytes(this ReadOnlySpan bytes) + { + var ecParameters = new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new ECPoint + { + X = bytes.Slice(1, 32).ToArray(), + Y = bytes.Slice(33, 32).ToArray() + } + }; + + return new ECPublicKeyParameters(ecParameters); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs new file mode 100644 index 000000000..0a782de20 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs @@ -0,0 +1,35 @@ +// 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; + +namespace Yubico.YubiKey.Cryptography +{ + /// + /// EC private key parameters + /// + public sealed class ECPrivateKeyParameters : ECKeyParameters + { + public ECPrivateKeyParameters(ECParameters parameters) : base(parameters) + { + if (parameters.D == null) + { + throw new ArgumentException("Parameters must contain private key data (D value)", nameof(parameters)); + } + } + + public ECPrivateKeyParameters(ECDsa ecdsaObject) : base(ecdsaObject.ExportParameters(true)) { } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs new file mode 100644 index 000000000..b320e0a69 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs @@ -0,0 +1,50 @@ +// 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.Linq; +using System.Security.Cryptography; + +namespace Yubico.YubiKey.Cryptography +{ + /// + /// EC public key parameters + /// + public sealed class ECPublicKeyParameters : ECKeyParameters + { + public ECPublicKeyParameters(ECParameters parameters) : base(parameters) + { + if (parameters.D != null) + { + throw new ArgumentException( + "Parameters must not contain private key data (D value)", nameof(parameters)); + } + } + + public ECPublicKeyParameters(ECDsa ecdsa) : base(ecdsa.ExportParameters(false)) { } + + public Memory GetBytes() + { + byte[] formatIdentifier = { 0x4 }; // Uncompressed point + var publicKeyRawData = + formatIdentifier + .Concat(Parameters.Q.X) + .Concat(Parameters.Q.Y) + .ToArray() + .AsMemory(); + + return publicKeyRawData; + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs index 944bca158..1b3c497c6 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -33,7 +33,7 @@ namespace Yubico.YubiKey.Pipelines internal class ScpApduTransform : IApduTransform, IDisposable { public ScpKeyParameters KeyParameters { get; } - public DataEncryptor? DataEncryptor; + public DataEncryptor? DataEncryptor; // When is this ever null? private ScpState ScpState => _scpState ?? throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs index 5a2456e75..6255984c5 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs @@ -32,7 +32,7 @@ public ExternalAuthenticateResponse(ResponseApdu responseApdu) : throw new ArgumentNullException(nameof(responseApdu)); } - if (responseApdu.Data.Length != 0) + if (responseApdu.SW != SWConstants.Success) { throw new ArgumentException(ExceptionMessages.IncorrectExternalAuthenticateData, nameof(responseApdu)); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs index 8a97be087..fabfb0c61 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs @@ -48,7 +48,7 @@ public ResetCommand(byte ins, byte keyVersionNumber, byte kid, byte[] data) public CommandApdu CreateCommandApdu() => new CommandApdu { - Cla = 0x84, + Cla = 0x80, Ins = _ins, P1 = _keyVersionNumber, P2 = _kid, diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs new file mode 100644 index 000000000..cb0334234 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs @@ -0,0 +1,68 @@ +// 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 Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Scp.Commands +{ + + + /// + /// TODO + /// + internal class StoreDataCommand : IYubiKeyCommand + { + private const byte InsStoreData = 0xE2; + private readonly ReadOnlyMemory _data; + + public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + + public StoreDataCommand(ReadOnlyMemory data) + { + _data = data; + } + + // The default constructor explicitly defined. We don't want it to be + // used. + private StoreDataCommand() + { + throw new NotImplementedException(); + } + + public CommandApdu CreateCommandApdu() => new CommandApdu + { + Cla = 0, + Ins = InsStoreData, + P1 = 0x90, + P2 = 0x00, + Data = _data + }; + + public StoreDataCommandResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new StoreDataCommandResponse(responseApdu); + + + } + + internal class StoreDataCommandResponse : ScpResponse, IYubiKeyResponseWithData> + { + public StoreDataCommandResponse(ResponseApdu responseApdu) : base(responseApdu) + { + } + + public ReadOnlyMemory GetData() => ResponseApdu.Data; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index c42f2f37b..0db6e81f1 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using Yubico.Core.Cryptography; using Yubico.Core.Iso7816; @@ -65,7 +66,7 @@ internal static Scp11State CreateScpState( byte[] keyLen = { 16 }; // 128-bit byte[] keyIdentifier = { 0x11, GetScpIdentifierByte(keyParameters.KeyReference) }; - var hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( + byte[] hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( new TlvObject( KeyAgreementTag, TlvObjects.EncodeMany( @@ -86,7 +87,7 @@ internal static Scp11State CreateScpState( hostAuthenticateTlvEncodedData) as IYubiKeyCommand; var authenticateResponseApdu = pipeline.Invoke( - authenticateCommand.CreateCommandApdu(), authenticateCommand.GetType(), typeof(ScpResponse)); + authenticateCommand.CreateCommandApdu(), authenticateCommand.GetType(), typeof(ScpResponse)); //works var authenticateResponse = authenticateCommand.CreateResponseForApdu(authenticateResponseApdu); authenticateResponse.ThrowIfFailed( @@ -146,7 +147,7 @@ private static (Memory encryptionKey, Memory macKey, Memory rM pkSdEcka.Curve, // Yubikey Public Key eskOceEcka.Curve, // Host Ephemeral Private Key skOceEcka.Curve // Host Private Key - }.All(c => c.Oid == curve.Oid); + }.All(c => c.Oid.Value == curve.Oid.Value); if (!allKeysAreSameCurve) { @@ -189,15 +190,16 @@ private static (Memory encryptionKey, Memory macKey, Memory rM } // Get keys - byte[] encryptionKey = keys[0]; - byte[] macKey = keys[1]; - byte[] rmacKey = keys[2]; - byte[] dekKey = keys[3]; + byte[] receiptVerificationKey = keys[0]; // receipt verificationKey + byte[] encryptionKey = keys[1]; + byte[] macKey = keys[2]; + byte[] rmacKey = keys[3]; + byte[] dekKey = keys[4]; // Do AES CMAC using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); Span oceReceipt = stackalloc byte[16]; // Our generated receipt - cmacObj.CmacInit(encryptionKey); + cmacObj.CmacInit(receiptVerificationKey); cmacObj.CmacUpdate(keyAgreementData); cmacObj.CmacFinal(oceReceipt); @@ -260,9 +262,7 @@ private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyPa for (int i = 0; i <= n; i++) { byte[] certificates = keyParams.Certificates[i].RawData; - byte oceRefPadded = (byte)(oceRef.Id | (i < n - ? 0b10000000 - : 0x00)); // Is this a good name? + byte oceRefPadded = (byte)(oceRef.Id | (i < n ? 0x80 : 0x00)); // Append 0x80 if more certificates following var securityOperationCommand = new SecurityOperationCommand( oceRef.VersionNumber, diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 1250d6414..20ae706c4 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -88,21 +88,25 @@ namespace Yubico.YubiKey.Scp /// public sealed class SecurityDomainSession : IDisposable { - private const byte KeyTypeEccPrivateKeyTag = 0xB1; - private const byte KeyTypeEccKeyParamsTag = 0xF0; - private const byte KeyTypeEccPublicKeyTag = 0xB0; - private const byte KeyTypeAesTag = 0x88; + private const byte KeyTypeEccPrivateKey = 0xB1; + private const byte KeyTypeEccKeyParams = 0xF0; + private const byte KeyTypeEccPublicKey = 0xB0; + private const byte KeyTypeAes = 0x88; + private const ushort CertificateStoreTag = 0xBF21; + private const byte ControlReferenceTag = 0xA6; + private const byte KidKvnTag = 0x83; + private const byte KeyInformationTag = 0xE0; private readonly IYubiKeyDevice _yubiKey; private readonly ILogger _log = Log.GetLogger(); private bool _disposed; - /// - /// The object that represents the connection to the YubiKey. Most - /// applications will ignore this, but it can be used to call Commands - /// directly. - /// - private IScpYubiKeyConnection? Connection { get; } + private readonly IScpYubiKeyConnection? _connection; + + private IScpYubiKeyConnection AuthenticatedConnection => + _connection ?? throw new InvalidOperationException("No secure connection initialized."); + + private IYubiKeyConnection UnauthenticatedConnection => _yubiKey.Connect(YubiKeyApplication.SecurityDomain); // The default constructor explicitly defined. We don't want it to be // used. @@ -138,15 +142,15 @@ private SecurityDomainSession() /// The object that represents the actual YubiKey which will perform the /// operations. /// - /// + /// /// The shared secret keys that will be used to authenticate the caller /// and encrypt the communications. This constructor will make a deep - /// copy of the keys, it will not copy a reference to the object. + /// copy of the keys, it will not copy a reference to the object. //TODO Deep copy /// /// /// The yubiKey or scpKeys argument is null. /// - public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeys) + public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeyParameters) { _log.LogInformation("Create a new instance of ScpSession."); @@ -155,26 +159,28 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeys) throw new ArgumentNullException(nameof(yubiKey)); } - if (scpKeys is null) + if (scpKeyParameters is null) { - throw new ArgumentNullException(nameof(scpKeys)); + throw new ArgumentNullException(nameof(scpKeyParameters)); } _yubiKey = yubiKey; - Connection = yubiKey.ConnectScp(YubiKeyApplication.SecurityDomain, scpKeys); + _connection = yubiKey.ConnectScp(YubiKeyApplication.SecurityDomain, scpKeyParameters); } /// /// Create an unauthenticated instance of , the object that /// represents SCP on the YubiKey. /// - /// Sessions created from this constructor will not be able to perform operations which require authentication + /// Sessions created from this constructor will not be able to perform operations which require authentication + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. + /// /// /// The object that represents the actual YubiKey which will perform the /// operations. /// /// - /// The yubiKey or scpKeys argument is null. + /// The yubiKey argument is null. /// public SecurityDomainSession(IYubiKeyDevice yubiKey) { @@ -191,7 +197,7 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey) /// Thrown when the KID is not 0x01 for SCP03 key sets. /// Thrown when the new key set's checksum failed to verify, or some other error /// described in the exception message. - public void PutKeySet(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) + public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) { _log.LogInformation("Importing SCP03 key set into KeyRef {KeyRef}", keyRef); @@ -200,8 +206,8 @@ public void PutKeySet(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) throw new ArgumentException("KID must be 0x01 for SCP03 key sets"); } - var connection = Connection ?? throw new InvalidOperationException("No connection initialized"); - var encryptor = connection.DataEncryptor ?? throw new InvalidOperationException("No session DEK available"); + var encryptor = AuthenticatedConnection.DataEncryptor ?? + throw new InvalidOperationException("No session DEK available"); using var dataStream = new MemoryStream(); using var dataWriter = new BinaryWriter(dataStream); @@ -225,15 +231,15 @@ public void PutKeySet(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) { // Key check value (KCV) is first 3 bytes of encrypted test vector var kcv = AesUtilities.AesCbcEncrypt( - key.Span, - kvcZeroIv, - kcvInput)[..3]; + key.Span, + kvcZeroIv, + kcvInput)[..3]; // Encrypt the key using session encryptor var encryptedKey = encryptor(key); // Write key structure - var tlvData = new TlvObject(KeyTypeAesTag, encryptedKey.Span.ToArray()).GetBytes(); + var tlvData = new TlvObject(KeyTypeAes, encryptedKey.Span.ToArray()).GetBytes(); dataWriter.Write(tlvData.ToArray()); // Write KCV @@ -241,7 +247,7 @@ public void PutKeySet(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) dataWriter.Write((byte)kcvData.Length); dataWriter.Write(kcvData); - // Add KCV to expected response + // Add KCV to expected response expectedKcvWriter.Write(kcvData); } @@ -249,41 +255,34 @@ public void PutKeySet(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) byte p2 = (byte)(0x80 | keyRef.Id); // OR with 0x80 indicates that we're sending multiple keys var command = new PutKeyCommand((byte)replaceKvn, p2, commandData); - var response = connection.SendCommand(command); - if (response.Status != ResponseStatus.Success) - { - throw new SecureChannelException( - string.Format( - CultureInfo.CurrentCulture, ExceptionMessages.YubiKeyOperationFailed, response.StatusMessage)); - } + var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); var responseKcvData = response.GetData().Span; ReadOnlySpan expectedKcvData = expectedKcvStream.ToArray().AsSpan(); - if (!CryptographicOperations.FixedTimeEquals(responseKcvData, expectedKcvData)) - { - throw new SecureChannelException(ExceptionMessages.ChecksumError); - } + ValidateCheckSum(expectedKcvData, responseKcvData); + + _log.LogInformation("Successsfully put static keys for KeyRef {KeyRef}", keyRef); } /// /// Puts an ECC private key onto the YubiKey using the Security Domain. /// /// The key reference identifying where to store the key. - /// The ECC private key parameters to store. + /// The ECC private key parameters to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the private key is not of type SECP256R1. /// Thrown when no secure session is established. - public void PutKeySet(KeyReference keyRef, ECParameters secretKey, int replaceKvn) + /// Thrown when key check sum is invalid. + public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParameters, int replaceKvn) { _log.LogInformation("Importing SCP11 private key into KeyRef {KeyRef}", keyRef); - var connection = Connection ?? throw new InvalidOperationException( - "No secure session established. Connection required for key import."); - - var encryptor = connection.DataEncryptor ?? throw new InvalidOperationException( + var encryptor = AuthenticatedConnection.DataEncryptor ?? throw new InvalidOperationException( "No secure session established. DataEncryptor required for key import."); - if (secretKey.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) + var privateKey = privateKeyParameters.Parameters; + if (privateKey.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) { throw new ArgumentException("Private key must be of type SECP256R1"); } @@ -291,58 +290,122 @@ public void PutKeySet(KeyReference keyRef, ECParameters secretKey, int replaceKv try { // Prepare the command data - using var dataStream = new MemoryStream(); - using var dataWriter = new BinaryWriter(dataStream); + using var commandDataStream = new MemoryStream(); + using var commandDataWriter = new BinaryWriter(commandDataStream); // Write the key version number - dataWriter.Write(keyRef.VersionNumber); + commandDataWriter.Write(keyRef.VersionNumber); // Convert the private key to bytes and encrypt it - var privateKeyBytes = secretKey.D.AsMemory(); + var privateKeyBytes = privateKey.D.AsMemory(); try { // Must be encrypted with the active sessions data encryption key var encryptedKey = encryptor(privateKeyBytes); - var privateKeyTlv = new TlvObject(KeyTypeEccPrivateKeyTag, encryptedKey.Span).GetBytes(); - dataWriter.Write(privateKeyTlv.ToArray()); + var privateKeyTlv = new TlvObject(KeyTypeEccPrivateKey, encryptedKey.Span).GetBytes(); + commandDataWriter.Write(privateKeyTlv.ToArray()); } finally { CryptographicOperations.ZeroMemory(privateKeyBytes.Span); } - // Write the ECC parameters (currently just 0x00 as per Java implementation) - var paramsTlv = new TlvObject(KeyTypeEccKeyParamsTag, new byte[] { 0x00 }).GetBytes(); - dataWriter.Write(paramsTlv.ToArray()); - dataWriter.Write((byte)0); + // Write the ECC parameters + var paramsTlv = new TlvObject(KeyTypeEccKeyParams, new byte[] { 0x00 }).GetBytes(); + commandDataWriter.Write(paramsTlv.ToArray()); + commandDataWriter.Write((byte)0); // Create and send the command - var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, dataStream.ToArray()); - var response = connection.SendCommand(command); - if (response.Status != ResponseStatus.Success) - { - throw new SecureChannelException( - string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.YubiKeyOperationFailed, - response.StatusMessage)); - } + var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, commandDataStream.ToArray()); + var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); - // Get the response + // Get and validate the response var responseData = response.GetData(); Span expectedResponseData = new[] { keyRef.VersionNumber }; - if (!CryptographicOperations.FixedTimeEquals(responseData.Span, expectedResponseData)) - { - throw new SecureChannelException("Incorrect key check value"); - } + ValidateCheckSum(responseData.Span, expectedResponseData); + + _log.LogInformation("Successsfully put private key for KeyRef {KeyRef}", keyRef); + } catch (Exception ex) { - _log.LogError(ex, "Failed to put key set for KeyRef {KeyRef}", keyRef); + _log.LogError(ex, "Failed to put private key for KeyRef {KeyRef}", keyRef); throw; } } + /// + /// Puts an ECC public key onto the YubiKey using the Security Domain. + /// + /// The key reference identifying where to store the key. + /// The ECC public key parameters to store. + /// The key version number to replace, or 0 for a new key. + /// Thrown when the public key is not of type SECP256R1. + /// Thrown when no secure session is established. + /// Thrown when no key check sum is invalid. + public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameters, int replaceKvn) + { + _log.LogInformation("Importing SCP11 public key into KeyRef {KeyRef}", keyRef); + + var pkParams = publicKeyParameters.Parameters; + if (pkParams.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) + { + throw new ArgumentException("Private key must be of type SECP256R1"); + } + + try + { + using var commandDataMs = new MemoryStream(); + using var commandDataWriter = new BinaryWriter(commandDataMs); + + // Write the key version number + commandDataWriter.Write(keyRef.VersionNumber); + + // Write the ECC public key + byte[] formatIdentifier = { 0x4 }; // Uncompressed point + var publicKeyRawData = + formatIdentifier + .Concat(pkParams.Q.X) + .Concat(pkParams.Q.Y).ToArray().AsSpan(); + + byte[] publicKeyTlvData = new TlvObject(KeyTypeEccPublicKey, publicKeyRawData).GetBytes().ToArray(); + commandDataWriter.Write(publicKeyTlvData); + + // Write the ECC parameters + var paramsTlv = new TlvObject(KeyTypeEccKeyParams, new byte[] { 0x00 }).GetBytes(); + commandDataWriter.Write(paramsTlv.ToArray()); + commandDataWriter.Write((byte)0); + + // Create and send the command + byte[] commandData = commandDataMs.ToArray(); + var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, commandData); + var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); + + // Get and validate the response + var responseData = response.GetData(); + Span expectedResponseData = new[] { keyRef.VersionNumber }; + + ValidateCheckSum(responseData.Span, expectedResponseData); + + _log.LogInformation("Successsfully put public key for KeyRef {KeyRef}", keyRef); + } + catch (Exception ex) + { + _log.LogError(ex, "Failed to put public key for KeyRef {KeyRef}", keyRef); + throw; + } + } + + private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySpan expectedResponseData) + { + if (!CryptographicOperations.FixedTimeEquals(responseData, expectedResponseData)) + { + throw new SecureChannelException(ExceptionMessages.ChecksumError); + } + } + /// /// Delete the key set with the given keyVersionNumber. If the key /// set to delete is the last SCP key set on the YubiKey, pass @@ -365,27 +428,83 @@ public void PutKeySet(KeyReference keyRef, ECParameters secretKey, int replaceKv /// public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) { - if (Connection is null) - { - throw new InvalidOperationException("No connection initialized. Use the other constructor"); - } - _log.LogInformation("Deleting an SCP key set from a YubiKey."); var command = new DeleteKeyCommand(keyVersionNumber, isLastKey); - var response = Connection.SendCommand(command); - if (response.Status != ResponseStatus.Success) - { - throw new SecureChannelException( - string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.YubiKeyOperationFailed, - response.StatusMessage)); - } + var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); } + // /// + // /// Delete one (or more) keys matching the specified criteria. + // /// + // /// + // /// All keys matching the given KID (Key ID) and/or KVN (Key Version Number) will be deleted, + // /// where 0 is treated as a wildcard. For SCP03 keys, they can only be deleted by KVN. + // /// + // /// A reference to the key(s) to delete. + // /// Must be true if deleting the final key, false otherwise. + // /// + // /// Thrown when both KID and KVN are 0, or when attempting to delete SCP03 keys by KID. + // /// + // /// + // /// Thrown when the delete operation fails. + // /// + // public void DeleteKey(KeyReference keyRef, bool deleteLast) + // { + // if (keyRef.Id == 0 && keyRef.VersionNumber == 0) + // { + // throw new ArgumentException("At least one of KID, KVN must be nonzero"); + // } + // + // byte kid = keyRef.Id; + // byte kvn = keyRef.VersionNumber; + // + // // Special handling for SCP03 keys (1, 2, 3) + // if (kid is 1 or 2 or 3) + // { + // if (kvn != 0) + // { + // kid = 0; // Only delete by KVN for SCP03 + // } + // else + // { + // throw new ArgumentException("SCP03 keys can only be deleted by KVN"); + // } + // } + // + // _log.LogDebug("Deleting keys matching KeyRef {KeyRef}", keyRef); + // + // // Build TLV list for command data + // var tlvList = new List(); + // if (kid != 0) + // { + // tlvList.Add(new TlvObject(0xD0, new[] { kid })); + // } + // + // if (kvn != 0) + // { + // tlvList.Add(new TlvObject(0xD2, new[] { kvn })); + // } + // + // var commandData = TlvObjects.EncodeList(tlvList); + // var command = new DeleteKeyCommand(commandData, deleteLast); + // var response = AuthenticatedConnection.SendCommand(command); + // + // if (response.Status != ResponseStatus.Success) + // { + // throw new SecureChannelException( + // string.Format( + // CultureInfo.CurrentCulture, + // ExceptionMessages.YubiKeyOperationFailed, + // response.StatusMessage)); + // } + // + // _log.LogInformation("Keys deleted"); + // } + /// - /// Generate a new ECC key pair for the given key reference. + /// Generate a new EC key pair for the given key reference. /// /// /// GlobalPlatform has no command to generate key pairs on the card itself. This is a @@ -395,13 +514,9 @@ public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) /// The KID-KVN pair of the key that should be generated. /// The key version number of the key set that should be replaced, or 0 to generate a new key pair. /// The parameters of the generated key, including the curve and the public point. - /// - public ECParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) + public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) { - var connection = Connection ?? - throw new InvalidOperationException("No connection initialized. Use the other constructor"); - - _log.LogDebug( + _log.LogInformation( "Generating new key for {KeyRef}{ReplaceMessage}", keyRef, replaceKvn == 0 @@ -409,33 +524,119 @@ public ECParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) : $", replacing KVN=0x{replaceKvn:X2}"); // Create tlv data for the command - var paramsTlv = new TlvObject(KeyTypeEccKeyParamsTag, new byte[] { 0 }).GetBytes(); + var paramsTlv = new TlvObject(KeyTypeEccKeyParams, new byte[] { 0 }).GetBytes(); byte[] commandData = new byte[paramsTlv.Length + 1]; commandData[0] = keyRef.VersionNumber; paramsTlv.CopyTo(commandData.AsMemory(1)); // Create and send the command var command = new GenerateEcKeyCommand(replaceKvn, keyRef.Id, commandData); - var response = connection.SendCommand(command); - if (response.Status != ResponseStatus.Success) - { - throw new SecureChannelException(response.StatusMessage); - } + var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); // Parse the response, extract the public point var tlvReader = new TlvReader(response.GetData()); - var encodedPoint = tlvReader.ReadValue(KeyTypeEccPublicKeyTag).Span; + var encodedPoint = tlvReader.ReadValue(KeyTypeEccPublicKey).Span; + + // Create the ECParameters with the public point + var eccPublicKey = encodedPoint.CreateEcPublicKeyFromBytes(); + return eccPublicKey; + } + + /// + /// Store the SKI (Subject Key Identifier) for the CA of a given key. + /// Requires off-card entity verification. + /// + /// A reference to the key for which to store the CA issuer. + /// The Subject Key Identifier to store. + public void StoreCaIssuer(KeyReference keyRef, ReadOnlyMemory ski) + { + _log.LogDebug("Storing CA issuer SKI for {KeyRef}", keyRef); + + byte klcc = 0; // Key Loading Card Certificate + switch (keyRef.Id) + { + case ScpKid.Scp11a: + case ScpKid.Scp11b: + case ScpKid.Scp11c: + klcc = 1; + break; + } + + // Create and serialize data + var data = new TlvObject( + ControlReferenceTag, TlvObjects.EncodeList( + new List + { + new TlvObject(0x80, new[] { klcc }), + new TlvObject(0x42, ski.Span), + new TlvObject(0x83, keyRef.GetBytes) + } + )).GetBytes(); + + // Send store data command + StoreData(data); + + _log.LogInformation("CA issuer SKI stored"); + } - // Create the ECParameters object with the public point - return new ECParameters + public void StoreCertificateBundle(KeyReference keyRef, IReadOnlyList certificates) + { + _log.LogDebug("Storing certificate bundle for {KeyRef}", keyRef); + + using var certDataMs = new MemoryStream(); + foreach (var cert in certificates) { - Curve = ECCurve.NamedCurves.nistP256, - Q = new ECPoint + try { - X = encodedPoint.Slice(1, 32).ToArray(), - Y = encodedPoint.Slice(33, 32).ToArray() + byte[] rawCertData = cert.GetRawCertData(); + certDataMs.Write(rawCertData, 0, rawCertData.Length); } - }; + catch (CryptographicException e) + { + throw new ArgumentException("Failed to get encoded version of certificate", e); + } + } + + Memory certDataEncoded = TlvObjects.EncodeMany( + new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), + new TlvObject(CertificateStoreTag, certDataMs.ToArray()) + ); + + StoreData(certDataEncoded); + + _log.LogInformation("Certificate bundle stored"); + } + + /// + /// Stores data in the Security Domain or targeted Application on the YubiKey using the GlobalPlatform STORE DATA command. + /// + /// + /// The STORE DATA command is used to transfer data to either the Security Domain itself or to an Application + /// being personalized. The data must be formatted as BER-TLV structures according to ISO 8825. + /// + /// This implementation: + /// - Uses a single block transfer (P1.b8=1 indicating last block) + /// - Requires BER-TLV formatted data (P1.b5-b4=10) + /// - Does not provide encryption information (P1.b7-b6=00) + /// + /// Note that this command's behavior depends on the current security context: + /// - Outside a personalization session: Data is processed by the Security Domain + /// - During personalization (after INSTALL [for personalization]): Data is forwarded to the target Application + /// + /// + /// The data to be stored, which must be formatted as BER-TLV structures according to ISO 8825. + /// + /// + /// Thrown when no secure connection is available or the security context is invalid. + /// + public void StoreData(ReadOnlyMemory data) + { + _log.LogInformation("Storing data with length:{Length}", data.Length); + + var command = new StoreDataCommand(data); + var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); } /// @@ -447,8 +648,6 @@ public void Reset() { _log.LogDebug("Resetting all SCP keys"); - var connection = _yubiKey.Connect(YubiKeyApplication.SecurityDomain); - const byte insInitializeUpdate = 0x50; const byte insExternalAuthenticate = 0x82; const byte insInternalAuthenticate = 0x88; @@ -486,7 +685,7 @@ public void Reset() // Keys have 65 attempts before blocking (and thus removal) for (int i = 0; i < 65; i++) { - var result = connection.SendCommand( + var result = UnauthenticatedConnection.SendCommand( new ResetCommand(ins, overridenKeyRef.VersionNumber, overridenKeyRef.Id, new byte[8])); switch (result.StatusWord) @@ -497,6 +696,9 @@ public void Reset() break; case SWConstants.InvalidCommandDataParameter: continue; + case SWConstants.Success: + continue; + default: continue; } } @@ -511,10 +713,10 @@ public void Reset() /// A read only dictionary containing the KeyReference as the key and a dictionary of key components as the value. public IReadOnlyDictionary> GetKeyInformation() { - const byte tagKeyInformation = 0xE0; + _log.LogInformation("Getting key information"); var keys = new Dictionary>(); - var getDataResult = GetData(tagKeyInformation).Span; + var getDataResult = GetData(KeyInformationTag).Span; var tlvDataList = TlvObjects.DecodeList(getDataResult); foreach (var tlvObject in tlvDataList) { @@ -542,16 +744,11 @@ public IReadOnlyList GetCertificates(KeyReference keyReference { _log.LogInformation("Getting certificates for key={KeyRef}", keyReference); - const int certificateStoreTag = 0xBF21; - const int controlReferenceTemplateTag = 0xA6; - const int kidKvnTag = 0x83; - - var nestedTlv = new TlvObject( - controlReferenceTemplateTag, - new TlvObject(kidKvnTag, keyReference.GetBytes).GetBytes().Span + var nestedTlv = new TlvObject(ControlReferenceTag, + new TlvObject(KidKvnTag, keyReference.GetBytes).GetBytes().Span ).GetBytes(); - var certificateTlvData = GetData(certificateStoreTag, nestedTlv); + var certificateTlvData = GetData(CertificateStoreTag, nestedTlv); var certificateTlvList = TlvObjects.DecodeList(certificateTlvData.Span); return certificateTlvList @@ -564,15 +761,28 @@ public IReadOnlyList GetCertificates(KeyReference keyReference /// /// The tag of the data to retrieve. /// Optional data to send with the command. - /// The encoded tlv data retrieved from the YubiKey. This will have to be decoded + /// The encoded tlv data retrieved from the YubiKey. public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) { - var connection = Connection ?? _yubiKey.Connect(YubiKeyApplication.SecurityDomain); + var connection = _connection ?? UnauthenticatedConnection; var response = connection.SendCommand(new GetDataCommand(tag, data)); + ThrowIfFailed(response); return response.GetData(); } + private static void ThrowIfFailed(ScpResponse response) + { + if (response.Status != ResponseStatus.Success) + { + throw new SecureChannelException( + string.Format( + CultureInfo.CurrentCulture, + ExceptionMessages.YubiKeyOperationFailed, + response.StatusMessage)); + } + } + /// /// When the ScpSession object goes out of scope, this method is called. /// It will close the session. The most important function of closing a @@ -592,7 +802,7 @@ public void Dispose() return; } - Connection?.Dispose(); + _connection?.Dispose(); _disposed = true; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Derivation.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Derivation.cs index cf3c5a76d..15711dfae 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Derivation.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Derivation.cs @@ -19,6 +19,7 @@ namespace Yubico.YubiKey.Scp03 { + [Obsolete("Use new Derivation class in Yubico.YubiKey.Scp namespace instead. This will be removed in a future release.")] internal static class Derivation { public const byte DDC_SENC = 0x04; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs index d7fc55cf6..afeba966a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Session.cs @@ -20,6 +20,7 @@ namespace Yubico.YubiKey.Scp03 { + [Obsolete("Use new SecurityDomainSesion class in Yubico.YubiKey.Scp namespace instead. This will be removed in a future release.")] internal class Session : IDisposable { private SessionKeys? _sessionKeys; diff --git a/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj b/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj index ad352daf2..346fb7c40 100644 --- a/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj +++ b/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj @@ -31,6 +31,7 @@ limitations under the License. --> + diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index f7b1b1850..c293508f4 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -54,12 +54,12 @@ public void TestImportKey() sk, sk)); - // assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + // assumeFalse("SCP03 not supported over NFC on FIPS capable devices", todo // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); + session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); } using (_ = new SecurityDomainSession(Device, newKeyParams)) @@ -85,13 +85,13 @@ public void TestDeleteKey() // Auth with default key, then replace default key using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(keyRef1.KeyReference, keyRef1.StaticKeys, 0); + session.PutKey(keyRef1.KeyReference, keyRef1.StaticKeys, 0); } // Authenticate with key1, then add additional key, keyref2 using (var session = new SecurityDomainSession(Device, keyRef1)) { - session.PutKeySet(keyRef2.KeyReference, keyRef2.StaticKeys, 0); + session.PutKey(keyRef2.KeyReference, keyRef2.StaticKeys, 0); } // Authenticate with key2, delete key 1 @@ -132,12 +132,12 @@ public void TestReplaceKey() using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(keyRef1.KeyReference, keyRef1.StaticKeys, 0); + session.PutKey(keyRef1.KeyReference, keyRef1.StaticKeys, 0); } using (var session = new SecurityDomainSession(Device, keyRef1)) { - session.PutKeySet(keyRef2.KeyReference, keyRef2.StaticKeys, keyRef1.KeyReference.VersionNumber); + session.PutKey(keyRef2.KeyReference, keyRef2.StaticKeys, keyRef1.KeyReference.VersionNumber); } using (_ = new SecurityDomainSession(Device, keyRef2)) @@ -198,7 +198,7 @@ public void Connect_GetInformation_WithDefaultKey_Returns_DefaultKey() } [Fact] - public void TestGetCertificateBundle() + public void GetCertificates_ReturnsCerts() { Skip.IfNot(Device.FirmwareVersion >= FirmwareVersion.V5_7_2); @@ -229,7 +229,7 @@ public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - session.PutKeySet(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); + session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); } using (var session = new SecurityDomainSession(Device, newKeyParams)) diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 60f47c186..02571aeec 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -15,19 +15,31 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; using Xunit; +using Yubico.Core.Tlv; +using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.TestUtilities; +using ECCurve = System.Security.Cryptography.ECCurve; +using ECPoint = System.Security.Cryptography.ECPoint; + // ReSharper disable UnusedVariable namespace Yubico.YubiKey.Scp { - [SuppressMessage("Style", "IDE0059:Unnecessary assignment of a value")] public class Scp11Tests { + private const byte OceKid = 0x010; + private IYubiKeyDevice Device { get; set; } + public Scp11Tests() { Device = IntegrationTestDeviceEnumeration.GetTestDevice( @@ -39,7 +51,19 @@ public Scp11Tests() } [Fact] - public void Scp11b_Authenticate_Succeeds() // Works + public void Scp11b_PutKeySet_WithPublicKey_Succeeds() + { + var keyReference = new KeyReference(0x010, 0x03); + + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + + var publicKey = new ECPublicKeyParameters(ecdsa); + session.PutKey(keyReference, publicKey, 0); + } + + [Fact] + public void Scp11b_Authenticate_Succeeds() // Works? No? { IReadOnlyCollection certificateList; var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); @@ -51,11 +75,12 @@ public void Scp11b_Authenticate_Succeeds() // Works var leaf = certificateList.Last(); var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); var keyParams = new Scp11KeyParameters(keyReference, ecDsaPublicKey); - + // Try create authenticated session using key params and public key from yubikey using (var session = new SecurityDomainSession(Device, keyParams)) { - session.GetKeyInformation(); + var result = session.GetKeyInformation(); + Assert.NotEmpty(result); } } @@ -64,14 +89,20 @@ public void Scp11b_Import_Succeeds() //Works { var keyReference = new KeyReference(ScpKid.Scp11b, 0x2); var ecDsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + // Start authenticated session with default key using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); - - session.PutKeySet(keyReference, ecDsa.ExportParameters(true), 0); - session.GetKeyInformation(); + + // Import private key + var privateKey = new ECPrivateKeyParameters(ecDsa); + session.PutKey(keyReference, privateKey, 0); + + var result = session.GetKeyInformation(); + Assert.NotEmpty(result); } [Fact] - public void TestGetCertificateBundle() //Works + public void GetCertificates_IsNotEmpty() //Works { using var session = new SecurityDomainSession(Device); @@ -81,125 +112,269 @@ public void TestGetCertificateBundle() //Works Assert.NotEmpty(certificateList); } - + [Fact] + public void StoreCertificateBundle_ReturnsCerts() + { + // var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + // IReadOnlyList certificateList; + // using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + // { + // certificateList = session.GetCertificates(keyReference); + + // Assert.NotEmpty(certificateList); + + // session.DeleteKeySet(keyReference.VersionNumber); + // }; + + // using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + // { + // certificateList = Scp11TestData.OceCerts.Span + // session.StoreCertificateBundle(keyReference, certificateList); // Doesnt work + // }; + } + [Fact] public void GenerateEcKey_Succeeds() // Works { - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); - var keyReference = new KeyReference(ScpKid.Scp11a, 0x3); - + + // Start authenticated session + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + // Generate a new EC key - ECParameters generatedKey = session.GenerateEcKey(keyReference, 0); + var generatedKey = session.GenerateEcKey(keyReference, 0); // Verify the generated key - Assert.NotNull(generatedKey.Q.X); - Assert.NotNull(generatedKey.Q.Y); - Assert.Equal(32, generatedKey.Q.X.Length); // P-256 curve should have 32-byte X and Y coordinates - Assert.Equal(32, generatedKey.Q.Y.Length); - Assert.Equal(ECCurve.NamedCurves.nistP256.Oid.Value, generatedKey.Curve.Oid.Value); + Assert.NotNull(generatedKey.Parameters.Q.X); + Assert.NotNull(generatedKey.Parameters.Q.Y); + Assert.Equal(32, generatedKey.Parameters.Q.X.Length); // P-256 curve should have 32-byte X and Y coordinates + Assert.Equal(32, generatedKey.Parameters.Q.Y.Length); + Assert.Equal(ECCurve.NamedCurves.nistP256.Oid.Value, generatedKey.Parameters.Curve.Oid.Value); - using ECDsa ecdsa = ECDsa.Create(generatedKey); + using var ecdsa = ECDsa.Create(generatedKey.Parameters); Assert.NotNull(ecdsa); } - // [Fact] - // public void Scp11a_Authenticate_Succeeds() - // { - // - // byte kvn = 0x03; - // var keyReference = new KeyReference(ScpKid.Scp11a, kvn); - // - // Scp11KeyParameters keyParams; - // using (var session = new SecurityDomainSession(Device)) - // { - // keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); - // } - // - // using (var session = new SecurityDomainSession(Device, keyParams)) - // { - // session.DeleteKeySet(keyReference.VersionNumber, false); - // } - // } - - - // private Scp11KeyParameters LoadKeys(SecurityDomainSession session, byte scpKid, byte kvn) - // { - // var sessionRef = new KeyReference(scpKid, kvn); - // var oceRef = new KeyReference(OceKid, kvn); - - // PublicKeyValues publicKeyValues = session.GenerateEcKey(sessionRef, 0); - - // var oceCerts = GetOceCertificates(OceCerts); - // if (oceCerts.Ca == null) - // { - // throw new InvalidOperationException("Missing CA certificate"); - // } - // session.PutKey(oceRef, PublicKeyValues.FromPublicKey(oceCerts.Ca.GetPublicKey()), 0); - - // byte[] ski = GetSki(oceCerts.Ca); - // if (ski == null) - // { - // throw new InvalidOperationException("CA certificate missing Subject Key Identifier"); - // } - // session.StoreCaIssuer(oceRef, ski); - - // using (var keyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - // { - // keyStore.Open(OpenFlags.ReadOnly); - - // // Assuming the certificate and private key are installed in the certificate store - // var cert = keyStore.Certificates.Find(X509FindType.FindBySubjectName, "YourCertSubjectName", false)[0]; - // var privateKey = (RSACng)cert.PrivateKey; - - // var certChain = new List { cert }; - // // Add any intermediate certificates to the chain if necessary - - // return new Scp11KeyParameters( - // sessionRef, - // publicKeyValues.ToPublicKey(), - // oceRef, - // privateKey, - // certChain - // ); - // } - // } - - // private ScpCertificates GetOceCertificates(byte[] pem) - // { - // try - // { - // var certificates = new List(); - - // // Convert PEM to a string - // string pemString = System.Text.Encoding.UTF8.GetString(pem); - - // // Split the PEM string into individual certificates - // string[] pemCerts = pemString.Split( - // new[] { "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----" }, - // StringSplitOptions.RemoveEmptyEntries - // ); - - // foreach (string certString in pemCerts) - // { - // if (!string.IsNullOrWhiteSpace(certString)) - // { - // // Remove any whitespace and convert to byte array - // byte[] 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 byte[] GetSki(X509Certificate2 certificate) + [Fact] + public void Scp11a_Authenticate_Succeeds() + { + byte kvn = 0x03; + var keyReference = new KeyReference(ScpKid.Scp11a, kvn); + + // Start authenticated session with default key + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + } + + // Start authenticated session using new key params and public key from yubikey + using (var session = new SecurityDomainSession(Device, keyParams)) + { + session.DeleteKeySet(keyReference.VersionNumber, false); + } + } + + 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); //20 byte + 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); + + // Load the OCE PKCS12 using Bouncy Castle + using var pkcsStream = new MemoryStream(Scp11TestData.Oce.ToArray()); + var pkcs12Store = new Pkcs12Store(pkcsStream, Scp11TestData.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 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"); + } + + var certs = ScpCertificates.From(pkcs12Store.GetCertificateChain(alias) + .Select(certEntry => + { + var cert = DotNetUtilities.ToX509Certificate(certEntry.Certificate); + return new X509Certificate2( + cert.Export(X509ContentType.Cert), + (string)null!, // no password needed for public cert + X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet + ); + })); + + var certChain = new List(certs.Bundle); + if (certs.Leaf != null) + { + certChain.Add(certs.Leaf); + } + + // Now we have the EC private key parameters and cert chain + return new Scp11KeyParameters( + sessionRef, + newPublicKey.Parameters, + oceRef, + ConvertToECParameters(ecPrivateKey), + certChain + ); + + + // var sessionRef = new KeyReference(scpKid, kvn); + // var oceRef = new KeyReference(OceKid, kvn); + // + // var publicKeyParameters = session.GenerateEcKey(sessionRef, 0); + // + // var oceCerts = GetOceCertificates(Scp11TestData.OceCerts.Span); + // if (oceCerts.Ca == null) + // { + // throw new InvalidOperationException("Missing CA certificate"); + // } + // + // var publicKey = + // new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()!.ExportParameters(false)); + // session.PutKeySet(oceRef, publicKey, 0); + // + // var ski = GetSki(oceCerts.Ca); + // if (ski.IsEmpty) + // { + // throw new InvalidOperationException("CA certificate missing Subject Key Identifier"); + // } + // + // session.StoreCaIssuer(oceRef, ski); + // + // using var pfx = + // new X509Certificate2(Scp11TestData.Oce.ToArray(), + // Scp11TestData.OcePassword.ToString()); // OCE is PKCS12 data + // + // var privateKey = pfx.GetECDiffieHellmanPrivateKey(); + // var privateKeyParameters = privateKey!.ExportParameters(true); + // // Build certificate chain + // var certChain = new List(); + // using (var chain = new X509Chain()) + // { + // chain.Build(pfx); // Returns bool but we'll collect certs regardless + // + // // Add certificates from the chain + // foreach (var element in chain.ChainElements) + // { + // certChain.Add(element.Certificate); + // } + // } + // + // // using var keyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + // // keyStore.Open(OpenFlags.ReadOnly); + // + // // // Assuming the certificate and private key are installed in the certificate store + // // var cert = keyStore.Certificates.Find(X509FindType.FindBySubjectName, "YourCertSubjectName", false).First(); + // // var privateKey = cert.GetECDsaPublicKey()!.ExportParameters(true); + // + // // var certChain = new List { cert }; + // // // Add any intermediate certificates to the chain if necessary + // + // return new Scp11KeyParameters( + // sessionRef, + // publicKeyParameters.Parameters, + // oceRef, + // privateKeyParameters, + // certChain + // ); + } + + static ECParameters ConvertToECParameters( + Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters bcPrivateKey) + { + // Convert the BigInteger D to byte array + byte[] 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 + byte[] xBytes = Q.XCoord.ToBigInteger().ToByteArrayUnsigned(); + byte[] 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 + string pemString = Encoding.UTF8.GetString(pem); + + // Split the PEM string into individual certificates + string[] pemCerts = pemString.Split( + new[] { "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----" }, + StringSplitOptions.RemoveEmptyEntries + ); + + foreach (string certString in pemCerts) + { + if (!string.IsNullOrWhiteSpace(certString)) + { + // Remove any whitespace and convert to byte array + byte[] 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)) @@ -207,28 +382,20 @@ private byte[] GetSki(X509Certificate2 certificate) throw new InvalidOperationException("Invalid Subject Key Identifier extension"); } - byte[] rawData = skiExtension.RawData; + var rawData = skiExtension.RawData; if (rawData == null || rawData.Length == 0) { throw new InvalidOperationException("Missing Subject Key Identifier"); } - // The raw data is already in the format we need, so we can return it directly - return rawData; + var tlv = TlvObject.Parse(skiExtension.RawData); + return tlv.Value; } - - // private readonly static byte OCE_KID = 0x010; - private IYubiKeyDevice Device { get; set; } } - - - - - - class Scp11TestData + public static class Scp11TestData { - private readonly static ReadOnlyMemory OCE_CERTS = Encoding.UTF8.GetBytes( + public readonly static ReadOnlyMemory OceCerts = Encoding.UTF8.GetBytes( "-----BEGIN CERTIFICATE-----\n" + "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + @@ -256,43 +423,47 @@ class Scp11TestData "-----END CERTIFICATE-----").AsMemory(); // PKCS12 certificate with a private key and full certificate chain - static readonly Memory OCE = Convert.FromBase64String("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCMEgggfMIIIGz" + - "CCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqGS" + - "Ib3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIHdoQx_USA3j" + - "mRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4G4sboIRw1v" + - "DH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3s0Yx5yMm_x" + - "zw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEYAn0F3LoMET" + - "QytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PBCpwzbFfVCL" + - "a6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8PFClFZJLQld" + - "u5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67joWydWAMp_" + - "lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAKlqCeAfvDvA" + - "3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixVmlwJ8ZbVxD" + - "6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnryAChdBsGdm" + - "StpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpewiLL7C22ler" + - "UT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRaPhOZEESZEw" + - "LKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcohWoO9EMX_bL" + - "K9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO55LN3rNpcD9" + - "-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_ieUBbebPz0" + - "Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXPfZ3jWxspKI" + - "REHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4CwmKu-12R6iY" + - "Q_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_FemmZyNl0qI1Mj" + - "9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0DD55FDIGCy0" + - "FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScfCsefRl7-o_" + - "9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4pfQqMWcPpqV" + - "p4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6cVrgt3EJWLe" + - "y5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDYxBFFtnd6hq" + - "4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJvQl8-Wp4MG" + - "Bsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYrDg7DAg_-Qc" + - "Oi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3uGZbeJEpU1" + - "hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0CAlJVOTF9uE" + - "pLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zCB7DBXBgkqh" + - "kiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUD" + - "BAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxDY7yIoTVQq7" + - "sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzTl5MLFAwn3N" + - "E49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvcNAQkVMRYEF" + - "JBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k23PH-qUXUGP" + - "EuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA==").AsMemory(); - - static readonly ReadOnlyMemory OCE_PASSWORD = "password".AsMemory(); + 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/ScpCertificates.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs index 54f203f17..20c1956ef 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs @@ -1,99 +1,99 @@ -// using System; -// using System.Collections.Generic; -// using System.Linq; -// using System.Security.Cryptography.X509Certificates; +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; } +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; -// } + 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); -// } + public static ScpCertificates From(IEnumerable? certificates) + { + if (certificates == null || !certificates.Any()) + { + return new ScpCertificates(null, Array.Empty(), null); + } -// var certList = new List(certificates); -// X509Certificate2? ca = null; -// byte[]? seenSerial = null; + var certList = new List(certificates); + X509Certificate2? ca = null; + byte[]? seenSerial = null; -// // Order certificates with the Root CA on top -// var ordered = new List { certList[0] }; -// certList.RemoveAt(0); + // 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); + 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, cert)) + { + ordered.Insert(0, cert); + ca = ordered[0]; + continue; + } -// if (IsIssuedBy(cert, tail)) -// { -// ordered.Add(cert); -// continue; -// } + if (IsIssuedBy(cert, tail)) + { + ordered.Add(cert); + continue; + } -// if (IsIssuedBy(head, cert)) -// { -// ordered.Insert(0, 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)}"); -// } + 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(); -// } + // 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); -// } + // 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 ?? Array.Empty(); -// if (keyUsage.Length > 4 && keyUsage[4]) -// { -// leaf = lastCert; -// ordered.RemoveAt(ordered.Count - 1); -// } -// } + X509Certificate2? leaf = null; + if (ordered.Count > 0) + { + // var lastCert = ordered[^1]; todo + // var keyUsage = lastCert.Extensions.OfType().FirstOrDefault()?.KeyUsages ?? Array.Empty(); + // if (keyUsage.Length > 4 && keyUsage[4]) + // { + // leaf = lastCert; + // ordered.RemoveAt(ordered.Count - 1); + // } + } -// return new ScpCertificates(ca, ordered, leaf); -// } + return new ScpCertificates(ca, ordered, leaf); + } -// private static bool IsIssuedBy(X509Certificate2 subjectCert, X509Certificate2 issuerCert) -// { -// return subjectCert.IssuerName.RawData.SequenceEqual(issuerCert.SubjectName.RawData); -// } -// } -// } + 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/Scp03/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs index a6a3237ab..2df01ede9 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs @@ -18,7 +18,7 @@ using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; -namespace Yubico.YubiKey.Scp.Commands +namespace Yubico.YubiKey.Scp { [Trait(TraitTypes.Category, TestCategories.Simple)] public class DeleteKeyCommandTests @@ -86,37 +86,37 @@ public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) 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); - //TODO -#pragma warning disable CS0618 // Type or member is obsolete - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -#pragma warning restore CS0618 // Type or member is obsolete - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new DeleteKeyCommand(3, true); - var 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); +// //TODO +// #pragma warning disable CS0618 // Type or member is obsolete +// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +// #pragma warning restore CS0618 // Type or member is obsolete +// +// Assert.True(isValid); +// Assert.NotNull(connection); +// +// var cmd = new DeleteKeyCommand(3, true); +// var 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 index dd838cb3c..2be7c725d 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs @@ -1,157 +1,158 @@ -// 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); - //TODO - -#pragma warning disable CS0618 // Type or member is obsolete - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -#pragma warning restore CS0618 // Type or member is obsolete - - 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); - //TODO -#pragma warning disable CS0618 // Type or member is obsolete - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -#pragma warning restore CS0618 // Type or member is obsolete - - 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); - //TODO -#pragma warning disable CS0618 // Type or member is obsolete - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -#pragma warning restore CS0618 // Type or member is obsolete - - 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); - } - } -} +// // 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.Scp03; +// 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); +// //TODO +// +// #pragma warning disable CS0618 // Type or member is obsolete +// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +// #pragma warning restore CS0618 // Type or member is obsolete +// +// 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); +// //TODO +// #pragma warning disable CS0618 // Type or member is obsolete +// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +// #pragma warning restore CS0618 // Type or member is obsolete +// +// 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); +// //TODO +// #pragma warning disable CS0618 // Type or member is obsolete +// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +// #pragma warning restore CS0618 // Type or member is obsolete +// +// 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/unit/Yubico/YubiKey/Scp03/DerivationTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs index ed571e511..f279ba825 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs @@ -15,8 +15,8 @@ using System; using Xunit; using Yubico.Core.Buffers; +namespace Yubico.YubiKey.Scp -namespace Yubico.YubiKey.Scp03 { public class DerivationTests { @@ -49,7 +49,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/Scp03/SessionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs index 61ad49384..77132cde8 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs @@ -16,9 +16,10 @@ using Xunit; using Yubico.Core.Buffers; using Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp03; using Yubico.YubiKey.Scp03.Commands; -namespace Yubico.YubiKey.Scp03 +namespace Yubico.YubiKey.Scp { [Obsolete("Replaced by SecurityDomainSession")] public class SessionTests From d2d1cf590ad2acba8c06eeae399ff181bf25d0a2 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 7 Nov 2024 00:42:49 +0100 Subject: [PATCH 05/53] feat(scp): remaining SCP Commands implemented Store identifiers in dictionary Utilize current and next TLV values Improve method functionality Enhance code readability --- .../YubiKey/Scp/Commands/DeleteKeyCommand.cs | 11 +- .../YubiKey/Scp/SecureChannelException.cs | 10 +- .../YubiKey/Scp/SecurityDomainSession.cs | 462 ++++++++++++------ .../Commands/InitializeUpdateResponse.cs | 2 +- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 4 +- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 238 +++++---- .../Scp03/Commands/DeleteKeyCommandTests.cs | 1 + 7 files changed, 473 insertions(+), 255 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs index 352d90dc1..f804d485f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/DeleteKeyCommand.cs @@ -68,9 +68,15 @@ private DeleteKeyCommand() /// deleted but the SCP03 application on the YubiKey will be reset with /// the default key. /// - public DeleteKeyCommand(byte keyVersionNumber, bool isLastKey) + public DeleteKeyCommand(int keyVersionNumber, bool isLastKey) { - _data = new[] { KvnTag, KvnLength, keyVersionNumber }; + _data = new byte[3] { KvnTag, KvnLength, (byte)keyVersionNumber }; + _p2Value = isLastKey ? GpDeleteLastKeyP2 : GpDeleteKeyP2; + } + + internal DeleteKeyCommand(ReadOnlyMemory data, bool isLastKey) + { + _data = data.ToArray(); _p2Value = isLastKey ? GpDeleteLastKeyP2 : GpDeleteKeyP2; } @@ -83,6 +89,7 @@ public DeleteKeyCommand(byte keyVersionNumber, bool isLastKey) Data = _data }; + public ScpResponse CreateResponseForApdu(ResponseApdu responseApdu) => new ScpResponse(responseApdu); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs index 107c36cb7..712aa2eb7 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecureChannelException.cs @@ -17,11 +17,9 @@ namespace Yubico.YubiKey.Scp { /// - /// Represents errors that occur during encoding or decoding data for SCP03. + /// Represents errors that occur during encoding or decoding data for SCP. /// -#pragma warning disable CA1064 // Exceptions should be public - internal class SecureChannelException : Exception -#pragma warning restore CA1064 // Exceptions should be public + public class SecureChannelException : Exception { public SecureChannelException() { @@ -29,13 +27,13 @@ public SecureChannelException() } public SecureChannelException(string message) : - base($"SCP03 CardDataException: {message}") + base($"SCP CardDataException: {message}") { } public SecureChannelException(string message, Exception e) : - base($"SCP03 CardDataException: {message}", e) + base($"SCP CardDataException: {message}", e) { } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 20ae706c4..98029c7d8 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -18,8 +18,10 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; using Microsoft.Extensions.Logging; using Yubico.Core.Iso7816; using Yubico.Core.Logging; @@ -92,15 +94,18 @@ public sealed class SecurityDomainSession : IDisposable private const byte KeyTypeEccKeyParams = 0xF0; private const byte KeyTypeEccPublicKey = 0xB0; private const byte KeyTypeAes = 0x88; - private const ushort CertificateStoreTag = 0xBF21; private const byte ControlReferenceTag = 0xA6; private const byte KidKvnTag = 0x83; private const byte KeyInformationTag = 0xE0; - + private const byte SerialsAllowListTag = 0x70; + private const byte SerialTag = 0x93; + private const byte CardRecognitionDataTag = 0x66; + private const ushort CertificateStoreTag = 0xBF21; + private const ushort CaKlocIdentifiersTag = 0xFF33; // Key Loading OCE Certificate + private const ushort CaKlccIdentifiersTag = 0xFF34; // Key Loading Card Certificate private readonly IYubiKeyDevice _yubiKey; private readonly ILogger _log = Log.GetLogger(); private bool _disposed; - private readonly IScpYubiKeyConnection? _connection; private IScpYubiKeyConnection AuthenticatedConnection => @@ -195,7 +200,7 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey) /// The new SCP03 key set to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the KID is not 0x01 for SCP03 key sets. - /// Thrown when the new key set's checksum failed to verify, or some other error + /// Thrown when the new key set's checksum failed to verify, or some other scp related error /// described in the exception message. public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) { @@ -273,7 +278,8 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) /// The key version number to replace, or 0 for a new key. /// Thrown when the private key is not of type SECP256R1. /// Thrown when no secure session is established. - /// Thrown when key check sum is invalid. + /// Thrown when the new key set's checksum failed to verify, or some other scp related error + /// described in the exception message. public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParameters, int replaceKvn) { _log.LogInformation("Importing SCP11 private key into KeyRef {KeyRef}", keyRef); @@ -343,7 +349,8 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet /// The key version number to replace, or 0 for a new key. /// Thrown when the public key is not of type SECP256R1. /// Thrown when no secure session is established. - /// Thrown when no key check sum is invalid. + /// Thrown when the new key set's checksum failed to verify, or some other scp related error + /// described in the exception message. public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameters, int replaceKvn) { _log.LogInformation("Importing SCP11 public key into KeyRef {KeyRef}", keyRef); @@ -406,102 +413,96 @@ private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySp } } + // /// + // /// Delete the key set with the given keyVersionNumber. If the key + // /// set to delete is the last SCP key set on the YubiKey, pass + // /// true as the isLastKey arg. + // /// + // /// + // /// The key set used to create the SCP session cannot be the key set to + // /// be deleted, unless both of the other key sets have been deleted, and + // /// you pass true for isLastKey. In this case, the key will + // /// be deleted but the SCP application on the YubiKey will be reset + // /// with the default key. + // /// + // /// + // /// The number specifying which key set to delete. + // /// + // /// + // /// If this key set is the last SCP key set on the YubiKey, pass + // /// true, otherwise, pass false. This arg has a default of + // /// false so if no argument is given, it will be false. + // /// + // /// Thrown when there was an scp error, described in the exception message. + // public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) + // { + // _log.LogInformation("Deleting an SCP key set from a YubiKey."); + // var command = new DeleteKeyCommand(keyVersionNumber, isLastKey); + // var response = AuthenticatedConnection.SendCommand(command); + // ThrowIfFailed(response); + // _log.LogInformation("Successfully deleted {{KeyVersionNumber}}", keyVersionNumber); + // } + /// - /// Delete the key set with the given keyVersionNumber. If the key - /// set to delete is the last SCP key set on the YubiKey, pass - /// true as the isLastKey arg. + /// Delete one (or more) keys matching the specified criteria. /// /// - /// The key set used to create the SCP session cannot be the key set to - /// be deleted, unless both of the other key sets have been deleted, and - /// you pass true for isLastKey. In this case, the key will - /// be deleted but the SCP application on the YubiKey will be reset - /// with the default key. + /// All keys matching the given KID (Key ID) and/or KVN (Key Version Number) will be deleted, + /// where 0 is treated as a wildcard. For SCP03 keys, they can only be deleted by KVN. /// - /// - /// The number specifying which key set to delete. - /// - /// - /// If this key set is the last SCP key set on the YubiKey, pass - /// true, otherwise, pass false. This arg has a default of - /// false so if no argument is given, it will be false. - /// - public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) + /// A reference to the key(s) to delete. + /// Must be true if deleting the final key, false otherwise. + /// + /// Thrown when both KID and KVN are 0, or when attempting to delete SCP03 keys by KID. + /// + /// + /// Thrown when the delete operation fails. + /// + public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) { - _log.LogInformation("Deleting an SCP key set from a YubiKey."); + if (keyRef.Id == 0 && keyRef.VersionNumber == 0) + { + throw new ArgumentException("At least one of KID, KVN must be nonzero"); + } + + byte kid = keyRef.Id; + byte kvn = keyRef.VersionNumber; + + // Special handling for SCP03 keys (1, 2, 3) + if (kid == 1 || kid == 2 || kid == 3) + { + if (kvn != 0) + { + kid = 0; + } + else + { + throw new ArgumentException("SCP03 keys can only be deleted by KVN"); + } + } + + _log.LogDebug("Deleting keys matching KeyRef {KeyRef}", keyRef); - var command = new DeleteKeyCommand(keyVersionNumber, isLastKey); + // Build TLV list for command data + var tlvList = new List(); + if (kid != 0) + { + tlvList.Add(new TlvObject(0xD0, new[] { kid })); + } + + if (kvn != 0) + { + tlvList.Add(new TlvObject(0xD2, new[] { kvn })); + } + + byte[] data = TlvObjects.EncodeList(tlvList); + var command = new DeleteKeyCommand(data, deleteLast); var response = AuthenticatedConnection.SendCommand(command); + ThrowIfFailed(response); - } - // /// - // /// Delete one (or more) keys matching the specified criteria. - // /// - // /// - // /// All keys matching the given KID (Key ID) and/or KVN (Key Version Number) will be deleted, - // /// where 0 is treated as a wildcard. For SCP03 keys, they can only be deleted by KVN. - // /// - // /// A reference to the key(s) to delete. - // /// Must be true if deleting the final key, false otherwise. - // /// - // /// Thrown when both KID and KVN are 0, or when attempting to delete SCP03 keys by KID. - // /// - // /// - // /// Thrown when the delete operation fails. - // /// - // public void DeleteKey(KeyReference keyRef, bool deleteLast) - // { - // if (keyRef.Id == 0 && keyRef.VersionNumber == 0) - // { - // throw new ArgumentException("At least one of KID, KVN must be nonzero"); - // } - // - // byte kid = keyRef.Id; - // byte kvn = keyRef.VersionNumber; - // - // // Special handling for SCP03 keys (1, 2, 3) - // if (kid is 1 or 2 or 3) - // { - // if (kvn != 0) - // { - // kid = 0; // Only delete by KVN for SCP03 - // } - // else - // { - // throw new ArgumentException("SCP03 keys can only be deleted by KVN"); - // } - // } - // - // _log.LogDebug("Deleting keys matching KeyRef {KeyRef}", keyRef); - // - // // Build TLV list for command data - // var tlvList = new List(); - // if (kid != 0) - // { - // tlvList.Add(new TlvObject(0xD0, new[] { kid })); - // } - // - // if (kvn != 0) - // { - // tlvList.Add(new TlvObject(0xD2, new[] { kvn })); - // } - // - // var commandData = TlvObjects.EncodeList(tlvList); - // var command = new DeleteKeyCommand(commandData, deleteLast); - // var response = AuthenticatedConnection.SendCommand(command); - // - // if (response.Status != ResponseStatus.Success) - // { - // throw new SecureChannelException( - // string.Format( - // CultureInfo.CurrentCulture, - // ExceptionMessages.YubiKeyOperationFailed, - // response.StatusMessage)); - // } - // - // _log.LogInformation("Keys deleted"); - // } + _log.LogInformation("Keys deleted. KeyRef: ({KeyReference})", keyRef); + } /// /// Generate a new EC key pair for the given key reference. @@ -514,6 +515,7 @@ public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) /// The KID-KVN pair of the key that should be generated. /// The key version number of the key set that should be replaced, or 0 to generate a new key pair. /// The parameters of the generated key, including the curve and the public point. + /// Thrown when there was an scp error, described in the exception message. public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) { _log.LogInformation( @@ -580,17 +582,28 @@ public void StoreCaIssuer(KeyReference keyRef, ReadOnlyMemory ski) _log.LogInformation("CA issuer SKI stored"); } - public void StoreCertificateBundle(KeyReference keyRef, IReadOnlyList certificates) + /// + /// Store a list of certificates associated with the given key reference. //TODO should document limitations of this command as well as other storedata command + /// + /// The key reference associated with the certificates. + /// The certificates to store. + /// + /// The certificates will be stored in the order they are provided in the list. + /// + /// Thrown when certificatedata + /// Thrown when there was an scp error, described in the exception message. + public void StoreCertificates(KeyReference keyRef, IReadOnlyList certificates) { _log.LogDebug("Storing certificate bundle for {KeyRef}", keyRef); + // Write each certificate to a memory stream using var certDataMs = new MemoryStream(); foreach (var cert in certificates) { try { - byte[] rawCertData = cert.GetRawCertData(); - certDataMs.Write(rawCertData, 0, rawCertData.Length); + byte[] certTlvEncoded = cert.GetRawCertData(); // ASN.1 DER (TLV) encoded certificate + certDataMs.Write(certTlvEncoded, 0, certTlvEncoded.Length); } catch (CryptographicException e) { @@ -598,8 +611,9 @@ public void StoreCertificateBundle(KeyReference keyRef, IReadOnlyList certDataEncoded = TlvObjects.EncodeMany( - new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), + new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), new TlvObject(CertificateStoreTag, certDataMs.ToArray()) ); @@ -608,6 +622,46 @@ public void StoreCertificateBundle(KeyReference keyRef, IReadOnlyList + /// Stores an allowlist of certificate serial numbers for a specified key reference. + /// + /// + /// This method requires off-card entity verification. If an allowlist is not stored, any + /// certificate signed by the CA can be used. + /// + /// A reference to the key for which the allowlist will be stored. + /// The list of certificate serial numbers to be stored in the allowlist. + /// Thrown when a serial number cannot be encoded properly. + /// Thrown when there was an scp error, described in the exception message. + public void StoreAllowlist(KeyReference keyRef, IReadOnlyList serials) + { + _log.LogDebug("Storing allow list for {KeyRef}", keyRef); + + using var serialDataMs = new MemoryStream(); + foreach (var serial in serials) + { + try + { + byte[] serialAsBytes = serial.ToByteArray(); + byte[] serialTlvEncoded = new TlvObject(SerialTag, serialAsBytes).GetBytes().ToArray(); + serialDataMs.Write(serialTlvEncoded, 0, serialTlvEncoded.Length); + } + catch (CryptographicException e) + { + throw new ArgumentException("Failed to get encoded version of certificate", e); + } + } + + Memory serialsDataEncoded = TlvObjects.EncodeMany( + new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), + new TlvObject(SerialsAllowListTag, serialDataMs.ToArray()) + ); + + StoreData(serialsDataEncoded); + + _log.LogInformation("Certificate bundle stored"); + } + /// /// Stores data in the Security Domain or targeted Application on the YubiKey using the GlobalPlatform STORE DATA command. /// @@ -630,6 +684,7 @@ public void StoreCertificateBundle(KeyReference keyRef, IReadOnlyList /// Thrown when no secure connection is available or the security context is invalid. /// + /// Thrown when there was an scp error, described in the exception message. public void StoreData(ReadOnlyMemory data) { _log.LogInformation("Storing data with length:{Length}", data.Length); @@ -639,6 +694,155 @@ public void StoreData(ReadOnlyMemory data) ThrowIfFailed(response); } + /// + /// Retrieves the key information stored in the YubiKey and returns it in a dictionary format. + /// + /// A read only dictionary containing the KeyReference as the key and a dictionary of key components as the value. + /// Thrown when there was an scp error, described in the exception message. + public IReadOnlyDictionary> GetKeyInformation() + { + _log.LogInformation("Getting key information"); + + var keyInformation = new Dictionary>(); + + var getDataResult = GetData(KeyInformationTag); + var tlvList = TlvObjects.DecodeList(getDataResult.Span); + foreach (var tlvObject in tlvList) + { + 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); + } + + return keyInformation; + } + + /// + /// Retrieves the certificates associated with the given . + /// + /// The key reference for which the certificates should be retrieved. + /// A list of X.509 certificates associated with the key reference. + /// Thrown when there was an scp error, described in the exception message. + public IReadOnlyList GetCertificates(KeyReference keyReference) + { + _log.LogInformation("Getting certificates for key={KeyRef}", keyReference); + + var nestedTlv = new TlvObject(ControlReferenceTag, + new TlvObject(KidKvnTag, keyReference.GetBytes).GetBytes().Span + ).GetBytes(); + + var certificateTlvData = GetData(CertificateStoreTag, nestedTlv); + var certificateTlvList = TlvObjects.DecodeList(certificateTlvData.Span); + + return certificateTlvList + .Select(tlv => new X509Certificate2(tlv.GetBytes().ToArray())) + .ToList(); + } + + /// + /// Gets the supported CA identifiers for KLOC and/or KLCC. + /// + /// Whether to retrieve Key Loading OCE Certificate (KLOC) identifiers. + /// Whether to retrieve Key Loading Card Certificate (KLCC) identifiers. + /// A dictionary of KeyReference and byte arrays representing the CA identifiers. + /// Thrown when both kloc and klcc are false. + /// Thrown when there was an SCP error, described in the exception message. + public IReadOnlyDictionary> GetSupportedCaIdentifiers(bool kloc, bool klcc) + { + if (!kloc && !klcc) + { + throw new ArgumentException("At least one of kloc and klcc must be true"); + } + + _log.LogDebug("Getting CA identifiers KLOC={Kloc}, KLCC={Klcc}", kloc, klcc); + + var dataMs = new MemoryStream(); + + if (kloc) + { + try + { + + var klocData = GetData(CaKlocIdentifiersTag); + dataMs.Write(klocData.Span.ToArray(), 0, klocData.Length); + } + catch (SecureChannelException e) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) + { + + // Ignore this specific exception + } + } + + if (klcc) + { + try + { + var klccData = GetData(CaKlccIdentifiersTag); + dataMs.Write(klccData.Span.ToArray(), 0, klccData.Length); + } + catch (SecureChannelException e) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) + { + // Ignore this specific exception + } + } + + var tlvs = TlvObjects.DecodeList(dataMs.ToArray()); + var identifiers = new Dictionary>(); + + var tlvsSpan = tlvs.ToArray().AsSpan(); + while (!tlvsSpan.IsEmpty) + { + var current = tlvsSpan[0]; + var next = tlvsSpan[1]; + + var refData = next.GetBytes().Span; + var keyRef = new KeyReference(refData[0], refData[1]); + identifiers[keyRef] = current.GetBytes(); + + tlvsSpan = tlvsSpan[..2]; + } + + return identifiers; + } + + + public Memory GetCardRecognitionData() + { + _log.LogInformation("Getting card recognition deta"); + + var tlvData = GetData(CardRecognitionDataTag, null).Span; + var cardRecognitionData = TlvObjects.UnpackValue(0x73, tlvData); + + return cardRecognitionData; + } + + /// + /// Gets data from the YubiKey associated with the given tag. + /// + /// The tag of the data to retrieve. + /// Optional data to send with the command. + /// The encoded tlv data retrieved from the YubiKey. + /// Thrown when there was an scp error, described in the exception message. + public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) + { + var connection = _connection ?? UnauthenticatedConnection; + + var command = new GetDataCommand(tag, data); + var response = connection.SendCommand(command); + ThrowIfFailed(response); + + return response.GetData(); + } + + /// /// Perform a factory reset of the Security Domain. /// This will remove all keys and associated data, as well as restore the default SCP03 static keys, @@ -707,70 +911,6 @@ public void Reset() _log.LogInformation("SCP keys reset"); } - /// - /// Retrieves the key information stored in the YubiKey and returns it in a dictionary format. - /// - /// A read only dictionary containing the KeyReference as the key and a dictionary of key components as the value. - public IReadOnlyDictionary> GetKeyInformation() - { - _log.LogInformation("Getting key information"); - - var keys = new Dictionary>(); - var getDataResult = GetData(KeyInformationTag).Span; - var tlvDataList = TlvObjects.DecodeList(getDataResult); - foreach (var tlvObject in tlvDataList) - { - var value = TlvObjects.UnpackValue(0xC0, tlvObject.GetBytes().Span); - var keyRef = new KeyReference(value.Span[0], value.Span[1]); - var keyComponents = new Dictionary(); - - while (!(value = value[2..]).IsEmpty) - { - keyComponents.Add(value.Span[0], value.Span[1]); - } - - keys.Add(keyRef, keyComponents); - } - - return new ReadOnlyDictionary>(keys); - } - - /// - /// Retrieves the certificates associated with the given . - /// - /// The key reference for which the certificates should be retrieved. - /// A list of X.509 certificates associated with the key reference. - public IReadOnlyList GetCertificates(KeyReference keyReference) - { - _log.LogInformation("Getting certificates for key={KeyRef}", keyReference); - - var nestedTlv = new TlvObject(ControlReferenceTag, - new TlvObject(KidKvnTag, keyReference.GetBytes).GetBytes().Span - ).GetBytes(); - - var certificateTlvData = GetData(CertificateStoreTag, nestedTlv); - var certificateTlvList = TlvObjects.DecodeList(certificateTlvData.Span); - - return certificateTlvList - .Select(tlv => new X509Certificate2(tlv.GetBytes().ToArray())) - .ToList(); - } - - /// - /// Gets data from the YubiKey associated with the given tag. - /// - /// The tag of the data to retrieve. - /// Optional data to send with the command. - /// The encoded tlv data retrieved from the YubiKey. - public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) - { - var connection = _connection ?? UnauthenticatedConnection; - var response = connection.SendCommand(new GetDataCommand(tag, data)); - ThrowIfFailed(response); - - return response.GetData(); - } - private static void ThrowIfFailed(ScpResponse response) { if (response.Status != ResponseStatus.Success) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs index 19ee5e979..7410fef90 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs @@ -19,7 +19,7 @@ namespace Yubico.YubiKey.Scp03.Commands { - [Obsolete("Use new InitializeUpdateResponse instead")] + [Obsolete("Use new InitializeUpdateResponse instead")] // TODO Verify still works internal class InitializeUpdateResponse : Scp03Response { public IReadOnlyCollection DiversificationData { get; protected set; } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index c293508f4..cc3ab37fc 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -97,7 +97,7 @@ public void TestDeleteKey() // Authenticate with key2, delete key 1 using (var session = new SecurityDomainSession(Device, keyRef2)) { - session.DeleteKeySet(keyRef1.KeyReference.VersionNumber); + session.DeleteKeySet(keyRef1.KeyReference); } // Authenticate with key 1, @@ -111,7 +111,7 @@ public void TestDeleteKey() using (var session = new SecurityDomainSession(Device, keyRef2)) { - session.DeleteKeySet(keyRef2.KeyReference.VersionNumber, true); + session.DeleteKeySet(keyRef2.KeyReference, true); } // Try to authenticate with key 2, diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 02571aeec..54120272d 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -17,6 +17,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -27,6 +28,7 @@ using Xunit; using Yubico.Core.Tlv; using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; using ECCurve = System.Security.Cryptography.ECCurve; using ECPoint = System.Security.Cryptography.ECPoint; @@ -51,15 +53,18 @@ public Scp11Tests() } [Fact] - public void Scp11b_PutKeySet_WithPublicKey_Succeeds() + public void Scp11b_PutKey_WithPublicKey_Succeeds() { - var keyReference = new KeyReference(0x010, 0x03); + var keyReference = new KeyReference(0x10, 0x3); - using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); var publicKey = new ECPublicKeyParameters(ecdsa); session.PutKey(keyReference, publicKey, 0); + + var keyInformation = session.GetKeyInformation(); + Assert.True(keyInformation.ContainsKey(keyReference)); } [Fact] @@ -113,24 +118,16 @@ public void GetCertificates_IsNotEmpty() //Works } [Fact] - public void StoreCertificateBundle_ReturnsCerts() + public void StoreCertificates_SavesCertificatesOnYubikey() { - // var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); - // IReadOnlyList certificateList; - // using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) - // { - // certificateList = session.GetCertificates(keyReference); - - // Assert.NotEmpty(certificateList); - - // session.DeleteKeySet(keyReference.VersionNumber); - // }; - - // using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) - // { - // certificateList = Scp11TestData.OceCerts.Span - // session.StoreCertificateBundle(keyReference, certificateList); // Doesnt work - // }; + var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + session.StoreCertificates(keyReference, GetOceCertificates(Scp11TestData.OceCerts.Span).Bundle); + var result = session.GetCertificates(keyReference); + + Assert.Single(result); + Assert.Equal("085C9FB32DAE995758BF0C989E29C7503411C779", result[0].Thumbprint); } [Fact] @@ -156,7 +153,96 @@ public void GenerateEcKey_Succeeds() // Works } [Fact] - public void Scp11a_Authenticate_Succeeds() + public void Scp11aAllowListTest() + { + byte kvn = 0x05; + var oceKeyRef = new KeyReference(OceKid, kvn); + + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + } + + var serials = new List + { + // Serial numbers from oce certs + System.Numerics.BigInteger.Parse("7f4971b0ad51f84c9da9928b2d5fef5e16b2920a", System.Globalization.NumberStyles.HexNumber), + System.Numerics.BigInteger.Parse("6b90028800909f9ffcd641346933242748fbe9ad", System.Globalization.NumberStyles.HexNumber) + }; + + using (var session = new SecurityDomainSession(Device, keyParams)) + { + session.StoreAllowlist(oceKeyRef, serials); // Works until here + } + + using (var session = new SecurityDomainSession(Device, keyParams)) // Test this on Android + { + session.DeleteKeySet(new KeyReference(ScpKid.Scp11a, kvn), false); + } + } + + [Fact] + public void Scp11aAllowListBlocked() // Works + { + byte kvn = 0x03; + var oceKeyRef = new KeyReference(OceKid, kvn); + + + Scp03KeyParameters scp03KeyParams; + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + // Import SCP03 key and get key parameters + scp03KeyParams = ImportScp03Key(session); + } + + Scp11KeyParameters scp11KeyParams; + using (var session = new SecurityDomainSession(Device, scp03KeyParams)) + { + // Make space for new key + session.DeleteKeySet(new KeyReference(ScpKid.Scp11b, 0x01), false); + + // Load SCP11a keys + scp11KeyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + + // Create list of serial numbers + var serials = new List + { + new System.Numerics.BigInteger(1), + new System.Numerics.BigInteger(2), + new System.Numerics.BigInteger(3), + new System.Numerics.BigInteger(4), + new System.Numerics.BigInteger(5) + }; + + // Store the allow list + session.StoreAllowlist(oceKeyRef, serials); + } + + // Authenticate with SCP11a should throw + Assert.Throws(() => + { + using (var session = new SecurityDomainSession(Device, scp11KeyParams)) + { + // ... + } + }); + + // Reset the allow list + using (var session = new SecurityDomainSession(Device, scp03KeyParams)) + { + session.StoreAllowlist(oceKeyRef, new List()); + } + + // Authenticate with SCP11a should now succeed + using (var session = new SecurityDomainSession(Device, scp11KeyParams)) + { + // ... Should work + } + } + + [Fact] + public void Scp11a_Authenticate_Succeeds() // Works { byte kvn = 0x03; var keyReference = new KeyReference(ScpKid.Scp11a, kvn); @@ -171,10 +257,30 @@ public void Scp11a_Authenticate_Succeeds() // Start authenticated session using new key params and public key from yubikey using (var session = new SecurityDomainSession(Device, keyParams)) { - session.DeleteKeySet(keyReference.VersionNumber, false); + session.DeleteKeySet(keyReference, false); } } + [Fact] + public void TestScp11cAuthenticate() + { + const byte kvn = 0x03; + var keyReference = new KeyReference(ScpKid.Scp11c, kvn); + + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + { + keyParams = LoadKeys(session, ScpKid.Scp11c, kvn); + } + + Assert.Throws(() => + { + using var session = new SecurityDomainSession(Device, keyParams); + session.DeleteKeySet(keyReference, false); + }); + } + + private Scp11KeyParameters LoadKeys( SecurityDomainSession session, byte scpKid, @@ -249,67 +355,6 @@ private Scp11KeyParameters LoadKeys( ConvertToECParameters(ecPrivateKey), certChain ); - - - // var sessionRef = new KeyReference(scpKid, kvn); - // var oceRef = new KeyReference(OceKid, kvn); - // - // var publicKeyParameters = session.GenerateEcKey(sessionRef, 0); - // - // var oceCerts = GetOceCertificates(Scp11TestData.OceCerts.Span); - // if (oceCerts.Ca == null) - // { - // throw new InvalidOperationException("Missing CA certificate"); - // } - // - // var publicKey = - // new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()!.ExportParameters(false)); - // session.PutKeySet(oceRef, publicKey, 0); - // - // var ski = GetSki(oceCerts.Ca); - // if (ski.IsEmpty) - // { - // throw new InvalidOperationException("CA certificate missing Subject Key Identifier"); - // } - // - // session.StoreCaIssuer(oceRef, ski); - // - // using var pfx = - // new X509Certificate2(Scp11TestData.Oce.ToArray(), - // Scp11TestData.OcePassword.ToString()); // OCE is PKCS12 data - // - // var privateKey = pfx.GetECDiffieHellmanPrivateKey(); - // var privateKeyParameters = privateKey!.ExportParameters(true); - // // Build certificate chain - // var certChain = new List(); - // using (var chain = new X509Chain()) - // { - // chain.Build(pfx); // Returns bool but we'll collect certs regardless - // - // // Add certificates from the chain - // foreach (var element in chain.ChainElements) - // { - // certChain.Add(element.Certificate); - // } - // } - // - // // using var keyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); - // // keyStore.Open(OpenFlags.ReadOnly); - // - // // // Assuming the certificate and private key are installed in the certificate store - // // var cert = keyStore.Certificates.Find(X509FindType.FindBySubjectName, "YourCertSubjectName", false).First(); - // // var privateKey = cert.GetECDsaPublicKey()!.ExportParameters(true); - // - // // var certChain = new List { cert }; - // // // Add any intermediate certificates to the chain if necessary - // - // return new Scp11KeyParameters( - // sessionRef, - // publicKeyParameters.Parameters, - // oceRef, - // privateKeyParameters, - // certChain - // ); } static ECParameters ConvertToECParameters( @@ -391,8 +436,35 @@ private Memory GetSki( var tlv = TlvObject.Parse(skiExtension.RawData); return tlv.Value; } + + private static Scp03KeyParameters ImportScp03Key(SecurityDomainSession session) + { + // assumeFalse("SCP03 management not supported over NFC on FIPS capable devices", + // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); // todo + + var scp03Ref = new KeyReference((byte)0x01, (byte)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(); + } } + public static class Scp11TestData { public readonly static ReadOnlyMemory OceCerts = Encoding.UTF8.GetBytes( diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs index 2df01ede9..e5cff0be9 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs @@ -82,6 +82,7 @@ public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) Assert.NotNull(connection); var cmd = new Scp.Commands.DeleteKeyCommand(2, false); + var rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } From 4017ae4cd95d313b1d41067538d37c5c8ffe4492 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Thu, 7 Nov 2024 01:25:51 +0100 Subject: [PATCH 06/53] docs: updated docstrings and manual update auth-decrypt.md update auth-sign.md updated RSA key sizes in user manual files updated RSA key info in API docs updated docs and minor adjustments and new tests --- Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs | 2 +- .../application-piv/apdu/auth-decrypt.md | 2 +- .../application-piv/apdu/auth-sign.md | 2 +- .../application-piv/apdu/generate-pair.md | 2 +- .../application-piv/apdu/import-asym.md | 2 +- .../application-piv/apdu/metadata.md | 2 +- .../Resources/ExceptionMessages.Designer.cs | 2 +- .../src/Resources/ExceptionMessages.resx | 2 +- .../src/Yubico/YubiKey/ApplicationSession.cs | 130 +++++++ .../src/Yubico/YubiKey/ConnectionFactory.cs | 83 +++-- .../YubiKey/Cryptography/AesUtilities.cs | 4 +- .../YubiKey/Cryptography/ECKeyParameters.cs | 6 + .../Cryptography/ECParametersExtensions.cs | 33 +- .../Cryptography/ECPrivateKeyParameters.cs | 32 +- .../Cryptography/ECPublicKeyParameters.cs | 40 ++- .../src/Yubico/YubiKey/IYubiKeyDevice.cs | 162 ++++----- .../src/Yubico/YubiKey/Otp/OtpSession.cs | 61 ++-- .../YubiKey/Pipelines/ScpApduTransform.cs | 86 +++-- .../Commands/AuthenticateDecryptCommand.cs | 3 +- .../Yubico/YubiKey/Scp/ChannelEncryption.cs | 38 ++- .../Commands/ExternalAuthenticateCommand.cs | 14 +- .../Commands/ExternalAuthenticateResponse.cs | 7 +- .../Scp/Commands/InitializeUpdateCommand.cs | 2 +- .../Scp/Commands/InitializeUpdateResponse.cs | 8 +- .../YubiKey/Scp/Commands/ResetCommand.cs | 28 +- .../YubiKey/Scp/Commands/ScpResponse.cs | 5 +- .../Scp/Commands/SecurityOperationCommand.cs | 45 ++- .../src/Yubico/YubiKey/Scp/KeyReference.cs | 35 +- .../Yubico/YubiKey/Scp/Scp11KeyParameters.cs | 96 ++++-- .../src/Yubico/YubiKey/Scp/ScpConnection.cs | 99 +++--- .../src/Yubico/YubiKey/Scp/ScpState.cs | 52 +-- .../YubiKey/Scp/SecurityDomainSession.cs | 322 ++++++++---------- .../src/Yubico/YubiKey/Scp03/Scp03Session.cs | 6 +- .../src/Yubico/YubiKey/Scp03/StaticKeys.cs | 2 +- .../src/Yubico/YubiKey/SmartCardConnection.cs | 136 +++++--- .../Yubico/YubiKey/YubiKeyDevice.Instance.cs | 2 - .../src/Yubico/YubiKey/YubiKeyFeature.cs | 12 +- .../YubiKey/YubiKeyFeatureExtensions.cs | 12 +- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 220 ++++++++---- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 153 +++++---- .../YubiKey/Cryptography/AesUtilitiesTests.cs | 4 +- .../YubiKey/Scp03/ChannelEncryptionTests.cs | 4 +- 42 files changed, 1239 insertions(+), 719 deletions(-) create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs diff --git a/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs index d8692d9b6..af5249ead 100644 --- a/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs @@ -13,7 +13,7 @@ public static class TlvObjects /// Decodes a sequence of BER-TLV encoded data into a list of Tlvs. /// /// Sequence of TLV encoded data - /// List of Tlvs + /// List of public static IReadOnlyList DecodeList(ReadOnlySpan data) { var tlvs = new List(); diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-decrypt.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-decrypt.md index 6d0e2bc33..571ed69ff 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-decrypt.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-decrypt.md @@ -20,7 +20,7 @@ limitations under the License. --> |:---:|:---:|:-----------:|:-------------:|:----------:|:-------------------------:|:--------:| | 00 | 87 | *algorithm* | *slot number* | *data len* | *encoded data to decrypt* | (absent) | -The *algorithm* is either `06` (RSA-1048), `07` (RSA-2048), `05` (RSA 3072), or `16` (RSA 4096). Note that it is not possible +The *algorithm* is either `06` (RSA-1024), `07` (RSA-2048), `05` (RSA 3072), or `16` (RSA 4096). Note that it is not possible to decrypt using ECC. The *slot number* can be the number of any slot that holds a private key, other than `F9`. diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md index 2851caa9d..d13e61a58 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md @@ -20,7 +20,7 @@ limitations under the License. --> |:---:|:---:|:-----------:|:-------------:|:----------:|:--------------------------------:|:--------:| | 00 | 87 | *algorithm* | *slot number* | *data len* | *encoded digest of data to sign* | (absent) | -The *algorithm* is either `06` (RSA-1048), `07` (RSA-2048), `05` (RSA 3072), `16` (RSA 4096), `11` (ECC-P256), or `14` +The *algorithm* is either `06` (RSA-1024), `07` (RSA-2048), `05` (RSA 3072), `16` (RSA 4096), `11` (ECC-P256), or `14` (ECC-P384). The *slot number* can be the number of any slot that holds a private key, other than `F9`. diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md index 72d411ed1..3de7a42a2 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md @@ -36,7 +36,7 @@ The value for the "remaining bytes" field must be equal to the number of bytes t bytes come after the "remaining bytes" field, the field's value must be 03. There are six choices for "alg" (algorithm and size): RSA-1024 (06), -RSA-2048 (07), RSA 3072 (05), RSA 4096 (16), ECC-P-256 (11), and ECC-P-384 (14). +RSA-2048 (07), RSA 3072 (08), RSA 4096 (09), ECC-P-256 (11), and ECC-P-384 (14). Both the PIN policy and touch policy are optional. If either or both are not given, they will be default. The default for PIN is "once" and touch is "never". diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md index 15524bee2..4be530bc8 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md @@ -30,7 +30,7 @@ F9 ``` There are six choices for "alg" (algorithm and size): RSA-1024 (06), -RSA-2048 (07), RSA 3072 (05), RSA 4096 (16), ECC-P-256 (11), and ECC-P-384 (14). +RSA-2048 (07), RSA 3072 (08), RSA 4096 (09), ECC-P-256 (11), and ECC-P-384 (14). The key data to load is a set of TLV constructions. The L (length) is DER encoding format. The V is the integer in canonical form. If the key is an RSA private key, there diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md index 592b69cb4..4db309d94 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md @@ -41,7 +41,7 @@ rules. The values (V of TLV) are dependent on the tags, described in the table b Tag | Name | Meaning | Data | Slots :---: | :---: | :---: | :---: -01 | Algorithm| Algorithm/Type of the key | ff (PIN or PUK), 03 (Triple DES), 08 (AES-128),
0A (AES-192), 0C (AES-256),
06 (RSA-1024), 07 (RSA-2048),
05 (RSA 3072), 16 (RSA 4096)
11 (ECC-P256), or 14 (ECC-P384) | all slots +01 | Algorithm| Algorithm/Type of the key | ff (PIN or PUK), 03 (Triple DES), 08 (AES-128),
0A (AES-192), 0C (AES-256),
06 (RSA-1024), 07 (RSA-2048),
08 (RSA 3072), 09 (RSA 4096)
11 (ECC-P256), or 14 (ECC-P384) | all slots 02 | Policy| PIN and touch policy | PIN: 0 (Default), 1 (Never),
2 (Once), 3 (Always)
Touch: 0 (Default), 1 (Never),
2 (Always), 3 (Cached) | 9a, 9b, 9c, 9d, 9e, f9, 82 - 95 03 | Origin| Imported or generated | 1 (generated), 2 (imported) | 9a, 9c, 9d, 9e, f9, 82 - 95 04 | Public| Pub key partner to the pri key | DER encoding of public key | 9a, 9c, 9d, 9e, f9, 82 - 95 diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs index 6bbad079c..aa7863731 100644 --- a/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs +++ b/Yubico.YubiKey/src/Resources/ExceptionMessages.Designer.cs @@ -2184,7 +2184,7 @@ internal static string UnknownFidoError { } /// - /// Looks up a localized string similar to An unknown SCP error has occurred.. + /// Looks up a localized string similar to An unknown SCP error has occurred. /// internal static string UnknownScpError { get { diff --git a/Yubico.YubiKey/src/Resources/ExceptionMessages.resx b/Yubico.YubiKey/src/Resources/ExceptionMessages.resx index 8df5141a7..f73f2e6a9 100644 --- a/Yubico.YubiKey/src/Resources/ExceptionMessages.resx +++ b/Yubico.YubiKey/src/Resources/ExceptionMessages.resx @@ -353,7 +353,7 @@ Candidate for removal - An unknown SCP 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. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs new file mode 100644 index 000000000..f0a53afed --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs @@ -0,0 +1,130 @@ +// 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.Globalization; +using Microsoft.Extensions.Logging; +using Yubico.YubiKey.InterIndustry.Commands; +using Yubico.YubiKey.Scp; + +namespace Yubico.YubiKey +{ + /// + /// Abstract base class for sessions with a YubiKey. This class is used + /// to wrap the IYubiKeyConnection and provide a way of + /// interacting with the connection that is more convenient for most + /// users. + /// + public abstract class ApplicationSession : IDisposable + { + /// + /// The object that represents the connection to the YubiKey. Most + /// applications will ignore this, but it can be used to call Commands + /// directly. + /// + public IYubiKeyConnection Connection { get; } + + /// + /// Gets the parameters used for establishing a Secure Channel Protocol (SCP) connection. + /// + public ScpKeyParameters? KeyParameters { get; } + + protected ILogger Logger { get; } + protected IYubiKeyDevice YubiKey { get; } + + private bool _disposed; + + /// + /// Initializes a new instance of the class with logging, YubiKey device, application, and optional SCP key parameters. + /// + /// The logger instance used for logging information. + /// The YubiKey device to establish a session with. + /// The specific YubiKey application to connect to. + /// The optional parameters for an SCP connection. + /// Thrown when the is null. + /// Thrown when the Yubikey does not support the requested SCP connection. + protected ApplicationSession( + ILogger logger, + IYubiKeyDevice device, + YubiKeyApplication application, + ScpKeyParameters? keyParameters) + { + Logger = logger; + YubiKey = device ?? throw new ArgumentNullException(nameof(device)); + KeyParameters = keyParameters; + Connection = GetConnection(YubiKey, application, KeyParameters); + } + + private IYubiKeyConnection GetConnection( + IYubiKeyDevice yubiKey, + YubiKeyApplication application, + ScpKeyParameters? keyParameters) + { + string scpType = keyParameters switch + { + Scp03KeyParameters _ when application == YubiKeyApplication.Oath && yubiKey.HasFeature(YubiKeyFeature.Scp03Oath) + => "SCP03", + Scp03KeyParameters _ when yubiKey.HasFeature(YubiKeyFeature.Scp03) + => "SCP03", + Scp11KeyParameters _ when yubiKey.HasFeature(YubiKeyFeature.Scp11) + => "SCP11", + null => string.Empty, + _ => throw new InvalidOperationException("The YubiKey does not support the requested SCP connection.") + }; + + string possibleScpDescription = string.IsNullOrEmpty(scpType) ? string.Empty : $" over {scpType}"; + Logger.LogInformation($"Connecting to {GetApplicationFriendlyName(application)}{possibleScpDescription}"); + + var connection = keyParameters != null + ? yubiKey.Connect(application, keyParameters) + : yubiKey.Connect(application); + + Logger.LogInformation($"Connected to {GetApplicationFriendlyName(application)}"); + return connection; + } + + private static string GetApplicationFriendlyName(YubiKeyApplication application) => Enum.GetName(typeof(YubiKeyApplication), application) ?? "Unknown"; + + /// + /// Clean up the resources used by the session. + /// + public virtual void Dispose() + { + if (_disposed) + { + return; + } + + // At the moment, there is no "close session" method. So for now, + // just connect to the management application. + // This can fail, possibly resulting in a SCardException (or other), so we wrap it in a try catch-block to complete the disposal of the PivSession + try + { + _ = Connection.SendCommand(new SelectApplicationCommand(YubiKeyApplication.Management)); + } + catch (Exception e) + { + string message = string.Format( + CultureInfo.CurrentCulture, + ExceptionMessages.SessionDisposeUnknownError, e.GetType(), e.Message); + + Logger.LogWarning(message); + } + + Connection.Dispose(); + _disposed = true; + GC.SuppressFinalize(this); + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs index 0e824403f..b907fdc0a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Yubico AB +// 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. @@ -18,12 +18,18 @@ using Yubico.Core.Devices.Hid; using Yubico.Core.Devices.SmartCard; using Yubico.YubiKey.Scp; -using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey { - // TODO Consider merging with ConnectionManager - public class ConnectionFactory + /// + /// Factory class responsible for creating connections to YubiKey devices. + /// + /// + /// The ConnectionFactory manages the creation of different types of connections to a YubiKey device, + /// including SmartCard and HID interfaces. It handles both secure channel protocol (SCP) and standard + /// connections based on the application requirements. + /// + internal class ConnectionFactory { private readonly ILogger _log; private readonly YubiKeyDevice _device; @@ -31,6 +37,14 @@ public class ConnectionFactory private readonly IHidDevice? _hidKeyboardDevice; private readonly IHidDevice? _hidFidoDevice; + /// + /// Initializes a new instance of the ConnectionFactory class. + /// + /// Logger instance for recording events and diagnostics. + /// The YubiKey device to create connections for. + /// The SmartCard interface of the YubiKey, if available. + /// The HID keyboard interface of the YubiKey, if available. + /// The HID FIDO interface of the YubiKey, if available. public ConnectionFactory( ILogger log, YubiKeyDevice device, @@ -46,13 +60,13 @@ public ConnectionFactory( } [Obsolete("Obsolete")] - internal IYubiKeyConnection CreateScpConnection(YubiKeyApplication application, StaticKeys scp03Keys) + internal IScp03YubiKeyConnection CreateScpConnection(YubiKeyApplication application, Scp03.StaticKeys scp03Keys) { if (_smartCardDevice is null) { - _log.LogError("No smart card interface present. Unable to establish SCP connection to YubiKey."); - throw new InvalidOperationException("TODO"); + string errorMessage = "No smart card interface present. Unable to establish SCP connection to YubiKey."; + throw new InvalidOperationException(errorMessage); } _log.LogInformation("Connecting via the SmartCard interface using SCP03."); @@ -60,15 +74,26 @@ internal IYubiKeyConnection CreateScpConnection(YubiKeyApplication application, return new Scp03Connection(_smartCardDevice, application, scp03Keys); } - - public IYubiKeyConnection CreateScpConnection(YubiKeyApplication application, ScpKeyParameters keyParameters) + + /// + /// Creates a secure channel protocol (SCP) connection to a specific YubiKey application. + /// + /// The YubiKey application to connect to. + /// The security parameters for establishing the SCP connection. + /// A secure connection to the specified YubiKey application. + /// Thrown when the SmartCard interface is not available on the YubiKey. + /// + /// This method establishes a secure channel to the YubiKey using the SmartCard interface. The connection + /// is protected using the Secure Channel Protocol (SCP) with the provided key parameters. + /// + public IScpYubiKeyConnection CreateScpConnection(YubiKeyApplication application, ScpKeyParameters keyParameters) { - LogConnectionAttempt(application, keyParameters); // No need + LogConnectionAttempt(application, keyParameters); if (_smartCardDevice is null) { - _log.LogError("No smart card interface present. Unable to establish SCP connection to YubiKey."); - throw new InvalidOperationException("TODO"); + string errorMessage = "No smart card interface present. Unable to establish SCP connection to YubiKey."; + throw new InvalidOperationException(errorMessage); } _log.LogInformation("Connecting via the SmartCard interface using SCP03."); @@ -77,7 +102,17 @@ public IYubiKeyConnection CreateScpConnection(YubiKeyApplication application, Sc return new ScpConnection(_smartCardDevice, application, keyParameters); } - public IYubiKeyConnection CreateNonScpConnection(YubiKeyApplication application) + /// + /// Creates a standard (non-SCP) connection to a specific YubiKey application. + /// + /// The YubiKey application to connect to. + /// A connection to the specified YubiKey application. + /// Thrown when no suitable interface is available for the requested application. + /// + /// This method creates a connection using the most appropriate interface available for the specified application. + /// It first attempts to use the SmartCard interface, then falls back to HID interfaces if necessary. + /// + public IYubiKeyConnection CreateConnection(YubiKeyApplication application) { if (_smartCardDevice != null) { @@ -87,7 +122,7 @@ public IYubiKeyConnection CreateNonScpConnection(YubiKeyApplication application) return new SmartCardConnection(_smartCardDevice, application); } - if (application == YubiKeyApplication.Otp && _hidKeyboardDevice != null) + if (_hidKeyboardDevice != null && application == YubiKeyApplication.Otp) { _log.LogInformation("Connecting via the Keyboard interface."); @@ -95,16 +130,16 @@ public IYubiKeyConnection CreateNonScpConnection(YubiKeyApplication application) return new KeyboardConnection(_hidKeyboardDevice); } - if ((application == YubiKeyApplication.Fido2 || application == YubiKeyApplication.FidoU2f) && _hidFidoDevice != null) + if (_hidFidoDevice != null && (application == YubiKeyApplication.Fido2 || application == YubiKeyApplication.FidoU2f)) { _log.LogInformation("Connecting via the FIDO interface."); WaitForReclaimTimeout(Transport.HidFido); return new FidoConnection(_hidFidoDevice); } - - _log.LogError("No suitable interface present. Unable to establish connection to YubiKey."); - throw new InvalidOperationException("TODO"); + + string errorMessage = "No suitable interface present. Unable to establish connection to YubiKey."; + throw new InvalidOperationException(errorMessage); } // This function handles waiting for the reclaim timeout on the YubiKey to elapse. The reclaim timeout requires @@ -182,15 +217,17 @@ private void LogConnectionAttempt( ScpKeyParameters keyParameters) { string applicationName = GetApplicationName(application); - string scpInfo = keyParameters is Scp03KeyParameters - ? "SCP03" - : "SCP11"; //TODO make better + string scpInfo = keyParameters switch + { + Scp03KeyParameters scp03KeyParameters => $"SCP03 ({scp03KeyParameters.KeyReference})", + Scp11KeyParameters scp11KeyParameters => $"SCP11 ({scp11KeyParameters.KeyReference})", + _ => "Unknown" + }; _log.LogInformation("YubiKey connecting to {Application} application over {ScpInfo}", applicationName, scpInfo); } - + private static string GetApplicationName(YubiKeyApplication application) => Enum.GetName(typeof(YubiKeyApplication), application) ?? "Unknown"; - } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs index 5e3f214f5..d22905fd2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs @@ -83,7 +83,7 @@ public static byte[] BlockCipher(ReadOnlySpan encryptionKey, ReadOnlySpan< /// 16-byte initialization vector (IV) /// Input blocks; must be a non-zero multiple of 16 bytes long /// Ciphertext of the same length as the plaintext - public static Memory AesCbcEncrypt(ReadOnlySpan encryptionKey, ReadOnlySpan iv, ReadOnlySpan plaintext) + public static ReadOnlyMemory AesCbcEncrypt(ReadOnlySpan encryptionKey, ReadOnlySpan iv, ReadOnlySpan plaintext) { if (encryptionKey.IsEmpty) { @@ -151,7 +151,7 @@ public static Memory AesCbcEncrypt(ReadOnlySpan encryptionKey, ReadO /// 16-byte initialization vector (IV) /// Input blocks; must be a non-zero multiple of 16 bytes long /// Plaintext of the same length as the ciphertext - public static Memory AesCbcDecrypt(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan ciphertext) + public static ReadOnlyMemory AesCbcDecrypt(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan ciphertext) { if (key == null) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs index 4ce2ef6d1..d100fd218 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Security.Cryptography; namespace Yubico.YubiKey.Cryptography @@ -25,6 +26,11 @@ public abstract class ECKeyParameters protected ECKeyParameters(ECParameters parameters) { + if (parameters.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) + { + throw new NotSupportedException("Key must be of type NIST P-256"); + } + Parameters = parameters.DeepCopy(); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs index 11f139316..6525f2c35 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECParametersExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Yubico AB +// 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. @@ -22,10 +22,15 @@ namespace Yubico.YubiKey.Cryptography /// /// Helper extensions for parameter copying /// - internal static class ECParametersExtensions + public static class ECParametersExtensions { public static ECParameters DeepCopy(this ECParameters parameters) { + if (parameters.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) + { + throw new NotSupportedException("Key must be of type NIST P-256"); + } + var copy = new ECParameters { Curve = parameters.Curve, @@ -43,28 +48,40 @@ public static ECParameters DeepCopy(this ECParameters parameters) return copy; } - + /// /// Creates an instance from a byte array. /// /// /// The byte array is expected to be in the format 0x04 || X || Y - /// where X and Y are the uncompressed coordinates of the point. + /// where X and Y are the uncompressed (32 bit) coordinates of the point. /// /// The byte array. - /// An instance of EccPrivateKeyParameters with the nistP256 curve. - public static ECPublicKeyParameters CreateEcPublicKeyFromBytes(this ReadOnlySpan bytes) + /// An instance of EcPrivateKeyParameters with the nistP256 curve. + /// Thrown when the byte array is not in the expected format. + /// Either the first byte is not 0x04, or the byte array is not 65 bytes long (Key must be of type NIST P-256). + public static ECPublicKeyParameters CreateECPublicKeyFromBytes(this ReadOnlySpan bytes) { + if (bytes[0] != 0x04) + { + throw new ArgumentException("The byte array must start with 0x04", nameof(bytes)); + } + + if (bytes.Length != 65) + { + throw new ArgumentException("The byte array must be 65 bytes long (Key must be of type NIST P-256)", nameof(bytes)); + } + var ecParameters = new ECParameters { Curve = ECCurve.NamedCurves.nistP256, Q = new ECPoint { - X = bytes.Slice(1, 32).ToArray(), + X = bytes.Slice(1, 32).ToArray(), // Starts at 1 because the first byte is 0x04, indicating that it is an uncompressed point Y = bytes.Slice(33, 32).ToArray() } }; - + return new ECPublicKeyParameters(ecParameters); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs index 0a782de20..ea588ec9e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPrivateKeyParameters.cs @@ -18,18 +18,46 @@ namespace Yubico.YubiKey.Cryptography { /// - /// EC private key parameters + /// Represents the parameters for an Elliptic Curve (EC) private key. /// + /// + /// This class encapsulates the parameters specific to EC private keys, + /// ensuring that the key is of type NIST P-256 and contains the necessary + /// private key data. It extends the base class + /// with additional validation for private key components. + /// + public sealed class ECPrivateKeyParameters : ECKeyParameters { + + /// + /// Initializes a new instance of the class. It is a wrapper for the class. + /// + /// + /// This constructor is used to create an instance from a object. It will deep copy + /// the parameters from the ECParameters object and ensure that the key is of type NIST P-256. + /// + /// The EC parameters. public ECPrivateKeyParameters(ECParameters parameters) : base(parameters) { + + if (parameters.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) + { + throw new NotSupportedException("Key must be of type NIST P-256"); + } + if (parameters.D == null) { throw new ArgumentException("Parameters must contain private key data (D value)", nameof(parameters)); } } - + /// + /// Initializes a new instance of the class using a object. + /// + /// + /// It exports the parameters from the ECDsa object and deep copy the parameters from the ECParameters object and ensure that the key is of type NIST P-256. + /// + /// The ECDsa object. public ECPrivateKeyParameters(ECDsa ecdsaObject) : base(ecdsaObject.ExportParameters(true)) { } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs index b320e0a69..6eecf4dc8 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs @@ -19,10 +19,28 @@ namespace Yubico.YubiKey.Cryptography { /// - /// EC public key parameters + /// Represents the parameters for an Elliptic Curve (EC) public key. /// + /// + /// This class encapsulates the parameters specific to EC public keys, + /// ensuring that the key is of type NIST P-256 and contains only the + /// necessary public key components. It extends the base + /// class with additional validation to prevent the inclusion of private key data. + /// + /// This class currently only supports NIST P-256. + /// + /// public sealed class ECPublicKeyParameters : ECKeyParameters { + /// + /// Initializes a new instance of the class. It is a wrapper for the class. + /// + /// + /// This constructor is used to create an instance from a object. It will deep copy + /// the parameters from the ECParameters object and ensure that the key is of type NIST P-256. + /// + /// + /// Thrown when the parameters contain private key data (D value). public ECPublicKeyParameters(ECParameters parameters) : base(parameters) { if (parameters.D != null) @@ -32,17 +50,23 @@ public ECPublicKeyParameters(ECParameters parameters) : base(parameters) } } + /// + /// Initializes a new instance of the class. + /// + /// public ECPublicKeyParameters(ECDsa ecdsa) : base(ecdsa.ExportParameters(false)) { } + /// + /// Gets the bytes representing the public key. + /// + /// A containing the public key bytes with the format 0x04 || X || Y. public Memory GetBytes() { - byte[] formatIdentifier = { 0x4 }; // Uncompressed point - var publicKeyRawData = - formatIdentifier - .Concat(Parameters.Q.X) - .Concat(Parameters.Q.Y) - .ToArray() - .AsMemory(); + byte[] publicKeyRawData = + new byte[] { 0x4 } // Format identifier (uncompressed point): 0x04 + .Concat(Parameters.Q.X) + .Concat(Parameters.Q.Y) + .ToArray(); return publicKeyRawData; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs index 8daef5eab..9324df6fc 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs @@ -16,9 +16,9 @@ using System.Diagnostics.CodeAnalysis; using Yubico.Core.Devices; using Yubico.YubiKey.Scp; -using Yubico.YubiKey.Scp03; using MgmtCmd = Yubico.YubiKey.Management.Commands; + namespace Yubico.YubiKey { /// @@ -57,7 +57,7 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// interface. /// IYubiKeyConnection Connect(YubiKeyApplication application); - + /// /// Initiate a connection to the specified application represented as an /// applicationId on a YubiKey device. @@ -70,73 +70,58 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// An instance of a class that implements the /// interface. /// - [Obsolete("Use corresponding YubiKeyApplication method")] IYubiKeyConnection Connect(byte[] applicationId); /// - /// Initiate a connection to the specified application on a YubiKey - /// device. The connection will be made over SCP03 (assuming the keys are - /// the ones loaded onto the YubiKey). + /// Initiate a connection to the specified application on a YubiKey device using SCP protocol. /// + /// + /// The to reference on the device. + /// + /// + /// The SCP key parameters to use in making an SCP connection. + /// /// - /// Note that SCP03 works only with SmartCard applications, namely PIV, OATH, + /// Note that SCP works only with SmartCard applications, namely PIV, OATH, OTP, Security Domain and YubiHsmAuth /// and OpenPgp. However, SCP03 is supported only on series 5 YubiKeys with - /// firmware version 5.3 and later, and only the PIV application. If you - /// specify any other application, the connection will likely fail. + /// firmware version on 5.3 and above. SCP 11 is supported only firmware version 5.7.2 and above. /// /// Note also that the return is an instance of a class that implements - /// which is a "subclass" of + /// which is a "subclass" of /// . /// /// - /// - /// The application to reference on the device. - /// - /// - /// The SCP03 key set to use in making an SCP03 connection. - /// /// - /// An instance of a class that implements the - /// interface. + /// An instance of a class that implements the interface. /// - [Obsolete("Use new Scp")] - IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication application, StaticKeys scp03Keys); + IScpYubiKeyConnection Connect(YubiKeyApplication application, ScpKeyParameters keyParameters); /// - /// Initiate a connection to the specified application represented as an - /// applicationId on a YubiKey device. The connection will be made - /// over SCP03 (assuming the keys are the ones loaded onto the YubiKey). + /// Initiate a connection to the specified application on a YubiKey device using SCP protocol. /// + /// A byte array representing the smart card Application ID (AID) for the + /// application to open. + /// + /// + /// The SCP key parameters to use in making an SCP connection. + /// /// - /// Note that SCP03 works only with SmartCard applications, namely PIV, OATH, + /// Note that SCP works only with SmartCard applications, namely PIV, OATH, OTP, Security Domain and YubiHsmAuth /// and OpenPgp. However, SCP03 is supported only on series 5 YubiKeys with - /// firmware version 5.3 and later, and only the PIV application. If you - /// specify any other application, the connection will likely fail. + /// firmware version on 5.3 and above. SCP 11 is supported only firmware version 5.7.2 and above. /// /// Note also that the return is an instance of a class that implements - /// which is a "subclass" of + /// which is a "subclass" of /// . /// /// - /// - /// A byte array representing the smart card Application ID (AID) for the - /// application to open. - /// - /// - /// The SCP03 key set to use in making an SCP03 connection. - /// /// - /// An instance of a class that implements the - /// interface. + /// An instance of a class that implements the interface. /// - [Obsolete("Use new Scp")] - IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys); - // TODO DOCU - IScpYubiKeyConnection ConnectScp(YubiKeyApplication application, ScpKeyParameters keyParameters); - // TODO DOCU - IScpYubiKeyConnection ConnectScp(byte[] applicationId, ScpKeyParameters keyParameters); + IScpYubiKeyConnection Connect(byte[] applicationId, ScpKeyParameters keyParameters); + - /// + /// /// Attempt to connect to the YubiKey device. /// /// The application to reference on the device. @@ -144,9 +129,8 @@ public interface IYubiKeyDevice : IYubiKeyDeviceInfo, IEquatable /// Boolean indicating whether the call was successful. bool TryConnect( YubiKeyApplication application, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection); - + [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection); + /// /// Attempt to connect to the YubiKey device. /// @@ -154,81 +138,57 @@ bool TryConnect( /// application to open. /// Out parameter containing the instance. /// Boolean indicating whether the call was successful. - [Obsolete("Obsolute")] bool TryConnect( byte[] applicationId, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection); + [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection); + /// - /// Attempt to connect to the YubiKey device. The connection will be made - /// over SCP03 (assuming the keys are the ones loaded onto the YubiKey). + /// Attempt to connect to the YubiKey device over SCP using the specified /// /// - /// Note that SCP03 works only with SmartCard applications, namely PIV, OATH, + /// Note that SCP works only with SmartCard applications, namely PIV, OATH, OTP, Security Domain and YubiHsmAuth /// and OpenPgp. However, SCP03 is supported only on series 5 YubiKeys with - /// firmware version 5.3 and later, and only the PIV application. If you - /// specify any other application, the connection will likely fail. + /// firmware version on 5.3 and above. SCP 11 is supported only firmware version 5.7.2 and above. /// /// Note also that the return is an instance of a class that implements - /// which is a "subclass" of + /// which is a "subclass" of /// . /// /// /// The application to reference on the device. - /// - /// The SCP03 key set to use in making an SCP03 connection. + /// + /// The key set to use in making an SCP connection. /// /// Out parameter containing the instance. /// Boolean indicating whether the call was successful. - [Obsolete("Use new Scp")] - bool TryConnectScp03( + bool TryConnect( YubiKeyApplication application, - StaticKeys scp03Keys, - [MaybeNullWhen(returnValue: false)] - out IScp03YubiKeyConnection connection); + ScpKeyParameters keyParameters, + [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection); /// - /// Attempt to connect to the YubiKey device. The connection will be made - /// over SCP03 (assuming the keys are the ones loaded onto the YubiKey). + /// Attempt to connect to the YubiKey device over SCP using the specified /// /// - /// Note that SCP03 works only with SmartCard applications, namely PIV, OATH, + /// Note that SCP works only with SmartCard applications, namely PIV, OATH, OTP, Security Domain and YubiHsmAuth /// and OpenPgp. However, SCP03 is supported only on series 5 YubiKeys with - /// firmware version 5.3 and later, and only the PIV application. If you - /// specify any other application, the connection will likely fail. + /// firmware version on 5.3 and above. SCP 11 is supported only firmware version 5.7.2 and above. /// /// Note also that the return is an instance of a class that implements - /// which is a "subclass" of + /// which is a "subclass" of /// . /// /// - /// A byte pattern representing the application to reference. - /// - /// The SCP03 key set to use in making an SCP03 connection. - /// - /// Out parameter containing the instance. - /// Boolean indicating whether the call was successful. - [Obsolete("Use new Scp")] - bool TryConnectScp03( - byte[] applicationId, - StaticKeys scp03Keys, - [MaybeNullWhen(returnValue: false)] - out IScp03YubiKeyConnection connection); - - // TODO DOcumentation - bool TryConnectScp( - YubiKeyApplication application, - ScpKeyParameters keyParameters, - [MaybeNullWhen(returnValue: false)] - out IScpYubiKeyConnection connection); - - bool TryConnectScp( + /// The Iso7816 application ID to use for the connection. + /// The parameters for the SCP connection. + /// The connection to the YubiKey, or null if unable to connect. + /// True if the connection was successful, false otherwise. + bool TryConnect( byte[] applicationId, ScpKeyParameters keyParameters, - [MaybeNullWhen(returnValue: false)] - out IScpYubiKeyConnection connection); - + [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection); + /// /// Checks whether a IYubiKeyDevice instance contains a particular platform . /// @@ -687,7 +647,6 @@ void SetLegacyDeviceConfiguration( bool touchEjectEnabled, int autoEjectTimeout = 0); - /// /// Sets the on the /// @@ -730,5 +689,22 @@ void SetLegacyDeviceConfiguration( /// /// void DeviceReset(); + + [Obsolete("Use the new Scp methods")] + bool TryConnectScp03( + YubiKeyApplication application, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, + [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection); + + [Obsolete("Use the new Scp methods")] + bool TryConnectScp03( + byte[] applicationId, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, + [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection); + + [Obsolete("Use the new Scp methods")] + IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication application, Yubico.YubiKey.Scp03.StaticKeys scp03Keys); + [Obsolete("Use the new Scp methods")] + IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, Yubico.YubiKey.Scp03.StaticKeys scp03Keys); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs index 4fa49d979..f4f28c622 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs @@ -14,6 +14,8 @@ using System; using System.Globalization; +using Yubico.Core.Logging; +using Yubico.YubiKey.Oath; using Yubico.YubiKey.Otp.Commands; using Yubico.YubiKey.Otp.Operations; using Yubico.YubiKey.Scp; @@ -61,21 +63,24 @@ namespace Yubico.YubiKey.Otp /// and finally, Execute() tells the operation class to perform the /// configuration on the YubiKey. /// - public sealed class OtpSession : IOtpSession + public sealed class OtpSession : ApplicationSession, IOtpSession { /// /// Constructs a instance for high-level OTP operations. /// - /// Instance of class implementing . - /// TODO - public OtpSession(IYubiKeyDevice yubiKey, Scp03KeyParameters? keyParameters = null) + /// + /// This constructor should be used to obtain an instance of this class for + /// performing operations on the YubiKey OTP application. The instance of + /// passed in should be a connected YubiKey. + /// + /// An instance of a class that implements . + /// An instance of containing the + /// parameters for the SCP03 key. If , the default parameters will be used. + public OtpSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Otp, keyParameters) { - if (yubiKey is null) { throw new ArgumentNullException(nameof(yubiKey)); } - YubiKey = yubiKey; - _connection = keyParameters is null - ? yubiKey.Connect(YubiKeyApplication.Oath) - : yubiKey.ConnectScp(YubiKeyApplication.Oath, keyParameters); - _otpStatus = _connection.SendCommand(new ReadStatusCommand()).GetData(); // fails on read data Incorrect parameters in the command data field. 0x6A80 + // Getting the OTP status allows the user to read the OTP status on the OtpSession object. + _otpStatus = Connection.SendCommand(new ReadStatusCommand()).GetData(); // } #region OTP Operation Object Factory @@ -85,7 +90,7 @@ public OtpSession(IYubiKeyDevice yubiKey, Scp03KeyParameters? keyParameters = nu /// The identifier for the OTP application slot to configure. /// An instance of . public CalculateChallengeResponse CalculateChallengeResponse(Slot slot) => - new CalculateChallengeResponse(_connection, this, slot); + new CalculateChallengeResponse(Connection, this, slot); /// /// Configures one of the OTP application slots to act as a Yubico OTP device. @@ -93,7 +98,7 @@ public CalculateChallengeResponse CalculateChallengeResponse(Slot slot) => /// The identifier for the OTP application slot to configure. /// Instance of . public ConfigureYubicoOtp ConfigureYubicoOtp(Slot slot) => - new ConfigureYubicoOtp(_connection, this, slot); + new ConfigureYubicoOtp(Connection, this, slot); /// /// Removes a slot configuration in the YubiKey's OTP application. @@ -101,7 +106,7 @@ public ConfigureYubicoOtp ConfigureYubicoOtp(Slot slot) => /// The identifier for the OTP application slot configuration to delete. /// An instance of . public DeleteSlotConfiguration DeleteSlotConfiguration(Slot slot) => - new DeleteSlotConfiguration(_connection, this, slot); + new DeleteSlotConfiguration(Connection, this, slot); /// /// Configures one of the OTP application slots to respond to challenges. @@ -109,17 +114,17 @@ public DeleteSlotConfiguration DeleteSlotConfiguration(Slot slot) => /// The identifier for the OTP application slot to configure. /// Instance of . public ConfigureChallengeResponse ConfigureChallengeResponse(Slot slot) => - new ConfigureChallengeResponse(_connection, this, slot); + new ConfigureChallengeResponse(Connection, this, slot); /// /// OTP Slot to configure. public ConfigureHotp ConfigureHotp(Slot slot) => - new ConfigureHotp(_connection, this, slot); + new ConfigureHotp(Connection, this, slot); /// /// OTP Slot to configure. public ConfigureNdef ConfigureNdef(Slot slot) => - new ConfigureNdef(_connection, this, slot); + new ConfigureNdef(Connection, this, slot); /// /// Sets a static password for an OTP application slot on a YubiKey. @@ -135,7 +140,7 @@ public ConfigureNdef ConfigureNdef(Slot slot) => /// The identifier for the OTP application slot to configure. /// Instance of . public ConfigureStaticPassword ConfigureStaticPassword(Slot slot) => - new ConfigureStaticPassword(_connection, this, slot); + new ConfigureStaticPassword(Connection, this, slot); /// /// Updates the settings of an OTP application slot on a YubiKey without removing @@ -144,7 +149,7 @@ public ConfigureStaticPassword ConfigureStaticPassword(Slot slot) => /// The identifier for the OTP application slot to configure. /// public UpdateSlot UpdateSlot(Slot slot) => - new UpdateSlot(_connection, this, slot); + new UpdateSlot(Connection, this, slot); #endregion #region Non-Builder Implementations @@ -184,7 +189,7 @@ public void SwapSlots() throw new InvalidOperationException(ExceptionMessages.OtpSlotsNotConfigured); } - var swapResponse = _connection.SendCommand(new SwapSlotsCommand()); + var swapResponse = Connection.SendCommand(new SwapSlotsCommand()); if (swapResponse.Status != ResponseStatus.Success) { throw new InvalidOperationException(swapResponse.StatusMessage); @@ -243,12 +248,12 @@ public NdefDataReader ReadNdefTag() // NDEF is actually a separate application that we need to connect to. Disconnect from OTP, run the NDEF // command, and then reconnect to OTP. - _connection.Dispose(); + Connection.Dispose(); ReadNdefDataResponse response; - using (_connection = YubiKey.Connect(YubiKeyApplication.OtpNdef)) + using (var tempConnection = YubiKey.Connect(YubiKeyApplication.OtpNdef)) // TODO Why cant we do this instead of swap connection, open another? { - var selectResponse = _connection.SendCommand(new SelectNdefDataCommand() { FileID = NdefFileId.Ndef }); + var selectResponse = tempConnection.SendCommand(new SelectNdefDataCommand() { FileID = NdefFileId.Ndef }); if (selectResponse.Status != ResponseStatus.Success) { throw new InvalidOperationException( @@ -258,10 +263,10 @@ public NdefDataReader ReadNdefTag() selectResponse.StatusMessage)); } - response = _connection.SendCommand(new ReadNdefDataCommand()); + response = tempConnection.SendCommand(new ReadNdefDataCommand()); } - _connection = YubiKey.Connect(YubiKeyApplication.Otp); + // Connection = YubiKey.Connect(YubiKeyApplication.Otp); return new NdefDataReader(response.GetData().Span); } @@ -283,18 +288,18 @@ public NdefDataReader ReadNdefTag() internal FirmwareVersion FirmwareVersion => _otpStatus.FirmwareVersion; - internal IYubiKeyDevice YubiKey { get; private set; } + // internal IYubiKeyDevice YubiKey { get; private set; } FirmwareVersion IOtpSession.FirmwareVersion => FirmwareVersion; IYubiKeyDevice IOtpSession.YubiKey => YubiKey; #endregion - /// - public void Dispose() => _connection.Dispose(); + // /// + // public void Dispose() => Connection.Dispose(); #region Private Fields - private IYubiKeyConnection _connection; + // private IYubiKeyConnection Connection; private readonly OtpStatus _otpStatus; #endregion } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs index 1b3c497c6..f078f8d6a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -13,6 +13,8 @@ // limitations under the License. using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Yubico.Core.Iso7816; using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.Scp; @@ -20,33 +22,36 @@ namespace Yubico.YubiKey.Pipelines { /// + /// Constructs the shared state for SCP communication over SCP03, SCP11a/b/c. /// Performs SCP encrypt-then-MAC on commands and verify-then-decrypt on responses. /// /// /// Does an SCP Initialize Update / External Authenticate handshake at setup. - /// /// Commands and responses sent through this pipeline are confidential and authenticated. - /// - /// Requires pre-shared . TODO /// - // broken into two transforms internal class ScpApduTransform : IApduTransform, IDisposable { public ScpKeyParameters KeyParameters { get; } - public DataEncryptor? DataEncryptor; // When is this ever null? + + public EncryptDataFunc EncryptDataFunc => + _dataEncryptor ?? ThrowIfUninitialized(); + + private EncryptDataFunc? _dataEncryptor; private ScpState ScpState => - _scpState ?? throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); - + _scpState ?? ThrowIfUninitialized(); + private readonly IApduTransform _pipeline; private ScpState? _scpState; private bool _disposed; + [DoesNotReturn] + private T ThrowIfUninitialized() => throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); /// /// Constructs a new pipeline from the given one. /// /// Underlying pipeline to send and receive encoded APDUs with - /// //todo + /// The for the SCP connection public ScpApduTransform(IApduTransform pipeline, ScpKeyParameters keyParameters) { _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); @@ -62,21 +67,59 @@ public void Setup() if (KeyParameters.GetType() == typeof(Scp03KeyParameters)) { - DataEncryptor = InitializeScp03((Scp03KeyParameters)KeyParameters); + _dataEncryptor = InitializeScp03((Scp03KeyParameters)KeyParameters); } else if (KeyParameters.GetType() == typeof(Scp11KeyParameters)) { - DataEncryptor = InitializeScp11((Scp11KeyParameters)KeyParameters); + _dataEncryptor = InitializeScp11((Scp11KeyParameters)KeyParameters); } } - private DataEncryptor InitializeScp11(Scp11KeyParameters keyParameters) + /// + /// Passes the supplied command into the pipeline, and returns the final response. + /// + /// + /// Encodes the command using the SCP state, sends it to the underlying pipeline, and then decodes the response. + /// + /// Note: Some commands should not be encoded. For those, the pipeline is invoked directly. + /// + /// + public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) + { + if (ShouldNotEncode(commandType)) + { + return _pipeline.Invoke(command, commandType, responseType); + } + + var encodedCommand = ScpState.EncodeCommand(command); + var response = _pipeline.Invoke(encodedCommand, commandType, responseType); + + + return ScpState.DecodeResponse(response); + } + + private static bool ShouldNotEncode(Type commandType) + { + // This method introduced high coupling between the SCP pipeline and the applications. + // The applications should not have to know about the SCP pipeline, or they should be able to + // send the commands without the pipeline. + var exceptionList = new[] + { + typeof(InterIndustry.Commands.SelectApplicationCommand), + typeof(Oath.Commands.SelectOathCommand), + typeof(Scp.Commands.ResetCommand), + }; + + return exceptionList.Contains(commandType); + } + + private EncryptDataFunc InitializeScp11(Scp11KeyParameters keyParameters) { _scpState = Scp11State.CreateScpState(_pipeline, keyParameters); return _scpState.GetDataEncryptor(); } - private DataEncryptor InitializeScp03(Scp03KeyParameters keyParams) + private EncryptDataFunc InitializeScp03(Scp03KeyParameters keyParams) { // Generate host challenge using var rng = CryptographyProviders.RngCreator(); @@ -87,25 +130,6 @@ private DataEncryptor InitializeScp03(Scp03KeyParameters keyParams) return _scpState.GetDataEncryptor(); } - - - public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) - { - // Encode command - var encodedCommand = ScpState.EncodeCommand(command); - - // Pass along the encoded command - var response = _pipeline.Invoke(encodedCommand, commandType, responseType); - - // Special carve out for SelectApplication here, since there will be nothing to decode - if (commandType == typeof(InterIndustry.Commands.SelectApplicationCommand)) - { - return response; - } - - // Decode response and return it - return ScpState.DecodeResponse(response); - } // There is a call to cleanup and a call to Dispose. The cleanup only // needs to call the cleanup on the local APDU Pipeline object. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/AuthenticateDecryptCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/AuthenticateDecryptCommand.cs index 7afe62fd4..c26734b38 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/AuthenticateDecryptCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/AuthenticateDecryptCommand.cs @@ -57,7 +57,8 @@ namespace Yubico.YubiKey.Piv.Commands /// /// /// The caller supplies the data to decrypt. It must be a block the same size - /// as the key. For an RSA-1024/RSA-2048/RSA-3072/RSA-4096 key, the block must be 128/256/384/512 bytes. If the actual data to decrypt + /// as the key. For an RSA-1024 key, the block must be 128 bytes, for an + /// RSA-2048 key, the block must be 256 bytes, for an RSA-3072 key, the block must be 384 bytes, and for an RSA-4096 key, the block must be 512 bytes. If the actual data to decrypt /// is shorter, it must be provided with as many prepended 00 bytes as needed /// to make sure the block is the appropriate length. /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs index 2571875eb..ccbf41762 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs @@ -16,11 +16,25 @@ using System.Buffers.Binary; using Yubico.YubiKey.Cryptography; -namespace Yubico.YubiKey.Scp +namespace Yubico.YubiKey.Scp.Helpers { internal static class ChannelEncryption { - public static Memory EncryptData(ReadOnlySpan dataToEncrypt, ReadOnlySpan encryptionKey, int encryptionCounter) + /// + /// Encrypts the provided data using AES CBC mode with the given key and encryption counter. + /// + /// The data to be encrypted. + /// The AES key to use for encryption. + /// + /// A counter used to generate the initialization vector (IV) for encryption. + /// + /// + /// A containing the encrypted data. + /// + public static ReadOnlyMemory EncryptData( + ReadOnlySpan dataToEncrypt, + ReadOnlySpan encryptionKey, + int encryptionCounter) { // NB: Could skip this if the payload is empty (rather than sending a 16-byte encrypted '0x800000...' payload byte[] countBytes = new byte[sizeof(int)]; @@ -28,7 +42,7 @@ public static Memory EncryptData(ReadOnlySpan dataToEncrypt, ReadOnl byte[] ivInput = new byte[16]; countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block - byte[] iv = AesUtilities.BlockCipher(encryptionKey.ToArray(), ivInput); //todo toarry + byte[] iv = AesUtilities.BlockCipher(encryptionKey, ivInput); var paddedPayload = Padding.PadToBlockSize(dataToEncrypt); var encryptedData = AesUtilities.AesCbcEncrypt(encryptionKey, iv, paddedPayload.Span); @@ -36,7 +50,21 @@ public static Memory EncryptData(ReadOnlySpan dataToEncrypt, ReadOnl return encryptedData; } - public static Memory DecryptData(ReadOnlySpan dataToDecrypt, ReadOnlySpan key, int encryptionCounter) + /// + /// Decrypts the provided data using AES CBC mode with the given key and encryption counter. + /// + /// The encrypted data to be decrypted. + /// The AES key to use for decryption. + /// + /// A counter used to generate the initialization vector (IV) for decryption. + /// + /// + /// A containing the decrypted data with padding removed. + /// + public static ReadOnlyMemory DecryptData( + ReadOnlySpan dataToDecrypt, + ReadOnlySpan key, + int encryptionCounter) { byte[] countBytes = new byte[sizeof(int)]; BinaryPrimitives.WriteInt32BigEndian(countBytes, encryptionCounter); @@ -44,8 +72,8 @@ public static Memory DecryptData(ReadOnlySpan dataToDecrypt, ReadOnl byte[] ivInput = new byte[16]; countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block ivInput[0] = 0x80; // to mark as RMAC calculation - byte[] iv = AesUtilities.BlockCipher(key, ivInput); + byte[] iv = AesUtilities.BlockCipher(key, ivInput); var decryptedData = AesUtilities.AesCbcDecrypt(key, iv, dataToDecrypt); return Padding.RemovePadding(decryptedData.Span); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs index 81f494266..82730d69f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs @@ -18,7 +18,7 @@ namespace Yubico.YubiKey.Scp.Commands { /// - /// Represents the second command in the SCP03 authentication handshake, 'EXTERNAL_AUTHENTICATE' TODO Fix better docu + /// Represents the second command in the SCP03 and SCP11a/c authentication handshakes, 'EXTERNAL_AUTHENTICATE' /// internal class ExternalAuthenticateCommand : IYubiKeyCommand { @@ -30,7 +30,7 @@ internal class ExternalAuthenticateCommand : IYubiKeyCommand _data; private readonly byte _keyVersionNumber; private readonly byte _keyId; - + /// /// Constructs an EXTERNAL_AUTHENTICATE command, containing the provided data. /// @@ -43,7 +43,13 @@ public ExternalAuthenticateCommand(ReadOnlyMemory data) _data = data; } - public ExternalAuthenticateCommand(byte keyVersionNumber, byte keyId, byte[] data) + /// + /// Constructs an EXTERNAL_AUTHENTICATE command, containing the provided data. This is used to create an SCP11a/c command. + /// + /// + /// + /// + public ExternalAuthenticateCommand(byte keyVersionNumber, byte keyId, ReadOnlyMemory data) { _keyVersionNumber = keyVersionNumber; _keyId = keyId; @@ -58,7 +64,7 @@ public ExternalAuthenticateCommand(byte keyVersionNumber, byte keyId, byte[] dat P2 = _keyId > 0 ? _keyId : default, Data = _data }; - + public ExternalAuthenticateResponse CreateResponseForApdu(ResponseApdu responseApdu) => new ExternalAuthenticateResponse(responseApdu); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs index 6255984c5..8516092ea 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Globalization; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp.Commands @@ -34,7 +35,11 @@ public ExternalAuthenticateResponse(ResponseApdu responseApdu) : if (responseApdu.SW != SWConstants.Success) { - throw new ArgumentException(ExceptionMessages.IncorrectExternalAuthenticateData, nameof(responseApdu)); + string message = string.Format( + CultureInfo.CurrentCulture, + $"{ExceptionMessages.IncorrectExternalAuthenticateData}" + " " + + $"SW: 0x{responseApdu.SW.ToString("X4", CultureInfo.InvariantCulture)}"); + throw new ArgumentException(message, nameof(responseApdu)); } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs index 20f10d516..5ef4adcd2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs @@ -40,7 +40,7 @@ public InitializeUpdateCommand(int keyVersionNumber, ReadOnlyMemory hostCh { if (hostChallenge.Length != 8) { - throw new ArgumentException("Invalid size, must be 8 bytes", nameof(_hostChallenge)); //TODO make localised string + throw new ArgumentException("Invalid size, must be 8 bytes", nameof(_hostChallenge)); } _hostChallenge = hostChallenge; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs index ef72bce88..d6ac6c9f8 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp.Commands @@ -41,7 +42,12 @@ public InitializeUpdateResponse(ResponseApdu responseApdu) : if (responseApdu.Data.Length != 29) { - throw new ArgumentException(ExceptionMessages.IncorrectInitializeUpdateResponseData, nameof(responseApdu)); + // This can indicate that the authentication was not successful due to incorrect authentication data, such as keys the keys being used + string message = string.Format( + CultureInfo.CurrentCulture, + $"{ExceptionMessages.IncorrectInitializeUpdateResponseData}" + " " + + $"SW: 0x{responseApdu.SW.ToString("X4", CultureInfo.InvariantCulture)}"); + throw new ArgumentException(message, nameof(responseApdu)); } var responseData = responseApdu.Data.Span; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs index fabfb0c61..6aa555e25 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp.Commands { + /// - /// TODO + /// This command is used to reset the SCP keys on the YubiKey device back to its factory default state + /// In order to reset the YubiKey to its factory default state, one must issue the reset command to the Yubikey + /// with incorrect parameters 65 times. This will block the keys and reset the YubiKey to its factory default state. /// internal class ResetCommand : IYubiKeyCommand { @@ -29,15 +31,20 @@ internal class ResetCommand : IYubiKeyCommand private readonly byte _ins; /// - /// TODO + /// Initialize a new instance of the + /// Clients should not generally build this manually. Instead, use the + /// to build commands. /// - /// - /// Clients should not generally build this manually. - /// - /// - /// Which key set to use. - /// - /// + /// The instruction byte for the command + /// the instruction bytes that are valid are, + /// , + /// , + /// , + /// + /// + /// The version number of the key + /// The Key id + /// The data to be reset public ResetCommand(byte ins, byte keyVersionNumber, byte kid, byte[] data) { _ins = ins; @@ -53,7 +60,6 @@ public ResetCommand(byte ins, byte keyVersionNumber, byte kid, byte[] data) P1 = _keyVersionNumber, P2 = _kid, Data = _data, - Ne = 0 }; public YubiKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => new YubiKeyResponse(responseApdu); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs index ba3e4e8d7..2fee52d01 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Diagnostics; using Yubico.Core.Iso7816; @@ -40,8 +39,10 @@ public virtual void ThrowIfFailed(string? message = null) Debug.Assert(Status == ResponseStatus.Success); return; default: - throw new SecureChannelException(message ?? StatusMessage); + throw new SecureChannelException(AddStatusWord(message ?? StatusMessage)); } + + string AddStatusWord(string message) => $"{message} (StatusWord: {StatusWord})"; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs index 308d2ec1f..bab94092b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs @@ -12,33 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp.Commands { - internal class SecurityOperationCommand : IYubiKeyCommand //todo visibility of classes? + /// + /// Implements the PERFORM SECURITY OPERATION command for SCP (Secure Channel Protocol) operations. + /// This command is used to perform security-related operations such as certificate verification + /// and key agreement during SCP11a/b/c authentication. + /// + /// + /// The PERFORM SECURITY OPERATION command is part of the SCP protocol suite and is used + /// specifically for operations that involve certificate handling and key establishment. + /// It is typically used in conjunction with other SCP commands like Initialize Update + /// and External/Internal Authenticate to establish a secure channel. + /// + internal class SecurityOperationCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; private readonly byte _oceRefVersionNumber; - private readonly byte _p2; - private readonly byte[] _certificates; + private readonly byte _oceKeyId; + private readonly ReadOnlyMemory _oceCertificates; - public SecurityOperationCommand(byte oceRefVersionNumber, byte p2, byte[] certificates) + /// + /// Initializes a new instance of the class. + /// + /// The Off-Card Entity key version number. + /// The Off-Card Entity key ID. + /// The certificate chain for the off-card entity + /// + /// This command is used as part of the SCP11 protocol suite for presenting the off-card entity's certificate chain to the YubiKey. + /// + public SecurityOperationCommand(byte oceKeyVersionNumer, byte oceKeyId, ReadOnlyMemory oceCertificates) { - _oceRefVersionNumber = oceRefVersionNumber; - _p2 = p2; - _certificates = certificates; + _oceRefVersionNumber = oceKeyVersionNumer; + _oceKeyId = oceKeyId; + _oceCertificates = oceCertificates; } + /// + /// Creates the APDU for the PERFORM SECURITY OPERATION command. + /// + /// + /// A object containing the formatted command APDU. + /// public CommandApdu CreateCommandApdu() => new CommandApdu { Cla = 0x80, Ins = 0x2A, P1 = _oceRefVersionNumber, - P2 = _p2, - Data = _certificates + P2 = _oceKeyId, + Data = _oceCertificates }; public SecurityOperationResponse CreateResponseForApdu(ResponseApdu responseApdu) => diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs index 4b707db19..1938f5fbd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs @@ -17,24 +17,49 @@ namespace Yubico.YubiKey.Scp { + /// + /// Represents a reference to a cryptographic key stored on the YubiKey. + /// public class KeyReference { + /// + /// The Key Id (KID) of the key. + /// public byte Id { get; } + + /// + /// The Key Version Number (KVN) of the key. + /// public byte VersionNumber { get; } + /// + /// Initializes a new instance of the class. + /// + /// The ID of the key. + /// The version number of the key. public KeyReference( byte id, - byte versionNumber - ) + byte versionNumber) { Id = id; VersionNumber = versionNumber; } + /// + /// Returns a span of bytes that represent the key reference. + /// + public ReadOnlyMemory GetBytes => new[] { Id, VersionNumber }; - public ReadOnlySpan GetBytes => new[] { Id, VersionNumber }.AsSpan(); - - public override string ToString() => $"KeyRef[Kid=0x{Id:X2}, Kvn=0x{VersionNumber:X2}"; + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object in the format + /// "KeyRef[Kid=0x{Id:X2}, Kvn=0x{VersionNumber:X2}]" + /// + /// + /// "KeyRef[Kid=0x01, Kvn=0x02]" + public override string ToString() => $"KeyRef[Kid=0x{Id:X2}, Kvn=0x{VersionNumber:X2}]"; public override bool Equals(object? obj) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs index c27adf92a..f6bea53fc 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs @@ -14,9 +14,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Yubico.YubiKey.Cryptography; namespace Yubico.YubiKey.Scp { @@ -25,31 +27,71 @@ namespace Yubico.YubiKey.Scp /// For SCP11b only keyRef and pkSdEcka are required. Note that this does not authenticate the off-card entity. /// For SCP11a and SCP11c the off-card entity CA key reference must be provided, as well as the off-card entity secret key and certificate chain. /// - public class Scp11KeyParameters : ScpKeyParameters + [SuppressMessage("Style", "IDE0032:Use auto property")] + public sealed class Scp11KeyParameters : ScpKeyParameters, IDisposable { - public ECParameters SecurityDomainEllipticCurveKeyAgreementKeyPublicKey { get; } // TODO Add docs - public KeyReference? OffCardEntityKeyReference { get; } - public ECParameters? OffCardEntityEllipticCurveAgreementPrivateKey { get; } - public IReadOnlyList Certificates { get; } + private KeyReference? _oceKeyReference; + private ECPublicKeyParameters _pkSdEcka; + private ECPrivateKeyParameters? _skOceEcka; + private X509Certificate2[]? _certificates; + private bool _disposed; + /// + /// The public key of the Security Domain Elliptic Curve Key Agreement (ECKA) key. + /// pkSdEcka is short for PublicKey SecurityDomain Elliptic Curve KeyAgreement Key + /// + public ECPublicKeyParameters PkSdEcka => _pkSdEcka; + + /// + /// The key reference of the off-card entity. Optional. + /// oceKeyReference is short for Off-Card Entity Key Reference + /// + public KeyReference? OceKeyReference => _oceKeyReference; + + /// + /// The private key of the off-card entity Elliptic Curve Key Agreement (ECKA) key. Optional. + /// skOceEcka is short for Secret Key Off-Card Entity Elliptic Curve KeyAgreement Key + /// + public ECPrivateKeyParameters? SkOceEcka => _skOceEcka; + + /// + /// The certificate chain for the off-card entity. This is used for SCP11a and SCP11c. Optional. //TODO Clarify which ones are for SCP11a and which ones are for SCP11c and which ones are for SCP11b + /// + public IReadOnlyList? Certificates => _certificates; + + /// + /// Creates a new instance. + /// This is used to initiate SCP11A and SCP11C connections. + /// + /// The key reference. + /// The security domain elliptic curve key agreement key public key. + /// The off-card entity key reference. Optional. + /// The off-card entity elliptic curve key agreement key private key. Optional. + /// The off-card entity certificate chain. Optional. public Scp11KeyParameters( KeyReference keyReference, - ECParameters pkSdEcka, + ECPublicKeyParameters pkSdEcka, KeyReference? oceKeyReference = null, - ECParameters? skOceEcka = null, + ECPrivateKeyParameters? skOceEcka = null, IEnumerable? certificates = null) : base(keyReference) - { - - SecurityDomainEllipticCurveKeyAgreementKeyPublicKey = pkSdEcka; - OffCardEntityKeyReference = oceKeyReference; - OffCardEntityEllipticCurveAgreementPrivateKey = skOceEcka; - Certificates = certificates?.ToList() ?? new List(); + { + _pkSdEcka = pkSdEcka; + _oceKeyReference = oceKeyReference; + _skOceEcka = skOceEcka; + _certificates = certificates?.ToArray(); ValidateParameters(); } - public Scp11KeyParameters(KeyReference keyReference, ECParameters pkSdEcka) + /// + /// Initializes a new instance of the class + /// with the specified key reference and security domain elliptic curve key agreement key public key. + /// This is used to create SCP11B connections which are authenticated connections. + /// + /// The key reference. + /// The security domain elliptic curve key agreement key public key. + public Scp11KeyParameters(KeyReference keyReference, ECPublicKeyParameters pkSdEcka) : this(keyReference, pkSdEcka, null, null, null) { @@ -61,9 +103,9 @@ private void ValidateParameters() { case ScpKid.Scp11b: if ( - OffCardEntityKeyReference != null || - OffCardEntityEllipticCurveAgreementPrivateKey != null || - Certificates.Count > 0 + OceKeyReference != null || + SkOceEcka != null || + Certificates?.Count > 0 ) { throw new ArgumentException("Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); @@ -73,9 +115,9 @@ private void ValidateParameters() case ScpKid.Scp11a: case ScpKid.Scp11c: if ( - OffCardEntityKeyReference == null || - OffCardEntityEllipticCurveAgreementPrivateKey == null || - Certificates.Count == 0 + OceKeyReference == null || + SkOceEcka == null || + Certificates?.Count == 0 ) { throw new ArgumentException("Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); @@ -86,5 +128,19 @@ private void ValidateParameters() throw new ArgumentException("KID must be 0x11, 0x13, or 0x15 for SCP11"); } } + + public void Dispose() + { + if (!_disposed) + { + CryptographicOperations.ZeroMemory(_skOceEcka?.Parameters.D); + _pkSdEcka = null!; + _oceKeyReference = null; + _skOceEcka = null; + _certificates = Array.Empty(); + + _disposed = true; + } + } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs index bb82b4597..3afdc5922 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using Yubico.Core.Devices.SmartCard; using Yubico.YubiKey.Pipelines; @@ -19,76 +20,58 @@ namespace Yubico.YubiKey.Scp { internal class ScpConnection : SmartCardConnection, IScpYubiKeyConnection { + public ScpKeyParameters KeyParameters => _scpApduTransform.KeyParameters; + + EncryptDataFunc IScpYubiKeyConnection.EncryptDataFunc => _scpApduTransform.EncryptDataFunc; + private bool _disposed; private readonly ScpApduTransform _scpApduTransform; public ScpConnection( ISmartCardDevice smartCardDevice, - YubiKeyApplication yubiKeyApplication, + YubiKeyApplication application, ScpKeyParameters keyParameters) - : base(smartCardDevice, yubiKeyApplication, null) // TODO Consider this, dont use this constructor + : base(smartCardDevice, application, null) { - // _scpApduTransform = SetObject(yubiKeyApplication, scpKeys); TODO Is this method really needed? - - var previousPipeline = GetPipeline(); - var nextPipeline = new ScpApduTransform(previousPipeline, keyParameters); + var scpPipeline = CreateScpPipeline(keyParameters); + var withErrorHandling = CreateParentPipeline(scpPipeline, application); - // Set parent pipeline - SetPipeline(nextPipeline); - nextPipeline.Setup(); + // Have the base class use the new error augmented pipeline + SetPipeline(withErrorHandling); - _scpApduTransform = nextPipeline; + // Setup the full pipeline + withErrorHandling.Setup(); + _scpApduTransform = scpPipeline; } - - // public ScpConnection( - // ISmartCardDevice smartCardDevice, - // ReadOnlyMemory applicationId, - // ScpKeyParameters scpKeys) - // : base(smartCardDevice, YubiKeyApplication.Unknown, applicationId.ToArray()) //TODO Consider using the Span - // { - // var application = YubiKeyApplication.Unknown; - // if (applicationId.Span.SequenceEqual(YubiKeyApplication.Fido2.GetIso7816ApplicationId())) - // { - // application = YubiKeyApplication.Fido2; - // } - // else if (applicationId.Span.SequenceEqual(YubiKeyApplication.Otp.GetIso7816ApplicationId())) - // { - // application = YubiKeyApplication.Otp; - // } - // - // _scpApduTransform = SetObject(application, scpKeys); - // } - - public ScpKeyParameters KeyParameters => _scpApduTransform.KeyParameters; - DataEncryptor? IScpYubiKeyConnection.DataEncryptor => _scpApduTransform.DataEncryptor; + private static IApduTransform CreateParentPipeline(IApduTransform pipeline, YubiKeyApplication application) + { + // Wrap the pipeline with error handling if needed + if (application == YubiKeyApplication.Fido2) + { + return new FidoErrorTransform(pipeline); + } + + if (application == YubiKeyApplication.Otp) + { + return new OtpErrorTransform(pipeline); + } + + return pipeline; + } + + private ScpApduTransform CreateScpPipeline(ScpKeyParameters keyParameters) + { + // Get the current pipeline + var previousPipeline = GetPipeline(); + + // Wrap the pipeline in ScpApduTransform + var scpApduTransform = new ScpApduTransform(previousPipeline, keyParameters); + + // Return both pipeline + return scpApduTransform; + } - // private ScpApduTransform SetObject( TODO Is this needed? I dont why - // YubiKeyApplication application, - // ScpKeyParameters keyParameters) - // { - // var previousPipeline = GetPipeline(); - // var appendedPipeline = new ScpApduTransform(previousPipeline, keyParameters); - // - // // Is it even possible to connect to Fido2 and Otp with SCP? - // // IApduTransform apduPipeline = application switch - // // { - // // YubiKeyApplication.Fido2 => new FidoErrorTransform(appendedPipeline), - // // YubiKeyApplication.Otp => new OtpErrorTransform(appendedPipeline), - // // _ => appendedPipeline - // // }; - // - // // Set parent pipeline - // // SetPipeline(apduPipeline); - // // apduPipeline.Setup(); - // - // // Set parent pipeline - // SetPipeline(appendedPipeline); - // appendedPipeline.Setup(); - // - // return appendedPipeline; - // } - protected override void Dispose(bool disposing) { if (!_disposed) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs index 4fbc714cd..8d1daca11 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs @@ -1,6 +1,7 @@ using System; using Yubico.Core.Iso7816; using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Scp.Helpers; namespace Yubico.YubiKey.Scp { @@ -15,7 +16,7 @@ internal abstract class ScpState : IDisposable /// /// Initializes the host-side state for an SCP session. /// - public ScpState(SessionKeys sessionKeys, Memory macChain) + protected ScpState(SessionKeys sessionKeys, Memory macChain) { MacChainingValue = macChain; SessionKeys = sessionKeys; @@ -46,9 +47,8 @@ public CommandApdu EncodeCommand(CommandApdu command) P2 = command.P2 }; - byte[] commandData = command.Data.ToArray(); var encryptedData = ChannelEncryption.EncryptData( - commandData, SessionKeys.EncKey.Span, _encryptionCounter); + command.Data.Span, SessionKeys.EncKey.Span, _encryptionCounter); _encryptionCounter++; encodedCommand.Data = encryptedData; @@ -96,13 +96,13 @@ public ResponseApdu DecodeResponse(ResponseApdu response) var responseData = response.Data; VerifyRmac(responseData.Span, SessionKeys.RmacKey.Span, MacChainingValue.Span); - Memory decryptedData = Array.Empty(); + ReadOnlyMemory decryptedData = Array.Empty(); if (responseData.Length > 8) { int previousEncryptionCounter = _encryptionCounter - 1; decryptedData = ChannelEncryption.DecryptData( - responseData[..^8].ToArray(), - SessionKeys.EncKey.ToArray(), //todo array + responseData[..^8].Span, + SessionKeys.EncKey.Span, previousEncryptionCounter ); } @@ -114,39 +114,47 @@ public ResponseApdu DecodeResponse(ResponseApdu response) return new ResponseApdu(fullDecryptedResponse); } - public DataEncryptor GetDataEncryptor() + /// + /// Get the encryptor to encrypt any data for a SCP command. + /// + /// + /// An encryptor function that takes the plaintext as a parameter and + /// returns the encrypted data. + /// + /// + /// If the data encryption key has not been set on the session keys. + /// + public EncryptDataFunc GetDataEncryptor() { if (!SessionKeys.DataEncryptionKey.HasValue) { - throw new InvalidOperationException(ExceptionMessages.UnknownScpError); //todo set correct message + throw new InvalidOperationException(ExceptionMessages.UnknownScpError); } return plainText => AesUtilities.AesCbcEncrypt( - SessionKeys.DataEncryptionKey.Value.ToArray(), - new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + SessionKeys.DataEncryptionKey.Value.Span, + new byte[16], plainText.Span); } - #pragma warning disable CA1822 // Is being used by subclasses - protected (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( - #pragma warning restore CA1822 + protected static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( CommandApdu commandApdu, ReadOnlySpan macKey, ReadOnlySpan macChainingValue) => ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); - protected static void VerifyRmac( - ReadOnlySpan responseData, - ReadOnlySpan rmacKey, - ReadOnlySpan macChainingValue) => - ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); - public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } + private static void VerifyRmac( + ReadOnlySpan responseData, + ReadOnlySpan rmacKey, + ReadOnlySpan macChainingValue) => + ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); + protected virtual void Dispose(bool disposing) { if (!_disposed) @@ -161,5 +169,9 @@ protected virtual void Dispose(bool disposing) } } - internal delegate ReadOnlyMemory DataEncryptor(ReadOnlyMemory data); + /// + /// This delegate is used to encrypt data with the session keys + /// + /// + internal delegate ReadOnlyMemory EncryptDataFunc(ReadOnlyMemory data); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 98029c7d8..9105e7a6e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -14,15 +14,13 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; -using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; using Microsoft.Extensions.Logging; +using Yubico.Core.Buffers; using Yubico.Core.Iso7816; using Yubico.Core.Logging; using Yubico.Core.Tlv; @@ -88,12 +86,13 @@ namespace Yubico.YubiKey.Scp /// exception. /// /// - public sealed class SecurityDomainSession : IDisposable + public sealed class SecurityDomainSession : ApplicationSession { - private const byte KeyTypeEccPrivateKey = 0xB1; - private const byte KeyTypeEccKeyParams = 0xF0; - private const byte KeyTypeEccPublicKey = 0xB0; - private const byte KeyTypeAes = 0x88; + #region Tags + private const byte EcKeyType = 0xF0; + private const byte EcPublicKeyKeyType = 0xB0; + private const byte EcPrivateKeyKeyType = 0xB1; + private const byte AesKeyType = 0x88; private const byte ControlReferenceTag = 0xA6; private const byte KidKvnTag = 0x83; private const byte KeyInformationTag = 0xE0; @@ -103,21 +102,38 @@ public sealed class SecurityDomainSession : IDisposable private const ushort CertificateStoreTag = 0xBF21; private const ushort CaKlocIdentifiersTag = 0xFF33; // Key Loading OCE Certificate private const ushort CaKlccIdentifiersTag = 0xFF34; // Key Loading Card Certificate - private readonly IYubiKeyDevice _yubiKey; - private readonly ILogger _log = Log.GetLogger(); - private bool _disposed; - private readonly IScpYubiKeyConnection? _connection; + #endregion - private IScpYubiKeyConnection AuthenticatedConnection => - _connection ?? throw new InvalidOperationException("No secure connection initialized."); + private EncryptDataFunc EncryptData + { + get + { + if (Connection is IScpYubiKeyConnection scpConnection) + { + return scpConnection.EncryptDataFunc; + } - private IYubiKeyConnection UnauthenticatedConnection => _yubiKey.Connect(YubiKeyApplication.SecurityDomain); + throw new InvalidOperationException("No secure connection initialized."); + } + } - // The default constructor explicitly defined. We don't want it to be - // used. - private SecurityDomainSession() + /// + /// Create an unauthenticated instance of , the object that + /// represents SCP on the YubiKey. + /// + /// Sessions created from this constructor will not be able to perform operations which require authentication + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. + /// + /// + /// The object that represents the actual YubiKey which will perform the + /// operations. + /// + /// + /// The yubiKey argument is null. + /// + public SecurityDomainSession(IYubiKeyDevice yubiKey) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.SecurityDomain, null) { - throw new NotImplementedException(); } /// @@ -126,18 +142,16 @@ private SecurityDomainSession() /// /// /// See the User's Manual entry on SCP. + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information on SCP. /// /// Because this class implements IDisposable, use the using /// keyword. For example, /// /// if (YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) /// { - /// var staticKeys = new StaticKeys(); - /// // Note that you do not need to call the "WithScp" method when - /// // using the ScpSession class. - /// using (var scp = new ScpSession(yubiKeyDevice, staticKeys)) + /// using (var scp = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey)) /// { - /// // Perform SCP operations. + /// // Perform SCP operations while authenticated with SCP03 /// } /// } /// @@ -156,41 +170,8 @@ private SecurityDomainSession() /// The yubiKey or scpKeys argument is null. /// public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeyParameters) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.SecurityDomain, scpKeyParameters) { - _log.LogInformation("Create a new instance of ScpSession."); - - if (yubiKey is null) - { - throw new ArgumentNullException(nameof(yubiKey)); - } - - if (scpKeyParameters is null) - { - throw new ArgumentNullException(nameof(scpKeyParameters)); - } - - _yubiKey = yubiKey; - _connection = yubiKey.ConnectScp(YubiKeyApplication.SecurityDomain, scpKeyParameters); - } - - /// - /// Create an unauthenticated instance of , the object that - /// represents SCP on the YubiKey. - /// - /// Sessions created from this constructor will not be able to perform operations which require authentication - /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. - /// - /// - /// The object that represents the actual YubiKey which will perform the - /// operations. - /// - /// - /// The yubiKey argument is null. - /// - public SecurityDomainSession(IYubiKeyDevice yubiKey) - { - _log.LogInformation("Create a new instance of ScpSession."); - _yubiKey = yubiKey ?? throw new ArgumentNullException(nameof(yubiKey)); } /// @@ -200,20 +181,17 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey) /// The new SCP03 key set to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the KID is not 0x01 for SCP03 key sets. - /// Thrown when the new key set's checksum failed to verify, or some other scp related error + /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) { - _log.LogInformation("Importing SCP03 key set into KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Importing SCP03 key set into KeyRef {KeyRef}", keyRef); if (keyRef.Id != ScpKid.Scp03) { throw new ArgumentException("KID must be 0x01 for SCP03 key sets"); } - var encryptor = AuthenticatedConnection.DataEncryptor ?? - throw new InvalidOperationException("No session DEK available"); - using var dataStream = new MemoryStream(); using var dataWriter = new BinaryWriter(dataStream); using var expectedKcvStream = new MemoryStream(); @@ -223,8 +201,9 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) dataWriter.Write(keyRef.VersionNumber); expectedKcvWriter.Write(keyRef.VersionNumber); - Span kcvInput = stackalloc byte[16] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; + Span kcvInput = stackalloc byte[16]; ReadOnlySpan kvcZeroIv = stackalloc byte[16]; + kcvInput.Fill(1); // Process all keys foreach (var key in new[] @@ -241,10 +220,10 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) kcvInput)[..3]; // Encrypt the key using session encryptor - var encryptedKey = encryptor(key); + var encryptedKey = EncryptData(key); // Write key structure - var tlvData = new TlvObject(KeyTypeAes, encryptedKey.Span.ToArray()).GetBytes(); + var tlvData = new TlvObject(AesKeyType, encryptedKey.Span.ToArray()).GetBytes(); dataWriter.Write(tlvData.ToArray()); // Write KCV @@ -260,14 +239,14 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) byte p2 = (byte)(0x80 | keyRef.Id); // OR with 0x80 indicates that we're sending multiple keys var command = new PutKeyCommand((byte)replaceKvn, p2, commandData); - var response = AuthenticatedConnection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); var responseKcvData = response.GetData().Span; ReadOnlySpan expectedKcvData = expectedKcvStream.ToArray().AsSpan(); ValidateCheckSum(expectedKcvData, responseKcvData); - _log.LogInformation("Successsfully put static keys for KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Successsfully put static keys for KeyRef {KeyRef}", keyRef); } /// @@ -278,14 +257,11 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) /// The key version number to replace, or 0 for a new key. /// Thrown when the private key is not of type SECP256R1. /// Thrown when no secure session is established. - /// Thrown when the new key set's checksum failed to verify, or some other scp related error + /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParameters, int replaceKvn) { - _log.LogInformation("Importing SCP11 private key into KeyRef {KeyRef}", keyRef); - - var encryptor = AuthenticatedConnection.DataEncryptor ?? throw new InvalidOperationException( - "No secure session established. DataEncryptor required for key import."); + Logger.LogInformation("Importing SCP11 private key into KeyRef {KeyRef}", keyRef); var privateKey = privateKeyParameters.Parameters; if (privateKey.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) @@ -307,8 +283,8 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet try { // Must be encrypted with the active sessions data encryption key - var encryptedKey = encryptor(privateKeyBytes); - var privateKeyTlv = new TlvObject(KeyTypeEccPrivateKey, encryptedKey.Span).GetBytes(); + var encryptedKey = EncryptData(privateKeyBytes); + var privateKeyTlv = new TlvObject(EcPrivateKeyKeyType, encryptedKey.Span).GetBytes(); commandDataWriter.Write(privateKeyTlv.ToArray()); } finally @@ -317,13 +293,13 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet } // Write the ECC parameters - var paramsTlv = new TlvObject(KeyTypeEccKeyParams, new byte[] { 0x00 }).GetBytes(); + var paramsTlv = new TlvObject(EcKeyType, new byte[] { 0x00 }).GetBytes(); commandDataWriter.Write(paramsTlv.ToArray()); commandDataWriter.Write((byte)0); // Create and send the command var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, commandDataStream.ToArray()); - var response = AuthenticatedConnection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); // Get and validate the response @@ -331,12 +307,11 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet Span expectedResponseData = new[] { keyRef.VersionNumber }; ValidateCheckSum(responseData.Span, expectedResponseData); - _log.LogInformation("Successsfully put private key for KeyRef {KeyRef}", keyRef); - + Logger.LogInformation("Successsfully put private key for KeyRef {KeyRef}", keyRef); } catch (Exception ex) { - _log.LogError(ex, "Failed to put private key for KeyRef {KeyRef}", keyRef); + Logger.LogError(ex, "Failed to put private key for KeyRef {KeyRef}", keyRef); throw; } } @@ -349,11 +324,11 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet /// The key version number to replace, or 0 for a new key. /// Thrown when the public key is not of type SECP256R1. /// Thrown when no secure session is established. - /// Thrown when the new key set's checksum failed to verify, or some other scp related error + /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameters, int replaceKvn) { - _log.LogInformation("Importing SCP11 public key into KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Importing SCP11 public key into KeyRef {KeyRef}", keyRef); var pkParams = publicKeyParameters.Parameters; if (pkParams.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) @@ -376,18 +351,18 @@ public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameter .Concat(pkParams.Q.X) .Concat(pkParams.Q.Y).ToArray().AsSpan(); - byte[] publicKeyTlvData = new TlvObject(KeyTypeEccPublicKey, publicKeyRawData).GetBytes().ToArray(); + byte[] publicKeyTlvData = new TlvObject(EcPublicKeyKeyType, publicKeyRawData).GetBytes().ToArray(); commandDataWriter.Write(publicKeyTlvData); // Write the ECC parameters - var paramsTlv = new TlvObject(KeyTypeEccKeyParams, new byte[] { 0x00 }).GetBytes(); + var paramsTlv = new TlvObject(EcKeyType, new byte[] { 0 }).GetBytes(); commandDataWriter.Write(paramsTlv.ToArray()); commandDataWriter.Write((byte)0); // Create and send the command byte[] commandData = commandDataMs.ToArray(); var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, commandData); - var response = AuthenticatedConnection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); // Get and validate the response @@ -396,23 +371,15 @@ public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameter ValidateCheckSum(responseData.Span, expectedResponseData); - _log.LogInformation("Successsfully put public key for KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Successsfully put public key for KeyRef {KeyRef}", keyRef); } catch (Exception ex) { - _log.LogError(ex, "Failed to put public key for KeyRef {KeyRef}", keyRef); + Logger.LogError(ex, "Failed to put public key for KeyRef {KeyRef}", keyRef); throw; } } - private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySpan expectedResponseData) - { - if (!CryptographicOperations.FixedTimeEquals(responseData, expectedResponseData)) - { - throw new SecureChannelException(ExceptionMessages.ChecksumError); - } - } - // /// // /// Delete the key set with the given keyVersionNumber. If the key // /// set to delete is the last SCP key set on the YubiKey, pass @@ -433,12 +400,12 @@ private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySp // /// true, otherwise, pass false. This arg has a default of // /// false so if no argument is given, it will be false. // /// - // /// Thrown when there was an scp error, described in the exception message. + // /// Thrown when there was an SCP error, described in the exception message. // public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) // { // _log.LogInformation("Deleting an SCP key set from a YubiKey."); // var command = new DeleteKeyCommand(keyVersionNumber, isLastKey); - // var response = AuthenticatedConnection.SendCommand(command); + // var response = Connection.SendCommand(command); // ThrowIfFailed(response); // _log.LogInformation("Successfully deleted {{KeyVersionNumber}}", keyVersionNumber); // } @@ -481,7 +448,7 @@ public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) } } - _log.LogDebug("Deleting keys matching KeyRef {KeyRef}", keyRef); + Logger.LogDebug("Deleting keys matching KeyRef {KeyRef}", keyRef); // Build TLV list for command data var tlvList = new List(); @@ -497,11 +464,11 @@ public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) byte[] data = TlvObjects.EncodeList(tlvList); var command = new DeleteKeyCommand(data, deleteLast); - var response = AuthenticatedConnection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); - _log.LogInformation("Keys deleted. KeyRef: ({KeyReference})", keyRef); + Logger.LogInformation("Keys deleted. KeyRef: ({KeyReference})", keyRef); } /// @@ -515,10 +482,10 @@ public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) /// The KID-KVN pair of the key that should be generated. /// The key version number of the key set that should be replaced, or 0 to generate a new key pair. /// The parameters of the generated key, including the curve and the public point. - /// Thrown when there was an scp error, described in the exception message. + /// Thrown when there was an SCP error, described in the exception message. public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) { - _log.LogInformation( + Logger.LogInformation( "Generating new key for {KeyRef}{ReplaceMessage}", keyRef, replaceKvn == 0 @@ -526,19 +493,19 @@ public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) : $", replacing KVN=0x{replaceKvn:X2}"); // Create tlv data for the command - var paramsTlv = new TlvObject(KeyTypeEccKeyParams, new byte[] { 0 }).GetBytes(); + var paramsTlv = new TlvObject(EcKeyType, new byte[] { 0 }).GetBytes(); byte[] commandData = new byte[paramsTlv.Length + 1]; commandData[0] = keyRef.VersionNumber; paramsTlv.CopyTo(commandData.AsMemory(1)); // Create and send the command var command = new GenerateEcKeyCommand(replaceKvn, keyRef.Id, commandData); - var response = AuthenticatedConnection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); // Parse the response, extract the public point var tlvReader = new TlvReader(response.GetData()); - var encodedPoint = tlvReader.ReadValue(KeyTypeEccPublicKey).Span; + var encodedPoint = tlvReader.ReadValue(EcPublicKeyKeyType).Span; // Create the ECParameters with the public point var eccPublicKey = encodedPoint.CreateEcPublicKeyFromBytes(); @@ -553,7 +520,7 @@ public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) /// The Subject Key Identifier to store. public void StoreCaIssuer(KeyReference keyRef, ReadOnlyMemory ski) { - _log.LogDebug("Storing CA issuer SKI for {KeyRef}", keyRef); + Logger.LogDebug("Storing CA issuer SKI for {KeyRef}", keyRef); byte klcc = 0; // Key Loading Card Certificate switch (keyRef.Id) @@ -579,7 +546,7 @@ public void StoreCaIssuer(KeyReference keyRef, ReadOnlyMemory ski) // Send store data command StoreData(data); - _log.LogInformation("CA issuer SKI stored"); + Logger.LogInformation("CA issuer SKI stored"); } /// @@ -591,10 +558,10 @@ public void StoreCaIssuer(KeyReference keyRef, ReadOnlyMemory ski) /// The certificates will be stored in the order they are provided in the list. /// /// Thrown when certificatedata - /// Thrown when there was an scp error, described in the exception message. + /// Thrown when there was an SCP error, described in the exception message. public void StoreCertificates(KeyReference keyRef, IReadOnlyList certificates) { - _log.LogDebug("Storing certificate bundle for {KeyRef}", keyRef); + Logger.LogDebug("Storing certificate bundle for {KeyRef}", keyRef); // Write each certificate to a memory stream using var certDataMs = new MemoryStream(); @@ -613,13 +580,13 @@ public void StoreCertificates(KeyReference keyRef, IReadOnlyList certDataEncoded = TlvObjects.EncodeMany( - new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), + new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), new TlvObject(CertificateStoreTag, certDataMs.ToArray()) - ); + ); StoreData(certDataEncoded); - _log.LogInformation("Certificate bundle stored"); + Logger.LogInformation("Certificate bundle stored"); } /// @@ -630,19 +597,19 @@ public void StoreCertificates(KeyReference keyRef, IReadOnlyList /// A reference to the key for which the allowlist will be stored. - /// The list of certificate serial numbers to be stored in the allowlist. + /// The list of certificate serial numbers (in hexadecimal string format) to be stored in the allowlist for the given . /// Thrown when a serial number cannot be encoded properly. - /// Thrown when there was an scp error, described in the exception message. - public void StoreAllowlist(KeyReference keyRef, IReadOnlyList serials) + /// Thrown when there was an SCP error, described in the exception message. + public void StoreAllowlist(KeyReference keyRef, IReadOnlyCollection serials) { - _log.LogDebug("Storing allow list for {KeyRef}", keyRef); + Logger.LogDebug("Storing allow list for {KeyRef}", keyRef); using var serialDataMs = new MemoryStream(); - foreach (var serial in serials) + foreach (string? serial in serials) { try { - byte[] serialAsBytes = serial.ToByteArray(); + byte[] serialAsBytes = Base16.DecodeText(serial); byte[] serialTlvEncoded = new TlvObject(SerialTag, serialAsBytes).GetBytes().ToArray(); serialDataMs.Write(serialTlvEncoded, 0, serialTlvEncoded.Length); } @@ -655,28 +622,38 @@ public void StoreAllowlist(KeyReference keyRef, IReadOnlyList serial Memory serialsDataEncoded = TlvObjects.EncodeMany( new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), new TlvObject(SerialsAllowListTag, serialDataMs.ToArray()) - ); + ); StoreData(serialsDataEncoded); - _log.LogInformation("Certificate bundle stored"); + Logger.LogInformation("Certificate bundle stored"); } + /// + /// Clears the allow list for the given + /// + /// + /// The key reference that holds the allow list + public void ClearAllowList(KeyReference keyRef) => StoreAllowlist(keyRef, Array.Empty()); + /// /// Stores data in the Security Domain or targeted Application on the YubiKey using the GlobalPlatform STORE DATA command. /// /// /// The STORE DATA command is used to transfer data to either the Security Domain itself or to an Application /// being personalized. The data must be formatted as BER-TLV structures according to ISO 8825. - /// + /// /// This implementation: /// - Uses a single block transfer (P1.b8=1 indicating last block) /// - Requires BER-TLV formatted data (P1.b5-b4=10) /// - Does not provide encryption information (P1.b7-b6=00) - /// + /// + /// /// Note that this command's behavior depends on the current security context: /// - Outside a personalization session: Data is processed by the Security Domain /// - During personalization (after INSTALL [for personalization]): Data is forwarded to the target Application + /// + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. /// /// /// The data to be stored, which must be formatted as BER-TLV structures according to ISO 8825. @@ -684,13 +661,13 @@ public void StoreAllowlist(KeyReference keyRef, IReadOnlyList serial /// /// Thrown when no secure connection is available or the security context is invalid. /// - /// Thrown when there was an scp error, described in the exception message. - public void StoreData(ReadOnlyMemory data) + /// Thrown when there was an SCP error, described in the exception message. + public void StoreData(ReadOnlyMemory data) // TODO make test { - _log.LogInformation("Storing data with length:{Length}", data.Length); + Logger.LogInformation("Storing data with length:{Length}", data.Length); var command = new StoreDataCommand(data); - var response = AuthenticatedConnection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); } @@ -698,10 +675,10 @@ public void StoreData(ReadOnlyMemory data) /// Retrieves the key information stored in the YubiKey and returns it in a dictionary format. /// /// A read only dictionary containing the KeyReference as the key and a dictionary of key components as the value. - /// Thrown when there was an scp error, described in the exception message. + /// Thrown when there was an SCP error, described in the exception message. public IReadOnlyDictionary> GetKeyInformation() { - _log.LogInformation("Getting key information"); + Logger.LogInformation("Getting key information"); var keyInformation = new Dictionary>(); @@ -730,12 +707,13 @@ public IReadOnlyDictionary> GetKeyInformati /// /// The key reference for which the certificates should be retrieved. /// A list of X.509 certificates associated with the key reference. - /// Thrown when there was an scp error, described in the exception message. + /// Thrown when there was an SCP error, described in the exception message. public IReadOnlyList GetCertificates(KeyReference keyReference) { - _log.LogInformation("Getting certificates for key={KeyRef}", keyReference); + Logger.LogInformation("Getting certificates for key={KeyRef}", keyReference); - var nestedTlv = new TlvObject(ControlReferenceTag, + var nestedTlv = new TlvObject( + ControlReferenceTag, new TlvObject(KidKvnTag, keyReference.GetBytes).GetBytes().Span ).GetBytes(); @@ -755,14 +733,15 @@ public IReadOnlyList GetCertificates(KeyReference keyReference /// A dictionary of KeyReference and byte arrays representing the CA identifiers. /// Thrown when both kloc and klcc are false. /// Thrown when there was an SCP error, described in the exception message. - public IReadOnlyDictionary> GetSupportedCaIdentifiers(bool kloc, bool klcc) + public IReadOnlyDictionary> + GetSupportedCaIdentifiers(bool kloc, bool klcc) // TODO make test { if (!kloc && !klcc) { throw new ArgumentException("At least one of kloc and klcc must be true"); } - _log.LogDebug("Getting CA identifiers KLOC={Kloc}, KLCC={Klcc}", kloc, klcc); + Logger.LogDebug("Getting CA identifiers KLOC={Kloc}, KLCC={Klcc}", kloc, klcc); var dataMs = new MemoryStream(); @@ -770,13 +749,12 @@ public IReadOnlyDictionary> GetSupportedCaIde { try { - var klocData = GetData(CaKlocIdentifiersTag); dataMs.Write(klocData.Span.ToArray(), 0, klocData.Length); } - catch (SecureChannelException e) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) + catch + (SecureChannelException) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) TODO how get response status? { - // Ignore this specific exception } } @@ -788,7 +766,7 @@ public IReadOnlyDictionary> GetSupportedCaIde var klccData = GetData(CaKlccIdentifiersTag); dataMs.Write(klccData.Span.ToArray(), 0, klccData.Length); } - catch (SecureChannelException e) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) + catch (SecureChannelException) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) TODO { // Ignore this specific exception } @@ -813,12 +791,11 @@ public IReadOnlyDictionary> GetSupportedCaIde return identifiers; } - - public Memory GetCardRecognitionData() + public Memory GetCardRecognitionData() // TODO Ask Dain // TODO make test { - _log.LogInformation("Getting card recognition deta"); + Logger.LogInformation("Getting card recognition deta"); - var tlvData = GetData(CardRecognitionDataTag, null).Span; + var tlvData = GetData(CardRecognitionDataTag).Span; var cardRecognitionData = TlvObjects.UnpackValue(0x73, tlvData); return cardRecognitionData; @@ -829,20 +806,20 @@ public Memory GetCardRecognitionData() /// /// The tag of the data to retrieve. /// Optional data to send with the command. + /// Sessions created from this constructor will not be able to perform operations which require authentication + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. + /// /// The encoded tlv data retrieved from the YubiKey. - /// Thrown when there was an scp error, described in the exception message. + /// Thrown when there was an SCP error, described in the exception message. public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) { - var connection = _connection ?? UnauthenticatedConnection; - var command = new GetDataCommand(tag, data); - var response = connection.SendCommand(command); + var response = Connection.SendCommand(command); ThrowIfFailed(response); return response.GetData(); } - /// /// Perform a factory reset of the Security Domain. /// This will remove all keys and associated data, as well as restore the default SCP03 static keys, @@ -850,12 +827,7 @@ public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) /// public void Reset() { - _log.LogDebug("Resetting all SCP keys"); - - const byte insInitializeUpdate = 0x50; - const byte insExternalAuthenticate = 0x82; - const byte insInternalAuthenticate = 0x88; - const byte insPerformSecurityOperation = 0x2A; + Logger.LogInformation("Resetting all SCP keys"); var keys = GetKeyInformation().Keys; foreach (var keyRef in keys) // Reset is done by blocking all available keys @@ -869,27 +841,27 @@ public void Reset() // SCP03 uses KID=0, we use KVN=0 to allow deleting the default keys // which have an invalid KVN (0xff). overridenKeyRef = new KeyReference(0, 0); - ins = insInitializeUpdate; + ins = InitializeUpdateCommand.GpInitializeUpdateIns; break; case 0x02: case 0x03: continue; // Skip these as they are deleted by 0x01 case ScpKid.Scp11a: case ScpKid.Scp11c: - ins = insExternalAuthenticate; + ins = ExternalAuthenticateCommand.GpExternalAuthenticateIns; break; case ScpKid.Scp11b: - ins = insInternalAuthenticate; + ins = InternalAuthenticateCommand.GpInternalAuthenticateIns; break; default: // 0x10, 0x20-0x2F - ins = insPerformSecurityOperation; + ins = SecurityOperationCommand.GpPerformSecurityOperationIns; break; } // Keys have 65 attempts before blocking (and thus removal) for (int i = 0; i < 65; i++) { - var result = UnauthenticatedConnection.SendCommand( + var result = Connection.SendCommand( new ResetCommand(ins, overridenKeyRef.VersionNumber, overridenKeyRef.Id, new byte[8])); switch (result.StatusWord) @@ -908,7 +880,15 @@ public void Reset() } } - _log.LogInformation("SCP keys reset"); + Logger.LogInformation("SCP keys reset"); + } + + private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySpan expectedResponseData) + { + if (!CryptographicOperations.FixedTimeEquals(responseData, expectedResponseData)) + { + throw new SecureChannelException(ExceptionMessages.ChecksumError); + } } private static void ThrowIfFailed(ScpResponse response) @@ -919,32 +899,8 @@ private static void ThrowIfFailed(ScpResponse response) string.Format( CultureInfo.CurrentCulture, ExceptionMessages.YubiKeyOperationFailed, - response.StatusMessage)); - } - } - - /// - /// When the ScpSession object goes out of scope, this method is called. - /// It will close the session. The most important function of closing a - /// session is to close the connection. - /// - - // Note that .NET recommends a Dispose method call Dispose(true) and - // GC.SuppressFinalize(this). The actual disposal is in the - // Dispose(bool) method. - // However, that does not apply to sealed classes. - // So the Dispose method will simply perform the - // "closing" process, no call to Dispose(bool) or GC. - public void Dispose() - { - if (_disposed) - { - return; + response.StatusMessage)); } - - _connection?.Dispose(); - - _disposed = true; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs index be49667db..f985f0cf8 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs @@ -77,6 +77,7 @@ namespace Yubico.YubiKey.Scp03 /// exception. /// /// + [Obsolete("Use new SecurityDomainSession")] public sealed class Scp03Session : IDisposable { private bool _disposed; @@ -87,7 +88,6 @@ public sealed class Scp03Session : IDisposable /// applications will ignore this, but it can be used to call Commands /// directly. /// - [Obsolete("Obsolete")] public IScp03YubiKeyConnection Connection { get; private set; } // The default constructor explicitly defined. We don't want it to be @@ -132,7 +132,6 @@ private Scp03Session() /// /// The yubiKey or scp03Keys argument is null. /// - [Obsolete("Use new Scp")] public Scp03Session(IYubiKeyDevice yubiKey, StaticKeys scp03Keys) { _log.LogInformation("Create a new instance of Scp03Session."); @@ -244,7 +243,6 @@ public Scp03Session(IYubiKeyDevice yubiKey, StaticKeys scp03Keys) /// The new key set's checksum failed to verify, or some other error /// described in the exception message. /// - [Obsolete("Obsolete")] public void PutKeySet(StaticKeys newKeySet) { _log.LogInformation("Put a new SCP03 key set onto a YubiKey."); @@ -292,7 +290,6 @@ public void PutKeySet(StaticKeys newKeySet) /// true, otherwise, pass false. This arg has a default of /// false so if no argument is given, it will be false. /// - [Obsolete("Obsolete")] public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) { _log.LogInformation("Deleting an SCP03 key set from a YubiKey."); @@ -321,7 +318,6 @@ public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) // However, that does not apply to sealed classes. // So the Dispose method will simply perform the // "closing" process, no call to Dispose(bool) or GC. - [Obsolete("Obsolete")] public void Dispose() { if (_disposed) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs index ba996ff21..63357ac94 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs @@ -93,7 +93,7 @@ public byte KeyVersionNumber _keyVersionNumber = value; } - } //TODO replace this? + } /// /// AES128 shared secret key used to calculate the Session-MAC key. Also diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs index e0c5d7787..b364877f0 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs @@ -29,6 +29,7 @@ internal class SmartCardConnection : IYubiKeyConnection { private readonly ILogger _log = Log.GetLogger(); + // The application can be set either by using the YubikeyApplication enum or the Iso7816ApplicationId private readonly YubiKeyApplication _yubiKeyApplication; private readonly byte[]? _applicationId; private readonly ISmartCardConnection _smartCardConnection; @@ -37,30 +38,22 @@ internal class SmartCardConnection : IYubiKeyConnection public ISelectApplicationData? SelectApplicationData { get; set; } - protected SmartCardConnection(ISmartCardDevice smartCardDevice, YubiKeyApplication application, byte[]? applicationId) - { - if (applicationId is null && application == YubiKeyApplication.Unknown) - { - throw new NotSupportedException(); - } - - _yubiKeyApplication = application; - _applicationId = applicationId; - - _smartCardConnection = smartCardDevice.Connect(); - - _apduPipeline = new SmartCardTransform(_smartCardConnection); - _apduPipeline = AddResponseChainingTransform(_apduPipeline); - _apduPipeline = new CommandChainingTransform(_apduPipeline); - } - - public SmartCardConnection(ISmartCardDevice smartCardDevice, YubiKeyApplication yubiKeyApplication) + /// + /// Initializes a new instance of the class with the specified + /// smart card device and YubiKey application. + /// + /// The smart card device to connect to. + /// The YubiKey application to be used for the connection. + public SmartCardConnection( + ISmartCardDevice smartCardDevice, + YubiKeyApplication yubiKeyApplication) : this(smartCardDevice, yubiKeyApplication, null) { if (yubiKeyApplication == YubiKeyApplication.Fido2) { _apduPipeline = new FidoErrorTransform(_apduPipeline); - } + } // TODO Why does this constructor following logic differ from overloaded one below? + // It appears to be missing Otp Error Transform // CCID has the concept of multiple applications. Since we cannot guarantee the // state of the smart card when connecting, we should always send down a connection @@ -68,8 +61,19 @@ public SmartCardConnection(ISmartCardDevice smartCardDevice, YubiKeyApplication SelectApplication(); } - public SmartCardConnection(ISmartCardDevice smartCardDevice, byte[] applicationId) - : this(smartCardDevice, YubiKeyApplication.Unknown, applicationId) + /// + /// Initializes a new instance of the class with the specified + /// smart card device and application id. + /// + /// The smart card device to connect to. + /// The application id of the YubiKey application to be used for the connection. + public SmartCardConnection( + ISmartCardDevice smartCardDevice, + byte[] applicationId) + : this( + smartCardDevice, + YubiKeyApplication.Unknown, + applicationId) { if (applicationId.SequenceEqual(YubiKeyApplication.Fido2.GetIso7816ApplicationId())) { @@ -86,12 +90,43 @@ public SmartCardConnection(ISmartCardDevice smartCardDevice, byte[] applicationI SelectApplication(); } + /// + /// Initializes a new instance of the class with the specified + /// smart card device, YubiKey application, and application id. + /// + /// The smart card device to connect to. + /// The YubiKey application to be used for the connection. + /// The application id of the YubiKey application to be used for the connection. + protected SmartCardConnection( + ISmartCardDevice smartCardDevice, + YubiKeyApplication application, + byte[]? applicationId) + { + if (applicationId is null && application == YubiKeyApplication.Unknown) + { + throw new NotSupportedException(); + } + + _yubiKeyApplication = application; + _applicationId = applicationId; + + _smartCardConnection = smartCardDevice.Connect(); + + _apduPipeline = new SmartCardTransform(_smartCardConnection); + _apduPipeline = AddResponseChainingTransform(_apduPipeline); + _apduPipeline = new CommandChainingTransform(_apduPipeline); + } + // Allow subclasses to build a different pipeline, which means they need // to get the current one. protected IApduTransform GetPipeline() => _apduPipeline; - // Allow subclasses to build a different pipeline and set it here in the - // base class. + /// + /// This method is protected thus allows subclasses to build a different pipeline and set it here in the + /// base class. + /// It also issues a call to SelectApplication() + /// + /// protected void SetPipeline(IApduTransform apduPipeline) { _apduPipeline = apduPipeline; @@ -99,43 +134,55 @@ protected void SetPipeline(IApduTransform apduPipeline) SelectApplication(); } - private bool IsOath => _yubiKeyApplication == YubiKeyApplication.Oath - || (_applicationId != null && _applicationId.SequenceEqual(YubiKeyApplicationExtensions.GetIso7816ApplicationId(YubiKeyApplication.Oath))); + // The application is set to Oath by enum or by application id + private bool IsOath => + _yubiKeyApplication == YubiKeyApplication.Oath || + (_applicationId != null && + _applicationId.SequenceEqual( + YubiKeyApplication.Oath.GetIso7816ApplicationId())); private IApduTransform AddResponseChainingTransform(IApduTransform pipeline) => IsOath - ? new OathResponseChainingTransform(pipeline) - : new ResponseChainingTransform(pipeline); + ? new OathResponseChainingTransform(pipeline) + : new ResponseChainingTransform(pipeline); private void SelectApplication() { - IYubiKeyCommand> selectApplicationCommand = _yubiKeyApplication switch - { - YubiKeyApplication.Oath => new Oath.Commands.SelectOathCommand(), - YubiKeyApplication.Unknown => new SelectApplicationCommand(_applicationId!), - _ => new SelectApplicationCommand(_yubiKeyApplication), - }; + // Gets the correct select application command. + // Note that Oath is special and has a different command than the generic SelectApplication command + IYubiKeyCommand> selectApplicationCommand = + _yubiKeyApplication switch + { + YubiKeyApplication.Oath => new Oath.Commands.SelectOathCommand(), + YubiKeyApplication.Unknown => new SelectApplicationCommand(_applicationId!), + _ => new SelectApplicationCommand(_yubiKeyApplication), + }; + + _log.LogInformation( + "Selecting smart card application [{AID}]", + Base16.EncodeBytes(_applicationId ?? _yubiKeyApplication.GetIso7816ApplicationId())); - _log.LogInformation("Selecting smart card application [{AID}]", Hex.BytesToHex(_applicationId ?? _yubiKeyApplication.GetIso7816ApplicationId())); - + // Transmit command var responseApdu = _smartCardConnection.Transmit(selectApplicationCommand.CreateCommandApdu()); if (responseApdu.SW != SWConstants.Success) { throw new ApduException( - string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.SmartCardPipelineSetupFailed, - responseApdu.SW)) - { - SW = responseApdu.SW - }; + string.Format( + CultureInfo.CurrentCulture, + ExceptionMessages.SmartCardPipelineSetupFailed, + responseApdu.SW)) + { + SW = responseApdu.SW + }; } + // Set the instance property SelectApplicationData var response = selectApplicationCommand.CreateResponseForApdu(responseApdu); SelectApplicationData = response.GetData(); } - public TResponse SendCommand(IYubiKeyCommand yubiKeyCommand) where TResponse : IYubiKeyResponse + public virtual TResponse SendCommand(IYubiKeyCommand yubiKeyCommand) + where TResponse : IYubiKeyResponse { using (var _ = _smartCardConnection.BeginTransaction(out bool cardWasReset)) { @@ -147,7 +194,8 @@ public TResponse SendCommand(IYubiKeyCommand yubiKeyComman var responseApdu = _apduPipeline.Invoke( yubiKeyCommand.CreateCommandApdu(), yubiKeyCommand.GetType(), - typeof(TResponse)); + typeof(TResponse) + ); return yubiKeyCommand.CreateResponseForApdu(responseApdu); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs index 4ede8850f..66a519f55 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs @@ -175,8 +175,6 @@ public YubiKeyDevice(IDevice device, IYubiKeyDeviceInfo info) _yubiKeyInfo = info; IsNfcDevice = _smartCardDevice?.IsNfcTransport() ?? false; LastActiveTransport = GetTransportIfOnlyDevice(); - - //TODO consolidate constructors.. } /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs index 705cfec8c..6694936e2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs @@ -63,6 +63,16 @@ public enum YubiKeyFeature /// The ability to communicate using Secure Channel Protocol 3 (SCP03). /// Scp03, + + /// + /// The ability to communicate using Secure Channel Protocol 3 (SCP03) for OATH credentials. + /// + Scp03Oath, + + /// + /// The ability to communicate using Secure Channel Protocol 11a/b/c (SCP11). + /// + Scp11, /// /// The YubiKey is capable of switching USB interfaces without the lengthy 3-second reclaim timeout. @@ -270,6 +280,6 @@ public enum YubiKeyFeature /// /// Allows temporarily disabling NFC until the next time the YubiKey is powered over USB. /// - ManagementNfcRestricted + ManagementNfcRestricted, } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs index 528c88dd0..4da78a2e4 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeatureExtensions.cs @@ -92,7 +92,17 @@ public static bool HasFeature(this IYubiKeyDevice yubiKeyDevice, YubiKeyFeature yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_3_0 && (HasApplication(yubiKeyDevice, YubiKeyCapabilities.Piv) || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Oath) - || HasApplication(yubiKeyDevice, YubiKeyCapabilities.OpenPgp)), + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.OpenPgp) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.Otp) + || HasApplication(yubiKeyDevice, YubiKeyCapabilities.YubiHsmAuth)), + + YubiKeyFeature.Scp03Oath => + yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_6_3 + && HasFeature(yubiKeyDevice, YubiKeyFeature.Scp03), + + YubiKeyFeature.Scp11 => + yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_7_2 + && HasFeature(yubiKeyDevice, YubiKeyFeature.Scp03), YubiKeyFeature.FastUsbReclaim => yubiKeyDevice.FirmwareVersion >= FirmwareVersion.V5_6_0, diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index cc3ab37fc..e8e196e00 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -13,17 +13,20 @@ // 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; -#pragma warning disable CS0618 // Type or member is obsolete - namespace Yubico.YubiKey.Scp { + [Trait(TraitTypes.Category, TestCategories.Simple)] public class Scp03Tests { private readonly ReadOnlyMemory _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; @@ -39,7 +42,6 @@ public Scp03Tests() session.Reset(); } - [Fact] public void TestImportKey() { @@ -49,14 +51,9 @@ public void TestImportKey() 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, }; - var newKeyParams = new Scp03KeyParameters(ScpKid.Scp03, 0x01, new StaticKeys( - sk, - sk, - sk)); - - // assumeFalse("SCP03 not supported over NFC on FIPS capable devices", todo - // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + var newKeyParams = Scp03KeyParameters.FromStaticKeys(new StaticKeys(sk, sk, sk)); + // Authenticate with default key, then replace default key using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); @@ -64,18 +61,33 @@ public void TestImportKey() using (_ = new SecurityDomainSession(Device, newKeyParams)) { - // Authentication with new key should succeed } - Assert.Throws(() => //TODO Is this the correct exception to throw? + // Default key should not work now and throw an exception + Assert.Throws(() => { using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - // Default key should not work now and throw an exception } }); } + + [Fact] + public void PutKey_WithPublicKey_Succeeds() + { + var keyReference = new KeyReference(0x10, 0x3); + + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + var publicKey = new ECPublicKeyParameters(ecdsa); + session.PutKey(keyReference, publicKey, 0); + + var keyInformation = session.GetKeyInformation(); + Assert.True(keyInformation.ContainsKey(keyReference)); + } + [Fact] public void TestDeleteKey() { @@ -97,7 +109,7 @@ public void TestDeleteKey() // Authenticate with key2, delete key 1 using (var session = new SecurityDomainSession(Device, keyRef2)) { - session.DeleteKeySet(keyRef1.KeyReference); + session.DeleteKey(keyRef1.KeyReference); } // Authenticate with key 1, @@ -111,7 +123,7 @@ public void TestDeleteKey() using (var session = new SecurityDomainSession(Device, keyRef2)) { - session.DeleteKeySet(keyRef2.KeyReference, true); + session.DeleteKey(keyRef2.KeyReference, true); } // Try to authenticate with key 2, @@ -130,46 +142,49 @@ public void TestReplaceKey() var keyRef1 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); var keyRef2 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); + // Authenticate with default key, then replace default key using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { session.PutKey(keyRef1.KeyReference, keyRef1.StaticKeys, 0); } - + // Authenticate with key1, then add additional key, keyref2 using (var session = new SecurityDomainSession(Device, keyRef1)) { session.PutKey(keyRef2.KeyReference, keyRef2.StaticKeys, keyRef1.KeyReference.VersionNumber); } + // Authentication with new key 2 should succeed using (_ = new SecurityDomainSession(Device, keyRef2)) { - // Authentication with new key should succeed } + // The ssession throws a SecureChannelException if the attempted key is incorrect -- + // But only if its not the default key. If it is the default key, it will throw an ArgumentException Assert.Throws( - () => // TODO SecureChannelException this time for some reason, but ArgumentException if I try with the DefaultKey, check google Keep + () => { using (_ = new SecurityDomainSession(Device, keyRef1)) { } }); - - using (_ = new SecurityDomainSession(Device, keyRef2)) - { - // Authentication with new key should succeed - } } [Fact] public void AuthenticateWithWrongKey_Should_ThrowException() { var incorrectKeys = RandomStaticKeys(); - var keyRef = new Scp03KeyParameters(ScpKid.Scp03, 0x01, incorrectKeys); + var keyRef = Scp03KeyParameters.FromStaticKeys(incorrectKeys); - Assert.Throws(() => new SecurityDomainSession(Device, keyRef)); + // Authentication with incorrect key should throw + Assert.Throws(() => + { + using (var session = new SecurityDomainSession(Device, keyRef)) { }; + }); + // Authentication with default key should succeed using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - // Authentication with default key should succeed + } } @@ -192,9 +207,25 @@ public void Connect_GetInformation_WithDefaultKey_Returns_DefaultKey() var response = connection.SendCommand(new GetDataCommand(TAG_KEY_INFORMATION)); var result = response.GetData(); - Assert.NotEmpty(result.ToArray()); - // Assert.Equal(4, result.Length); - // Assert.Equal(0xFF, result.ToArray().First()); + 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(4, keyInformation.Keys.Count); + Assert.Equal(0xFF, keyInformation.Keys.First().VersionNumber); } [Fact] @@ -232,17 +263,18 @@ public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); } + // Authentication with new key should succeed using (var session = new SecurityDomainSession(Device, newKeyParams)) { - // Authentication with new key should succeed session.GetKeyInformation(); } - Assert.Throws(() => //TODO Is this the correct exception to throw? + + // Default key should not work now and throw an exception + Assert.Throws(() => { using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { - // Default key should not work now and throw an exception } }); @@ -259,54 +291,72 @@ public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() } [Fact] - public void TryConnectScp_WithApplicationId_Succeeds() + public void Scp03_GetSupportedCaIdentifiers_Succeeds() { - var isValid = - Device.TryConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); - using (connection) - { - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new VerifyPinCommand(_defaultPin); - var rsp = connection.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + var result = session.GetSupportedCaIdentifiers(true, true); + Assert.NotEmpty(result); } - + [Fact] - public void TryConnectScp_WithApplication_Succeeds() + public void Scp03_GetCardRecognitionData_Succeeds() { - var isValid = - Device.TryConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); - using (connection) - { - Assert.NotNull(connection); - Assert.True(isValid); - var cmd = new VerifyPinCommand(_defaultPin); - var rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + var result = session.GetCardRecognitionData(); + Assert.True(result.Length > 0); + } + + [Fact] + public void Scp03_GetData_Succeeds() + { + using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + var result = session.GetData(0x66); // Card Data + Assert.True(result.Length > 0); + } + + [Theory] + [InlineData(StandardTestDevice.Fw5)] + public void PivSession_TryVerifyPinAndGetMetaData_Succeeds( + StandardTestDevice testDeviceType) + { + var device = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + Assert.True(device.FirmwareVersion >= FirmwareVersion.V5_3_0); + Assert.True(device.HasFeature(YubiKeyFeature.Scp03)); + + using var pivSession = new PivSession(device, Scp03KeyParameters.DefaultKey); + + var result = pivSession.TryVerifyPin( + new ReadOnlyMemory(new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }), + out _); + + Assert.True(result); + + var metadata = pivSession.GetMetadata(PivSlot.Pin)!; + Assert.Equal(3, metadata.RetryCount); } [Fact] - public void ConnectScp_WithApplication_Succeeds() + public void Device_Connect_With_Application_Succeeds() { - using IYubiKeyConnection connection = - Device.ConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey); + var device = IntegrationTestDeviceEnumeration.GetTestDevice( + Transport.SmartCard, FirmwareVersion.V5_3_0); + + using var connection = device.Connect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey); Assert.NotNull(connection); var cmd = new VerifyPinCommand(_defaultPin); - var rsp = connection.SendCommand(cmd); + var rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } [Fact] - public void ConnectScp_WithApplicationId_Succeeds() + public void Device_Connect_ApplicationId_Succeeds() { - using IYubiKeyConnection connection = Device.ConnectScp( - YubiKeyApplication.Piv.GetIso7816ApplicationId(), - Scp03KeyParameters.DefaultKey); + var device = IntegrationTestDeviceEnumeration.GetTestDevice( + Transport.SmartCard, FirmwareVersion.V5_3_0); + + using IYubiKeyConnection connection = device.Connect( + YubiKeyApplication.Piv.GetIso7816ApplicationId(), Scp03KeyParameters.DefaultKey); Assert.NotNull(connection); @@ -315,6 +365,46 @@ public void ConnectScp_WithApplicationId_Succeeds() Assert.Equal(ResponseStatus.Success, rsp.Status); } + [Fact] + public void Device_TryConnect_With_Application_Succeeds() + { + var device = IntegrationTestDeviceEnumeration.GetTestDevice( + Transport.SmartCard, FirmwareVersion.V5_3_0); + + var isValid = device.TryConnect( + YubiKeyApplication.Piv, + Scp03KeyParameters.DefaultKey, + out var connection); + + using (connection) + { + Assert.NotNull(connection); + Assert.True(isValid); + var cmd = new VerifyPinCommand(_defaultPin); + var rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + } + + [Fact] + public void Device_TryConnect_With_ApplicationId_Succeeds() + { + var device = IntegrationTestDeviceEnumeration.GetTestDevice( + Transport.SmartCard, FirmwareVersion.V5_3_0); + + var isValid = device.TryConnect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, + out var connection); + + using (connection) + { + Assert.True(isValid); + Assert.NotNull(connection); + var cmd = new VerifyPinCommand(_defaultPin); + var rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } + } + #region Helpers private static StaticKeys RandomStaticKeys() => diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 54120272d..5b15871ee 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -14,29 +14,24 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; -using Org.BouncyCastle.Math; -using Org.BouncyCastle.Math.EC; using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.Security; using Xunit; using Yubico.Core.Tlv; using Yubico.YubiKey.Cryptography; -using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; using ECCurve = System.Security.Cryptography.ECCurve; using ECPoint = System.Security.Cryptography.ECPoint; -// ReSharper disable UnusedVariable namespace Yubico.YubiKey.Scp { + [Trait(TraitTypes.Category, TestCategories.Simple)] public class Scp11Tests { private const byte OceKid = 0x010; @@ -68,7 +63,7 @@ public void Scp11b_PutKey_WithPublicKey_Succeeds() } [Fact] - public void Scp11b_Authenticate_Succeeds() // Works? No? + public void Scp11b_Authenticate_Succeeds() // Works { IReadOnlyCollection certificateList; var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); @@ -79,9 +74,9 @@ public void Scp11b_Authenticate_Succeeds() // Works? No? var leaf = certificateList.Last(); var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); - var keyParams = new Scp11KeyParameters(keyReference, ecDsaPublicKey); + var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); - // Try create authenticated session using key params and public key from yubikey + // Try to create authenticated session using key params and public key from yubikey using (var session = new SecurityDomainSession(Device, keyParams)) { var result = session.GetKeyInformation(); @@ -90,7 +85,7 @@ public void Scp11b_Authenticate_Succeeds() // Works? No? } [Fact] - public void Scp11b_Import_Succeeds() //Works + public void Scp11b_Import_Succeeds() // Works { var keyReference = new KeyReference(ScpKid.Scp11b, 0x2); var ecDsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); @@ -107,7 +102,7 @@ public void Scp11b_Import_Succeeds() //Works } [Fact] - public void GetCertificates_IsNotEmpty() //Works + public void GetCertificates_IsNotEmpty() // Works { using var session = new SecurityDomainSession(Device); @@ -118,20 +113,24 @@ public void GetCertificates_IsNotEmpty() //Works } [Fact] - public void StoreCertificates_SavesCertificatesOnYubikey() + public void Scp11b_StoreCertificates_CanBeRetrieved() { var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); - session.StoreCertificates(keyReference, GetOceCertificates(Scp11TestData.OceCerts.Span).Bundle); + 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("085C9FB32DAE995758BF0C989E29C7503411C779", result[0].Thumbprint); + Assert.Equal(oceThumbprint, result[0].Thumbprint); } [Fact] - public void GenerateEcKey_Succeeds() // Works + public void Scp11a_GenerateEcKey_Succeeds() // Works { var keyReference = new KeyReference(ScpKid.Scp11a, 0x3); @@ -144,7 +143,7 @@ public void GenerateEcKey_Succeeds() // Works // Verify the generated key Assert.NotNull(generatedKey.Parameters.Q.X); Assert.NotNull(generatedKey.Parameters.Q.Y); - Assert.Equal(32, generatedKey.Parameters.Q.X.Length); // P-256 curve should have 32-byte X and Y coordinates + Assert.Equal(32, generatedKey.Parameters.Q.X.Length); Assert.Equal(32, generatedKey.Parameters.Q.Y.Length); Assert.Equal(ECCurve.NamedCurves.nistP256.Oid.Value, generatedKey.Parameters.Curve.Oid.Value); @@ -153,42 +152,42 @@ public void GenerateEcKey_Succeeds() // Works } [Fact] - public void Scp11aAllowListTest() + public void Scp11a_WithAllowList_AllowsApprovedSerials() { - byte kvn = 0x05; + const byte kvn = 0x05; var oceKeyRef = new KeyReference(OceKid, kvn); - Scp11KeyParameters keyParams; + Scp11KeyParameters keyParams; // Means this is not containing the correct OCECerts? using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); } - var serials = new List - { - // Serial numbers from oce certs - System.Numerics.BigInteger.Parse("7f4971b0ad51f84c9da9928b2d5fef5e16b2920a", System.Globalization.NumberStyles.HexNumber), - System.Numerics.BigInteger.Parse("6b90028800909f9ffcd641346933242748fbe9ad", System.Globalization.NumberStyles.HexNumber) - }; - using (var session = new SecurityDomainSession(Device, keyParams)) { - session.StoreAllowlist(oceKeyRef, serials); // Works until here + 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(Device, keyParams)) // Test this on Android + using (var session = new SecurityDomainSession(Device, keyParams)) { - session.DeleteKeySet(new KeyReference(ScpKid.Scp11a, kvn), false); + session.DeleteKeySet(new KeyReference(ScpKid.Scp11a, kvn)); } } [Fact] - public void Scp11aAllowListBlocked() // Works + public void Scp11a_WithAllowList_BlocksUnapprovedSerials() // Works { - byte kvn = 0x03; + const byte kvn = 0x03; var oceKeyRef = new KeyReference(OceKid, kvn); - Scp03KeyParameters scp03KeyParams; using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) { @@ -206,46 +205,44 @@ public void Scp11aAllowListBlocked() // Works scp11KeyParams = LoadKeys(session, ScpKid.Scp11a, kvn); // Create list of serial numbers - var serials = new List + var serials = new List { - new System.Numerics.BigInteger(1), - new System.Numerics.BigInteger(2), - new System.Numerics.BigInteger(3), - new System.Numerics.BigInteger(4), - new System.Numerics.BigInteger(5) + "01", + "02", + "03" }; // Store the allow list session.StoreAllowlist(oceKeyRef, serials); } - // Authenticate with SCP11a should throw + // This is the test. Authenticate with SCP11a should throw. Assert.Throws(() => { using (var session = new SecurityDomainSession(Device, scp11KeyParams)) { - // ... + // ... Authenticated } }); // Reset the allow list using (var session = new SecurityDomainSession(Device, scp03KeyParams)) { - session.StoreAllowlist(oceKeyRef, new List()); + session.ClearAllowList(oceKeyRef); } - // Authenticate with SCP11a should now succeed + // Now, with the allowlist removed, authenticate with SCP11a should now succeed using (var session = new SecurityDomainSession(Device, scp11KeyParams)) { - // ... Should work + // ... Authenticated } } [Fact] public void Scp11a_Authenticate_Succeeds() // Works { - byte kvn = 0x03; - var keyReference = new KeyReference(ScpKid.Scp11a, kvn); + const byte kvn = 0x03; + var keyRef = new KeyReference(ScpKid.Scp11a, kvn); // Start authenticated session with default key Scp11KeyParameters keyParams; @@ -257,12 +254,12 @@ public void Scp11a_Authenticate_Succeeds() // Works // Start authenticated session using new key params and public key from yubikey using (var session = new SecurityDomainSession(Device, keyParams)) { - session.DeleteKeySet(keyReference, false); + session.DeleteKeySet(keyRef); } } [Fact] - public void TestScp11cAuthenticate() + public void Scp11c_Authenticate_Succeeds() { const byte kvn = 0x03; var keyReference = new KeyReference(ScpKid.Scp11c, kvn); @@ -276,11 +273,10 @@ public void TestScp11cAuthenticate() Assert.Throws(() => { using var session = new SecurityDomainSession(Device, keyParams); - session.DeleteKeySet(keyReference, false); + session.DeleteKeySet(keyReference); }); } - private Scp11KeyParameters LoadKeys( SecurityDomainSession session, byte scpKid, @@ -303,7 +299,7 @@ private Scp11KeyParameters LoadKeys( session.PutKey(oceRef, ocePublicKey, 0); // Get Oce subject key identifier - var ski = GetSki(oceCerts.Ca); //20 byte + var ski = GetSki(oceCerts.Ca); if (ski.IsEmpty) { throw new InvalidOperationException("CA certificate missing Subject Key Identifier"); @@ -312,6 +308,20 @@ private Scp11KeyParameters LoadKeys( // Store the key identifier with the referenced off card entity on the Yubikey session.StoreCaIssuer(oceRef, ski); + var (certChain, privateKey) = GetOceCertificateChainAndPrivateKey(); + + // 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() + { // Load the OCE PKCS12 using Bouncy Castle using var pkcsStream = new MemoryStream(Scp11TestData.Oce.ToArray()); var pkcs12Store = new Pkcs12Store(pkcsStream, Scp11TestData.OcePassword.ToArray()); @@ -330,31 +340,26 @@ private Scp11KeyParameters LoadKeys( throw new InvalidOperationException("Private key is not an EC key"); } - var certs = ScpCertificates.From(pkcs12Store.GetCertificateChain(alias) - .Select(certEntry => - { - var cert = DotNetUtilities.ToX509Certificate(certEntry.Certificate); - return new X509Certificate2( - cert.Export(X509ContentType.Cert), - (string)null!, // no password needed for public cert - X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet - ); - })); - + var x509CertificateEntries = pkcs12Store.GetCertificateChain(alias); + var x509Certs = x509CertificateEntries + .Select(certEntry => + { + var cert = DotNetUtilities.ToX509Certificate(certEntry.Certificate); + return new X509Certificate2( + cert.Export(X509ContentType.Cert), + (string)null!, // no password needed for public cert + X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet + ); + }); + + var certs = ScpCertificates.From(x509Certs); var certChain = new List(certs.Bundle); if (certs.Leaf != null) { certChain.Add(certs.Leaf); } - // Now we have the EC private key parameters and cert chain - return new Scp11KeyParameters( - sessionRef, - newPublicKey.Parameters, - oceRef, - ConvertToECParameters(ecPrivateKey), - certChain - ); + return (certChain, ConvertToECParameters(ecPrivateKey)); } static ECParameters ConvertToECParameters( @@ -437,12 +442,15 @@ private Memory GetSki( return tlv.Value; } - private static Scp03KeyParameters ImportScp03Key(SecurityDomainSession session) + private static Scp03KeyParameters ImportScp03Key( + SecurityDomainSession session) { // assumeFalse("SCP03 management not supported over NFC on FIPS capable devices", // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); // todo - var scp03Ref = new KeyReference((byte)0x01, (byte)0x01); + + + var scp03Ref = new KeyReference(0x01, 0x01); var staticKeys = new StaticKeys( GetRandomBytes(16), GetRandomBytes(16), @@ -454,7 +462,8 @@ private static Scp03KeyParameters ImportScp03Key(SecurityDomainSession session) return new Scp03KeyParameters(scp03Ref, staticKeys); } - private static Memory GetRandomBytes(byte length) + private static Memory GetRandomBytes( + byte length) { using var rng = CryptographyProviders.RngCreator(); Span hostChallenge = stackalloc byte[length]; diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs index 76e477b2d..2cc5508ec 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs @@ -116,7 +116,7 @@ public void AesCbcEncrypt_GivenKeyIVPlaintext_EncryptsCorrectly() byte[] iv = GetIV(); // Act - Memory 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 - Memory 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/Scp03/ChannelEncryptionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs index afff39bc5..84d05794a 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs @@ -46,7 +46,7 @@ public void EncryptData_GivenCorrectKeyPayload_ReturnsCorrectly() int ec = GetEncryptionCounter(); // Act - Memory output = ChannelEncryption.EncryptData(payload, key, ec); + ReadOnlyMemory output = ChannelEncryption.EncryptData(payload, key, ec); // Assert Assert.Equal(GetCorrectEncryptOutput(), output); @@ -77,7 +77,7 @@ public void DecryptData_GivenCorrectKeyPayload_ReturnsCorrectly() int ec = 1; // Act - Memory output = ChannelEncryption.DecryptData(payload, key, ec); + ReadOnlyMemory output = ChannelEncryption.DecryptData(payload, key, ec); // Assert Assert.Equal(GetCorrectDecryptedOutput(), output); From 093f919332d8637aec9745c76e816ac1f479cbd0 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Mon, 11 Nov 2024 17:19:32 +0100 Subject: [PATCH 07/53] tests, misc: added and fixed tests, misc updates --- Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs | 112 ++--- Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs | 27 +- .../{TlvObjectTest.cs => TlvObjectTests.cs} | 41 +- Yubico.YubiKey/src/Yubico.YubiKey.csproj | 5 +- .../src/Yubico/YubiKey/ApplicationSession.cs | 2 +- .../src/Yubico/YubiKey/ConnectionFactory.cs | 15 +- .../YubiKey/Cryptography/AesUtilities.cs | 16 +- .../src/Yubico/YubiKey/FirmwareVersion.cs | 2 + .../src/Yubico/YubiKey/IApduTransform.cs | 2 +- .../Yubico/YubiKey/IScp03YubiKeyConnection.cs | 4 +- .../src/Yubico/YubiKey/IYubiKeyConnection.cs | 4 +- .../YubiKey/Oath/OathSession.Password.cs | 8 +- .../src/Yubico/YubiKey/Oath/OathSession.cs | 57 +-- .../src/Yubico/YubiKey/Otp/OtpSettings.cs | 2 +- .../Pipelines/CommandChainingTransform.cs | 2 +- .../OathResponseChainingTransform.cs | 5 +- .../Pipelines/ResponseChainingTransform.cs | 4 +- .../YubiKey/Pipelines/Scp03ApduTransform.cs | 4 +- .../Yubico/YubiKey/Piv/PivSession.Crypto.cs | 2 +- .../src/Yubico/YubiKey/Piv/PivSession.cs | 200 +++------ .../Commands/ExternalAuthenticateResponse.cs | 2 +- .../Scp/Commands/InitializeUpdateCommand.cs | 2 +- .../YubiKey/Scp/Commands/PutKeyCommand.cs | 4 +- .../Scp/InternalAuthenticateCommand.cs | 4 +- .../src/Yubico/YubiKey/Scp/KeyReference.cs | 2 +- .../src/Yubico/YubiKey/Scp/Padding.cs | 6 +- .../Yubico/YubiKey/Scp/Scp03KeyParameters.cs | 25 +- .../src/Yubico/YubiKey/Scp/Scp03State.cs | 16 +- .../Yubico/YubiKey/Scp/Scp11KeyParameters.cs | 68 +-- .../src/Yubico/YubiKey/Scp/Scp11State.cs | 44 +- .../src/Yubico/YubiKey/Scp/ScpKeyIds.cs | 68 +++ .../Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs | 58 --- .../YubiKey/Scp/SecurityDomainSession.cs | 401 +++++++++--------- .../src/Yubico/YubiKey/Scp/SessionKeys.cs | 8 +- .../src/Yubico/YubiKey/Scp/StaticKeys.cs | 234 ++++++++++ .../src/Yubico/YubiKey/Scp03/ChannelMac.cs | 2 +- .../Yubico/YubiKey/Scp03/Scp03Connection.cs | 8 +- .../src/Yubico/YubiKey/Scp03/Scp03Session.cs | 2 +- .../YubiKey/Scp03/Scp03YubiKeyDevice.cs | 3 +- .../YubiKey/Scp03/SecureChannelException.cs | 2 +- .../src/Yubico/YubiKey/Scp03/StaticKeys.cs | 14 +- .../src/Yubico/YubiKey/SmartCardConnection.cs | 10 +- .../src/Yubico/YubiKey/YubiKeyApplication.cs | 50 +-- .../Yubico/YubiKey/YubiKeyDevice.Instance.cs | 226 +++++----- .../Yubico/YubiKey/YubiKeyDeviceExtensions.cs | 122 ------ .../src/Yubico/YubiKey/YubiKeyDeviceInfo.cs | 2 +- .../src/Yubico/YubiKey/YubiKeyFeature.cs | 2 +- .../YubiKey/Oath/OathSessionPasswordTests.cs | 150 ++++--- .../Yubico/YubiKey/Oath/OathSessionTests.cs | 4 +- .../YubiKey/Otp/ConfigureStaticTests.cs | 3 +- .../Yubico/YubiKey/Otp/OtpSessionTests.cs | 83 ++++ .../Yubico/YubiKey/Piv/GenerateTests.cs | 8 +- .../Yubico/YubiKey/Piv/GetPutDataTests.cs | 192 ++++----- .../Yubico/YubiKey/Piv/SignTests.cs | 6 +- .../Commands/DeleteKeyCommandTests.cs | 105 +++-- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 314 +++++++------- .../Yubico/YubiKey/Scp/Scp11TestData.cs | 79 ++++ .../Yubico/YubiKey/Scp/Scp11Tests.cs | 386 +++++++++-------- .../Yubico/YubiKey/Scp/ScpCertificates.cs | 16 +- .../Scp03/Commands/PutKeyCommandTests.cs | 158 ------- .../Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs | 157 ------- .../YubiKey/Scp03/SimpleSessionTests.cs | 139 ------ .../tests/sandbox/Plugins/Scp03Plugin.cs | 6 +- .../Yubico/YubiKey/Fido2/Fido2SessionTests.cs | 8 +- .../CommandChainingTransformTests.cs | 14 +- .../OathResponseChainingTransformTests.cs | 20 +- .../ResponseChainingTransformTests.cs | 20 +- .../Pipelines/Scp03ApduTransformTests.cs | 29 +- .../{Scp03 => Scp}/ChannelEncryptionTests.cs | 0 .../YubiKey/{Scp03 => Scp}/ChannelMacTests.cs | 0 .../Commands/Scp03ResponseTests.cs | 14 +- .../YubiKey/{Scp03 => Scp}/DerivationTests.cs | 0 .../YubiKey/{Scp03 => Scp}/PaddingTests.cs | 0 .../YubiKey/{Scp03 => Scp}/StaticKeysTests.cs | 2 +- .../ExternalAuthenticateCommandTests.cs | 80 ---- .../ExternalAuthenticateResponseTests.cs | 58 --- .../Commands/InitializeUpdateCommandTests.cs | 83 ---- .../Commands/InitializeUpdateResponseTests.cs | 112 ----- .../unit/Yubico/YubiKey/Scp03/SessionTests.cs | 126 ------ .../YubiKey/YubiKeyDeviceExtensionsTests.cs | 62 --- .../Yubico/YubiKey/YubikeyApplicationTests.cs | 20 +- .../YubiKey/TestUtilities/HollowConnection.cs | 2 +- .../TestUtilities/HollowYubiKeyDevice.cs | 34 +- .../TestUtilities/StandardTestDevice.cs | 10 - 84 files changed, 1824 insertions(+), 2575 deletions(-) rename Yubico.Core/tests/Yubico/Core/Tlv/{TlvObjectTest.cs => TlvObjectTests.cs} (88%) create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyIds.cs delete mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs delete mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs create mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/OtpSessionTests.cs rename Yubico.YubiKey/tests/integration/Yubico/YubiKey/{Scp03 => Scp}/Commands/DeleteKeyCommandTests.cs (52%) create mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11TestData.cs delete mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs delete mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs delete mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/{Scp03 => Scp}/ChannelEncryptionTests.cs (100%) rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/{Scp03 => Scp}/ChannelMacTests.cs (100%) rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/{Scp03 => Scp}/Commands/Scp03ResponseTests.cs (84%) rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/{Scp03 => Scp}/DerivationTests.cs (100%) rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/{Scp03 => Scp}/PaddingTests.cs (100%) rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/{Scp03 => Scp}/StaticKeysTests.cs (98%) delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubiKeyDeviceExtensionsTests.cs diff --git a/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs index f15b19306..a8aac8319 100644 --- a/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs @@ -24,6 +24,21 @@ namespace Yubico.Core.Tlv /// 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; @@ -32,52 +47,42 @@ public class TlvObject /// public TlvObject(int tag, ReadOnlySpan value) { + if (tag < 0 || tag > 0xFFFF) + { + throw new TlvException(ExceptionMessages.TlvUnsupportedTag); + } + Tag = tag; - byte[] buffer = value.ToArray(); - using var stream = new MemoryStream(); + byte[] valueBuffer = value.ToArray(); + using var ms = new MemoryStream(); - byte[] tagBytes = BitConverter.GetBytes(tag).Reverse().SkipWhile(b => b == 0).ToArray(); //TODO check - stream.Write(tagBytes, 0, tagBytes.Length); + byte[] tagBytes = BitConverter.GetBytes(tag).Reverse().SkipWhile(b => b == 0).ToArray(); + ms.Write(tagBytes, 0, tagBytes.Length); - Length = buffer.Length; + Length = valueBuffer.Length; if (Length < 0x80) { - stream.WriteByte((byte)Length); + ms.WriteByte((byte)Length); } else { byte[] lnBytes = BitConverter.GetBytes(Length).Reverse().SkipWhile(b => b == 0).ToArray(); - stream.WriteByte((byte)(0x80 | lnBytes.Length)); - stream.Write(lnBytes, 0, lnBytes.Length); + ms.WriteByte((byte)(0x80 | lnBytes.Length)); + ms.Write(lnBytes, 0, lnBytes.Length); } - _offset = (int)stream.Position; + _offset = (int)ms.Position; - stream.Write(buffer, 0, Length); + ms.Write(valueBuffer, 0, Length); - _bytes = stream.ToArray(); + _bytes = ms.ToArray(); } - /// - /// 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; } - /// /// Returns a copy ofthe Tlv as a BER-TLV encoded byte array. /// public Memory GetBytes() => _bytes.ToArray(); - + /// /// Parse a Tlv from a BER-TLV encoded byte array. /// @@ -88,47 +93,50 @@ public static TlvObject Parse(ReadOnlySpan data) ReadOnlySpan buffer = data; return ParseFrom(ref buffer); } - - /// - /// Parse a Tlv from a BER-TLV encoded byte array. - /// - /// A byte array containing the TLV encoded data. - /// The offset in data where the TLV data begins. - /// The length of the TLV encoded data. - /// The parsed Tlv - public static TlvObject Parse(ReadOnlySpan data, int offset, int length) => Parse(data.Slice(offset, length)); /// - /// Parse a Tlv from a BER-TLV encoded byte array. + /// 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. - /// The method will not throw any exceptions if the buffer is too short to parse the TLV. /// - public static TlvObject ParseFrom(ref ReadOnlySpan buffer) + /// 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]; - buffer = buffer[1..]; - if ((tag & 0x1F) == 0x1F) // Long form tag + + // Determine if the tag is in long form. + // Long form tags have more than one byte, starting with 0x1F. + if ((buffer[0] & 0x1F) == 0x1F) { - do + // Ensure there is enough data for a long form tag. + if (buffer.Length < 2) { - tag = (tag << 8) | buffer[0]; - buffer = buffer[1..]; - } while ((tag & 0x80) == 0x80); + 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 (length == 0x80) - { - throw new ArgumentException("Indefinite length not supported"); - } - + // If the length is more than one byte, process remaining bytes. if (length > 0x80) { int lengthLn = length - 0x80; @@ -141,11 +149,11 @@ public static TlvObject ParseFrom(ref ReadOnlySpan buffer) } ReadOnlySpan value = buffer[..length]; - buffer = 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. /// diff --git a/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs index af5249ead..aa75d1d23 100644 --- a/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs @@ -23,6 +23,7 @@ public static IReadOnlyList DecodeList(ReadOnlySpan data) var tlv = TlvObject.ParseFrom(ref buffer); tlvs.Add(tlv); } + return tlvs.AsReadOnly(); } @@ -41,9 +42,10 @@ public static IReadOnlyDictionary> DecodeMap(ReadOnlyS 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. /// @@ -55,7 +57,7 @@ public static byte[] EncodeList(IEnumerable list) { throw new ArgumentNullException(nameof(list)); } - + using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream); foreach (TlvObject? tlv in list) @@ -63,6 +65,7 @@ public static byte[] EncodeList(IEnumerable list) ReadOnlyMemory bytes = tlv.GetBytes(); writer.Write(bytes.Span.ToArray()); } + return stream.ToArray(); } @@ -73,25 +76,6 @@ public static byte[] EncodeList(IEnumerable list) /// BER-TLV encoded array public static byte[] EncodeMany(params TlvObject[] tlvs) => EncodeList(tlvs); - - //Todo keep? - public static Memory EncodeMap(IReadOnlyDictionary> map) - { - if (map is null) - { - throw new ArgumentNullException(nameof(map)); - } - - using var stream = new MemoryStream(); - foreach (KeyValuePair> entry in map) - { - var tlv = new TlvObject(entry.Key, entry.Value.ToArray()); - ReadOnlyMemory bytes = tlv.GetBytes(); - stream.Write(bytes.ToArray(), 0,bytes.Length);; - } - return stream.ToArray(); - } - /// /// Decode a single TLV encoded object, returning only the value. /// @@ -106,6 +90,7 @@ public static Memory UnpackValue(int expectedTag, ReadOnlySpan tlvDa { throw new InvalidOperationException($"Expected tag: {expectedTag:X2}, got {tlv.Tag:X2}"); } + return tlv.Value.ToArray(); } } diff --git a/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs b/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTests.cs similarity index 88% rename from Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs rename to Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTests.cs index a8bfe3f2d..3f6325405 100644 --- a/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTest.cs +++ b/Yubico.Core/tests/Yubico/Core/Tlv/TlvObjectTests.cs @@ -19,7 +19,7 @@ namespace Yubico.Core.Tlv.UnitTests { - public class TlvObjectTest + public class TlvObjectTests { [Fact] public void TestDoubleByteTags() @@ -43,6 +43,23 @@ public void TestDoubleByteTags() 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() { @@ -59,7 +76,7 @@ public void TestUnwrapThrowsException() { Assert.Throws(() => TlvObjects.UnpackValue(0x7F48, new byte[] { 0x7F, 0x49, 0 })); } - + [Fact] public void DecodeList_ValidInput_ReturnsCorrectTlvs() { @@ -121,26 +138,6 @@ public void EncodeList_EmptyInput_ReturnsEmptyArray() Assert.Empty(result.ToArray()); } - [Fact] - public void EncodeMap_ValidInput_ReturnsCorrectBytes() - { - var map = new Dictionary> - { - { 0x01, new byte[] { 0xFF } }, - { 0x02, new byte[] { 0xAA, 0xBB } } - }; - - var result = TlvObjects.EncodeMap(map); - Assert.Equal(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x02, 0xAA, 0xBB }, result.ToArray()); - } - - [Fact] - public void EncodeMap_EmptyInput_ReturnsEmptyArray() - { - var result = TlvObjects.EncodeMap(new Dictionary>()); - Assert.Empty(result.ToArray()); - } - [Fact] public void UnpackValue_CorrectTag_ReturnsValue() { diff --git a/Yubico.YubiKey/src/Yubico.YubiKey.csproj b/Yubico.YubiKey/src/Yubico.YubiKey.csproj index dbb191916..edba741b4 100644 --- a/Yubico.YubiKey/src/Yubico.YubiKey.csproj +++ b/Yubico.YubiKey/src/Yubico.YubiKey.csproj @@ -1,4 +1,4 @@ - OtpSettings.cs - @@ -107,8 +106,6 @@ limitations under the License. --> Yubico.NET.SDK.snk - - diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs index f0a53afed..01f521925 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs @@ -86,7 +86,7 @@ Scp11KeyParameters _ when yubiKey.HasFeature(YubiKeyFeature.Scp11) string possibleScpDescription = string.IsNullOrEmpty(scpType) ? string.Empty : $" over {scpType}"; Logger.LogInformation($"Connecting to {GetApplicationFriendlyName(application)}{possibleScpDescription}"); - var connection = keyParameters != null + var connection = keyParameters != null ? yubiKey.Connect(application, keyParameters) : yubiKey.Connect(application); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs index b907fdc0a..b4f78e3da 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs @@ -65,8 +65,7 @@ internal IScp03YubiKeyConnection CreateScpConnection(YubiKeyApplication applicat if (_smartCardDevice is null) { - string errorMessage = "No smart card interface present. Unable to establish SCP connection to YubiKey."; - throw new InvalidOperationException(errorMessage); + throw new InvalidOperationException("No smart card interface present. Unable to establish SCP connection to YubiKey."); } _log.LogInformation("Connecting via the SmartCard interface using SCP03."); @@ -92,8 +91,7 @@ public IScpYubiKeyConnection CreateScpConnection(YubiKeyApplication application, if (_smartCardDevice is null) { - string errorMessage = "No smart card interface present. Unable to establish SCP connection to YubiKey."; - throw new InvalidOperationException(errorMessage); + throw new InvalidOperationException("No smart card interface present. Unable to establish SCP connection to YubiKey."); } _log.LogInformation("Connecting via the SmartCard interface using SCP03."); @@ -117,7 +115,7 @@ public IYubiKeyConnection CreateConnection(YubiKeyApplication application) if (_smartCardDevice != null) { _log.LogInformation("Connecting via the SmartCard interface."); - + WaitForReclaimTimeout(Transport.SmartCard); return new SmartCardConnection(_smartCardDevice, application); } @@ -125,7 +123,7 @@ public IYubiKeyConnection CreateConnection(YubiKeyApplication application) if (_hidKeyboardDevice != null && application == YubiKeyApplication.Otp) { _log.LogInformation("Connecting via the Keyboard interface."); - + WaitForReclaimTimeout(Transport.HidKeyboard); return new KeyboardConnection(_hidKeyboardDevice); } @@ -133,13 +131,12 @@ public IYubiKeyConnection CreateConnection(YubiKeyApplication application) if (_hidFidoDevice != null && (application == YubiKeyApplication.Fido2 || application == YubiKeyApplication.FidoU2f)) { _log.LogInformation("Connecting via the FIDO interface."); - + WaitForReclaimTimeout(Transport.HidFido); return new FidoConnection(_hidFidoDevice); } - string errorMessage = "No suitable interface present. Unable to establish connection to YubiKey."; - throw new InvalidOperationException(errorMessage); + throw new InvalidOperationException("No suitable interface present. Unable to establish connection to YubiKey."); } // This function handles waiting for the reclaim timeout on the YubiKey to elapse. The reclaim timeout requires diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs index d22905fd2..132173565 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs @@ -51,22 +51,22 @@ public static byte[] BlockCipher(ReadOnlySpan encryptionKey, ReadOnlySpan< using var aesObj = CryptographyProviders.AesCreator(); byte[] aesObjKey = encryptionKey.ToArray(); - #pragma warning disable CA5358 // Allow the usage of cipher mode 'ECB' +#pragma warning disable CA5358 // Allow the usage of cipher mode 'ECB' aesObj.Mode = CipherMode.ECB; - #pragma warning restore CA5358 +#pragma warning restore CA5358 aesObj.KeySize = BlockSizeBits; aesObj.BlockSize = BlockSizeBits; aesObj.Key = aesObjKey; aesObj.IV = new byte[BlockSizeBytes]; aesObj.Padding = PaddingMode.None; - #pragma warning disable CA5401 // Justification: Allow the symmetric encryption to use +#pragma warning disable CA5401 // Justification: Allow the symmetric encryption to use // a non-default initialization vector var encryptor = aesObj.CreateEncryptor(); - #pragma warning restore CA5401 - +#pragma warning restore CA5401 + using var msEncrypt = new MemoryStream(); using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); - + csEncrypt.Write(plaintext.ToArray(), 0, plaintext.Length); byte[] ciphertext = msEncrypt.ToArray(); @@ -184,7 +184,7 @@ public static ReadOnlyMemory AesCbcDecrypt(ReadOnlySpan key, ReadOnl aesObj.Mode = CipherMode.CBC; aesObj.KeySize = BlockSizeBits; aesObj.BlockSize = BlockSizeBits; - + aesObj.Key = aesObjKey; aesObj.IV = aesObjIv; aesObj.Padding = PaddingMode.None; @@ -194,7 +194,7 @@ public static ReadOnlyMemory AesCbcDecrypt(ReadOnlySpan key, ReadOnl using var msDecrypt = new MemoryStream(); using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write); csDecrypt.Write(ciphertext.ToArray(), 0, ciphertext.Length); - + byte[] plaintext = msDecrypt.ToArray(); return plaintext; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs b/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs index b6fe55228..24e0ee42a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs @@ -44,6 +44,8 @@ public class FirmwareVersion : IComparable, IComparable, IEquat internal static readonly FirmwareVersion V5_4_3 = new FirmwareVersion(5, 4, 3); internal static readonly FirmwareVersion V5_6_0 = new FirmwareVersion(5, 6, 0); internal static readonly FirmwareVersion V5_7_0 = new FirmwareVersion(5, 7, 0); + internal static readonly FirmwareVersion V5_7_2 = new FirmwareVersion(5, 7, 2); // Scp11 + #endregion public byte Major { get; set; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs index 189859e7a..a4eb849df 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs @@ -28,7 +28,7 @@ public interface IApduTransform /// /// Passes the supplied command into the pipeline, and returns the final response. /// - ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType); + ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType, bool encrypt = false); /// /// Sets up the pipeline; should be called only once, before any `Invoke` calls. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs index 60a50b59b..ed4385948 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IScp03YubiKeyConnection.cs @@ -13,8 +13,6 @@ // limitations under the License. using System; -using Yubico.YubiKey.Scp; -using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey { @@ -28,6 +26,6 @@ public interface IScp03YubiKeyConnection : IYubiKeyConnection /// /// Return a reference to the SCP03 key set used to make the connection. /// - public StaticKeys GetScp03Keys(); + public Yubico.YubiKey.Scp03.StaticKeys GetScp03Keys(); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs index 5fcf08f19..760e4967c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs @@ -18,7 +18,9 @@ namespace Yubico.YubiKey { public interface IYubiKeyConnection : IDisposable { - TResponse SendCommand(IYubiKeyCommand yubiKeyCommand) where TResponse : IYubiKeyResponse; + TResponse SendCommand(IYubiKeyCommand yubiKeyCommand, bool encrypt = false) + where TResponse : IYubiKeyResponse; + /// /// An object representing the response received from the YubiKey after selecting the application. /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs index dd96dd792..03f13b193 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs @@ -52,7 +52,7 @@ public bool TryVerifyPassword() { var password = keyEntryData.GetCurrentValue(); var command = new ValidateCommand(password, _oathData); - + var response = Connection.SendCommand(command); if (response.Status == ResponseStatus.Success) { @@ -322,10 +322,10 @@ public bool TrySetPassword(ReadOnlyMemory currentPassword, ReadOnlyMemory< } } - var setPasswordCommand = new SetPasswordCommand(newPassword, _oathData); - var setPasswordResponse = Connection.SendCommand(setPasswordCommand); - if (setPasswordResponse.Status == ResponseStatus.Success) + var setPasswordResponse = Connection.SendCommand(new SetPasswordCommand(newPassword, _oathData)); + if (setPasswordResponse.Status == ResponseStatus.Success) { + using var unauthenticatedConnection = _yubiKeyDevice.Connect(YubiKeyApplication.Oath); var selectOathResponse = Connection.SendCommand(new SelectOathCommand()); _oathData = selectOathResponse.GetData(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs index 6bb2d5a5d..b70b1d0fa 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.cs @@ -25,12 +25,11 @@ namespace Yubico.YubiKey.Oath /// /// The main entry-point for all OATH related operations. /// - public sealed partial class OathSession : IDisposable + public sealed partial class OathSession : ApplicationSession { private bool _disposed; - private readonly ILogger _log = Log.GetLogger(); - private readonly IYubiKeyDevice _yubiKeyDevice; - internal OathApplicationData _oathData; + // ReSharper disable once InconsistentNaming + internal OathApplicationData _oathData; // Internal for testing /// /// Indicates whether the OATH application on the YubiKey is @@ -39,11 +38,6 @@ public sealed partial class OathSession : IDisposable /// public bool IsPasswordProtected => !_oathData.Challenge.IsEmpty; - /// - /// The object that represents the connection to the YubiKey. - /// - public IYubiKeyConnection Connection { get; private set; } - /// /// The Delegate this class will call when it needs a password to unlock the OATH application. /// @@ -61,12 +55,6 @@ public sealed partial class OathSession : IDisposable /// public Func? KeyCollector { get; set; } - // The default constructor explicitly defined. We don't want it to be used. - private OathSession() - { - throw new NotImplementedException(); - } - /// /// Create an instance of OathSession class, the object that represents /// the OATH application on the YubiKey. @@ -85,20 +73,16 @@ private OathSession() /// /// The object that represents the actual YubiKey which will perform the operations. /// - /// TODO + /// If supplied, the parameters used open the SCP connection. /// /// The yubiKey argument is null. /// /// - /// The SelectApplicationData recived from the yubiKey is null. + /// The SelectApplicationData received from the yubiKey is null. /// public OathSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Oath, keyParameters) { - _yubiKeyDevice = yubiKey ?? throw new ArgumentNullException(nameof(yubiKey)); - - Connection = keyParameters is null - ? yubiKey.Connect(YubiKeyApplication.Oath) - : yubiKey.ConnectScp(YubiKeyApplication.Oath, keyParameters); if (!(Connection.SelectApplicationData is OathApplicationData data)) { @@ -106,8 +90,6 @@ public OathSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = nul } _oathData = data; - - _disposed = false; } /// @@ -127,11 +109,10 @@ public void ResetApplication() throw new InvalidOperationException(resetOathResponse.StatusMessage); } - var selectOathResponse = Connection.SendCommand(new SelectOathCommand()); // TODO throws error: Wrong syntax + var selectOathResponse = Connection.SendCommand(new SelectOathCommand()); _oathData = selectOathResponse.GetData(); } - // Checks if the KeyCollector delegate is null private void EnsureKeyCollector() { if (KeyCollector is null) @@ -146,40 +127,20 @@ private void EnsureKeyCollector() /// /// When the OathSession object goes out of scope, this method is called. It will close the session. /// - // Note that .NET recommends a Dispose method call Dispose(true) and GC.SuppressFinalize(this). // The actual disposal is in the Dispose(bool) method. // // However, that does not apply to sealed classes. So the Dispose method will simply perform the // "closing" process, no call to Dispose(bool) or GC. - public void Dispose() + public override void Dispose() { if (_disposed) { return; } - // At the moment, there is no "close session" method. So for now, - // just connect to the management application. - // This can fail, possibly resulting in a SCardException (or other), so we wrap it in a try catch-block to complete the disposal of the PivSession - try - { - _ = Connection.SendCommand(new SelectApplicationCommand(YubiKeyApplication.Management)); - } -#pragma warning disable CA1031 - catch (Exception e) -#pragma warning restore CA1031 - { - string message = string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.SessionDisposeUnknownError, e.GetType(), e.Message); - - _log.LogWarning(message); - } - KeyCollector = null; - Connection.Dispose(); - + base.Dispose(); _disposed = true; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSettings.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSettings.cs index fe0a8a0c4..4318a6791 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSettings.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSettings.cs @@ -66,7 +66,7 @@ public YubiKeyFlags YubiKeyFlags foreach (var flag in FlagsSet.Where(k => k != Flag.None)) { var flagItem = _flagDefinitions[flag]; - + // Doing this here makes the RequiredOr check easier. var requiredOr = flagItem.RequiredOrFlags == Flag.None diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs index 1aec2bbed..745191eb3 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs @@ -67,7 +67,7 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT responseApdu = _pipeline.Invoke(partialApdu, commandType, responseType); } - return responseApdu!; // Covered by Debug.Assert above. TODO err?? + return responseApdu!; // Covered by Debug.Assert above. TODO What debug assert?? } public void Setup() => _pipeline.Setup(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/OathResponseChainingTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/OathResponseChainingTransform.cs index 98dd37413..1d21c2871 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/OathResponseChainingTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/OathResponseChainingTransform.cs @@ -24,10 +24,11 @@ internal class OathResponseChainingTransform : ResponseChainingTransform { public OathResponseChainingTransform(IApduTransform pipeline) : base(pipeline) { - } - protected override IYubiKeyCommand CreateGetResponseCommand(CommandApdu originatingCommand, short SW2) => + protected override IYubiKeyCommand CreateGetResponseCommand( + CommandApdu originatingCommand, + short SW2) => new Oath.Commands.GetResponseCommand(originatingCommand, SW2); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs index b7685f027..a69db2b7c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ResponseChainingTransform.cs @@ -47,7 +47,6 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT var responseApdu = _pipeline.Invoke(command, commandType, responseType); // Unless we see that bytes are available, there's nothing for this transform to do. - // TODO refactor away do while if (responseApdu.SW1 != SW1Constants.BytesAvailable) { return responseApdu; @@ -79,7 +78,8 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT return new ResponseApdu(tempBuffer.ToArray(), responseApdu.SW); } - protected virtual IYubiKeyCommand CreateGetResponseCommand(CommandApdu originatingCommand, short SW2) => + protected virtual IYubiKeyCommand + CreateGetResponseCommand(CommandApdu originatingCommand, short SW2) => new InterIndustry.Commands.GetResponseCommand(originatingCommand, SW2); public void Setup() => _pipeline.Setup(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/Scp03ApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/Scp03ApduTransform.cs index 5ea916efc..85a29ce7f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/Scp03ApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/Scp03ApduTransform.cs @@ -89,7 +89,7 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT { // Encode command var encodedCommand = _session.EncodeCommand(command); - + // Pass along the encoded command var response = _pipeline.Invoke(encodedCommand, commandType, responseType); @@ -98,7 +98,7 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT { return response; } - + // Decode response and return it return _session.DecodeResponse(response); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs index 2e94aae64..31a5262cf 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs @@ -386,7 +386,7 @@ public byte[] KeyAgree(byte slotNumber, PivPublicKey correspondentPublicKey) // other information. private byte[] PerformPrivateKeyOperation( byte slotNumber, IYubiKeyCommand> commandToPerform, - PivAlgorithm algorithm,string algorithmExceptionMessage) + PivAlgorithm algorithm, string algorithmExceptionMessage) { bool pinRequired = true; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs index c74a847a7..996347266 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs @@ -19,10 +19,8 @@ using Yubico.Core.Iso7816; using Yubico.Core.Logging; using Yubico.YubiKey.Cryptography; -using Yubico.YubiKey.InterIndustry.Commands; using Yubico.YubiKey.Piv.Commands; using Yubico.YubiKey.Scp; -using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey.Piv { @@ -151,57 +149,51 @@ namespace Yubico.YubiKey.Piv /// to learn how to do so. /// /// - public sealed partial class PivSession : IDisposable + public sealed partial class PivSession : ApplicationSession { - private readonly ILogger _log = Log.GetLogger(); - private readonly IYubiKeyDevice _yubiKeyDevice; private bool _disposed; - // The default constructor explicitly defined. We don't want it to be - // used. - private PivSession() - { - throw new NotImplementedException(); - } - /// /// Create an instance of PivSession, the object that represents - /// the PIV application on the YubiKey. + /// the PIV application on the YubiKey. The communication between the SDK + /// and the YubiKey will be protected by SCP03. /// /// - /// Because this class implements IDisposable, use the using - /// keyword. For example, - /// - /// IYubiKeyDevice yubiKeyToUse = SelectYubiKey(); - /// using (var piv = new PivSession(yubiKeyToUse)) - /// { - /// /* Perform PIV operations. */ - /// } - /// + /// See the User's Manual entry on + /// SCP03 for more information on + /// this communication protocol. + /// + /// Because this class implements IDisposable, use the using + /// keyword. For example, + /// + /// IYubiKeyDevice yubiKeyToUse = SelectYubiKey(); + /// // Assume you have some method that obtains the appropriate SCP03 + /// // key set. + /// using StaticKeys scp03Keys = CollectScp03Keys(); + /// using (var piv = new PivSession(yubiKeyToUse, scp03Keys)) + /// { + /// /* Perform PIV operations. */ + /// } + /// + /// /// /// /// The object that represents the actual YubiKey which will perform the /// operations. /// + /// + /// The SCP03 key set to use in establishing the connection. + /// /// /// The yubiKey argument is null. /// - public PivSession(IYubiKeyDevice yubiKey) + /// + /// This exception is thrown when unable to determine the management key type. + /// + [Obsolete("Use new Scp")] + public PivSession(IYubiKeyDevice yubiKey, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) + : this(yubiKey, scp03Keys.ConvertToScp03KeyParameters()) { - if (yubiKey is null) - { - throw new ArgumentNullException(nameof(yubiKey)); - } - - _log.LogInformation("Create a new instance of PivSession."); - - Connection = yubiKey.Connect(YubiKeyApplication.Piv); - - ResetAuthenticationStatus(); - UpdateManagementKey(yubiKey); - - _yubiKeyDevice = yubiKey; - _disposed = false; } /// @@ -232,8 +224,8 @@ public PivSession(IYubiKeyDevice yubiKey) /// The object that represents the actual YubiKey which will perform the /// operations. /// - /// - /// The SCP03 key set to use in establishing the connection. + /// + /// The SCP03 key parameters, if any, to use in establishing the SCP connection. /// /// /// The yubiKey argument is null. @@ -241,66 +233,19 @@ public PivSession(IYubiKeyDevice yubiKey) /// /// This exception is thrown when unable to determine the management key type. /// - [Obsolete("Use new Scp")] - public PivSession(IYubiKeyDevice yubiKey, StaticKeys scp03Keys) - : this(scp03Keys, yubiKey) + public PivSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Piv, keyParameters) { - } - - [Obsolete("Use new Scp")] - private PivSession(StaticKeys? scp03Keys, IYubiKeyDevice yubiKey) - { - _log.LogInformation( - "Create a new instance of PivSession" + (scp03Keys is null - ? "." - : " over SCP03")); - - if (yubiKey is null) - { - throw new ArgumentNullException(nameof(yubiKey)); - } - - Connection = scp03Keys is null - ? yubiKey.Connect(YubiKeyApplication.Piv) - : yubiKey.ConnectScp03(YubiKeyApplication.Piv, scp03Keys); - ResetAuthenticationStatus(); UpdateManagementKey(yubiKey); - - _yubiKeyDevice = yubiKey; - _disposed = false; } - - public PivSession(IYubiKeyDevice yubiKey, ScpKeyParameters keyParameters) - { - if (yubiKey is null) - { - throw new ArgumentNullException(nameof(yubiKey)); - } - - string scpType = keyParameters switch - { - Scp03KeyParameters _ => "SCP03", - Scp11KeyParameters _ => "SCP11", - _ => string.Empty - }; - _log.LogInformation($"Create a new instance of PivSession over {scpType}"); - Connection = yubiKey.ConnectScp(YubiKeyApplication.Piv, keyParameters); - - ResetAuthenticationStatus(); - UpdateManagementKey(yubiKey); - - _yubiKeyDevice = yubiKey; - _disposed = false; - } - - /// - /// The object that represents the connection to the YubiKey. Most - /// applications will ignore this, but it can be used to call Commands - /// directly. - /// - public IYubiKeyConnection Connection { get; } + // /// + // /// The object that represents the connection to the YubiKey. Most + // /// applications will ignore this, but it can be used to call Commands + // /// directly. + // /// + // public IYubiKeyConnection Connection { get; } /// /// The Delegate this class will call when it needs a PIN, PUK, or @@ -331,55 +276,25 @@ public PivSession(IYubiKeyDevice yubiKey, ScpKeyParameters keyParameters) /// session is to "un-authenticate" the management key and "un-verify" /// the PIN. /// - // Note that .NET recommends a Dispose method call Dispose(true) and // GC.SuppressFinalize(this). The actual disposal is in the // Dispose(bool) method. - // // However, that does not apply to sealed classes. // So the Dispose method will simply perform the // "closing" process, no call to Dispose(bool) or GC. - public void Dispose() + public override void Dispose() { if (_disposed) { return; } - // At the moment, there is no "close session" method. So for now, - // just connect to the management application. - // This can fail, possibly resulting in a SCardException (or other), so we wrap it in a try catch-block to complete the disposal of the PivSession - try - { - _ = Connection.SendCommand(new SelectApplicationCommand(YubiKeyApplication.Management)); - } -#pragma warning disable CA1031 - catch (Exception e) -#pragma warning restore CA1031 - { - string message = string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.SessionDisposeUnknownError, e.GetType(), e.Message); - - _log.LogWarning(message); - } - KeyCollector = null; ResetAuthenticationStatus(); - Connection.Dispose(); - + base.Dispose(); _disposed = true; } - // Reset any fields and properties related to authentication or - // verification to the initial state: not authenticated, verified, etc. - private void ResetAuthenticationStatus() - { - ManagementKeyAuthenticated = false; - ManagementKeyAuthenticationResult = AuthenticateManagementKeyResult.Unauthenticated; - PinVerified = false; - } - /// /// Get information about the specified slot. /// @@ -424,9 +339,9 @@ private void ResetAuthenticationStatus() /// public PivMetadata GetMetadata(byte slotNumber) { - _log.LogInformation("GetMetadata for slot number {SlotNumber:X2}.", slotNumber); + Logger.LogInformation("GetMetadata for slot number {SlotNumber:X2}.", slotNumber); - if (!_yubiKeyDevice.HasFeature(YubiKeyFeature.PivMetadata)) + if (!YubiKey.HasFeature(YubiKeyFeature.PivMetadata)) { throw new NotSupportedException( string.Format( @@ -478,7 +393,7 @@ public PivMetadata GetMetadata(byte slotNumber) /// public PivBioMetadata GetBioMetadata() { - _log.LogInformation("GetBioMetadata"); + Logger.LogInformation("GetBioMetadata"); return Connection.SendCommand(new GetBioMetadataCommand()).GetData(); } @@ -525,7 +440,7 @@ public PivBioMetadata GetBioMetadata() /// public void ResetApplication() { - _log.LogInformation("Resetting the PIV application."); + Logger.LogInformation("Resetting the PIV application."); // To reset, both the PIN and PUK must be blocked. TryBlock(PivSlot.Pin); @@ -546,7 +461,7 @@ public void ResetApplication() // As resetting the PIV application resets the management key, // the management key must be updated to account for the case when the previous management key type // was not the default key type. - UpdateManagementKey(_yubiKeyDevice); + UpdateManagementKey(YubiKey); } /// @@ -577,14 +492,14 @@ public void ResetApplication() /// Thrown when the Yubikey doesn't support the Move-operation. public void MoveKey(byte sourceSlot, byte destinationSlot) { - _yubiKeyDevice.ThrowOnMissingFeature(YubiKeyFeature.PivMoveOrDeleteKey); + YubiKey.ThrowOnMissingFeature(YubiKeyFeature.PivMoveOrDeleteKey); if (!ManagementKeyAuthenticated) { AuthenticateManagementKey(); } - _log.LogDebug("Moving key from {SourceSlot} to {DestinationSlot}", sourceSlot, destinationSlot); + Logger.LogDebug("Moving key from {SourceSlot} to {DestinationSlot}", sourceSlot, destinationSlot); var command = new MoveKeyCommand(sourceSlot, destinationSlot); var response = Connection.SendCommand(command); @@ -593,7 +508,7 @@ public void MoveKey(byte sourceSlot, byte destinationSlot) throw new InvalidOperationException(response.StatusMessage); } - _log.LogInformation( + Logger.LogInformation( "Successfully moved key from {SourceSlot} to {DestinationSlot}", sourceSlot, destinationSlot); } @@ -625,14 +540,14 @@ public void MoveKey(byte sourceSlot, byte destinationSlot) /// public void DeleteKey(byte slotToClear) { - _yubiKeyDevice.ThrowOnMissingFeature(YubiKeyFeature.PivMoveOrDeleteKey); + YubiKey.ThrowOnMissingFeature(YubiKeyFeature.PivMoveOrDeleteKey); if (!ManagementKeyAuthenticated) { AuthenticateManagementKey(); } - _log.LogDebug("Deleting key at slot {TargetSlot}", slotToClear); + Logger.LogDebug("Deleting key at slot {TargetSlot}", slotToClear); var command = new DeleteKeyCommand(slotToClear); var response = Connection.SendCommand(command); @@ -650,7 +565,7 @@ public void DeleteKey(byte slotToClear) ? "Successfully deleted key at slot {targetSlot}." : "No data received from Yubikey after attempted delete on slot {targetSlot}, indicating that was likely empty to begin with."; - _log.LogInformation(logMessage, slotToClear); + Logger.LogInformation(logMessage, slotToClear); } // Block the PIN or PUK @@ -666,7 +581,7 @@ public void DeleteKey(byte slotToClear) // PivSlot.Puk, block the PUK. private bool BlockPinOrPuk(byte slotNumber) { - _log.LogInformation($"Block the {(slotNumber == 0x80 ? "PIN" : "PUK")}."); + Logger.LogInformation($"Block the {(slotNumber == 0x80 ? "PIN" : "PUK")}."); int retriesRemaining; do @@ -724,5 +639,14 @@ private PivAlgorithm GetManagementKeyAlgorithm() var metadata = response.GetData(); return metadata.Algorithm; } + + // Reset any fields and properties related to authentication or + // verification to the initial state: not authenticated, verified, etc. + private void ResetAuthenticationStatus() + { + ManagementKeyAuthenticated = false; + ManagementKeyAuthenticationResult = AuthenticateManagementKeyResult.Unauthenticated; + PinVerified = false; + } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs index 8516092ea..d8d9038e4 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs @@ -36,7 +36,7 @@ public ExternalAuthenticateResponse(ResponseApdu responseApdu) : if (responseApdu.SW != SWConstants.Success) { string message = string.Format( - CultureInfo.CurrentCulture, + CultureInfo.CurrentCulture, $"{ExceptionMessages.IncorrectExternalAuthenticateData}" + " " + $"SW: 0x{responseApdu.SW.ToString("X4", CultureInfo.InvariantCulture)}"); throw new ArgumentException(message, nameof(responseApdu)); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs index 5ef4adcd2..e1b8f3b7b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs @@ -42,7 +42,7 @@ public InitializeUpdateCommand(int keyVersionNumber, ReadOnlyMemory hostCh { throw new ArgumentException("Invalid size, must be 8 bytes", nameof(_hostChallenge)); } - + _hostChallenge = hostChallenge; _keyVersionNumber = keyVersionNumber; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs index 38de52f50..1205ea5fb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs @@ -111,8 +111,8 @@ internal class PutKeyCommand : IYubiKeyCommand private readonly byte _p1; private readonly byte _p2; - public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; - + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + public PutKeyCommand(byte p1, byte p2, ReadOnlyMemory data) { _p1 = p1; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs index 4d1d53bdd..b437f5b28 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs @@ -31,7 +31,7 @@ public InternalAuthenticateCommand(byte keyReferenceVersionNumber, byte keyRefer _keyReferenceId = keyReferenceId; _data = data; } - + public CommandApdu CreateCommandApdu() => new CommandApdu { @@ -45,7 +45,7 @@ public CommandApdu CreateCommandApdu() => public InternalAuthenticateResponse CreateResponseForApdu(ResponseApdu responseApdu) => new InternalAuthenticateResponse(responseApdu); } - + internal class InternalAuthenticateResponse : ScpResponse { public InternalAuthenticateResponse(ResponseApdu responseApdu) : base(responseApdu) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs index 1938f5fbd..472523c8e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs @@ -26,7 +26,7 @@ public class KeyReference /// The Key Id (KID) of the key. /// public byte Id { get; } - + /// /// The Key Version Number (KVN) of the key. /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs index 4653dce6f..da9699d44 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs @@ -36,10 +36,10 @@ public static Memory PadToBlockSize(ReadOnlySpan payload) Span padded = stackalloc byte[paddedLen]; payload.CopyTo(padded); padded[payload.Length] = 0x80; - + return padded.ToArray(); } - + /// /// Remove the padding from the given . /// @@ -60,7 +60,7 @@ public static Memory RemovePadding(ReadOnlySpan paddedPayload) { return paddedPayload[..i].ToArray(); } - + if (paddedPayload[i] != 0x00) { throw new SecureChannelException(ExceptionMessages.InvalidPadding); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs index f2fec4dfa..f57a0c51c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs @@ -13,7 +13,6 @@ // limitations under the License. using System; -using Yubico.YubiKey.Scp03; namespace Yubico.YubiKey.Scp { @@ -21,6 +20,16 @@ public class Scp03KeyParameters : ScpKeyParameters { public StaticKeys StaticKeys { get; } + /// + /// Creates a new instance of , representing the parameters for + /// a Secure Channel Protocol 03 (SCP03) key. + /// + /// A reference to the key. + /// The static keys shared with the device. + /// + /// Thrown if is greater than 3, which is an invalid Key ID + /// for SCP03. + /// public Scp03KeyParameters( KeyReference keyReference, StaticKeys staticKeys) : base(keyReference) @@ -33,6 +42,13 @@ public Scp03KeyParameters( StaticKeys = staticKeys; } + /// + /// Creates a new instance of , representing the parameters for + /// a Secure Channel Protocol 03 (SCP03) key. + /// + /// The ID of the key. + /// The version number of the key. + /// The static keys shared with the device. public Scp03KeyParameters( byte keyId, byte keyVersionNumber, @@ -40,6 +56,13 @@ public Scp03KeyParameters( { } + /// + /// Gets the default SCP03 key parameters using the default key identifier and static keys. + /// + /// + /// This property provides a convenient way to access default SCP03 key parameters, + /// using the standard SCP03 key identifier and default static keys. + /// public static Scp03KeyParameters DefaultKey => new Scp03KeyParameters(ScpKid.Scp03, 0xFF, new StaticKeys()); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs index f3e670a4b..0b42bbfc0 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs @@ -102,27 +102,27 @@ private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCry private void PerformExternalAuthenticate( - IApduTransform pipeline) + IApduTransform pipeline) { // Create a MAC:ed APDU - var unMacedCommand = new ExternalAuthenticateCommand(_hostCryptogram); + var eaCommandPlain = new ExternalAuthenticateCommand(_hostCryptogram); (var macdApdu, byte[] newMacChainingValue) = MacApdu( - unMacedCommand.CreateCommandApdu(), + eaCommandPlain.CreateCommandApdu(), SessionKeys.MacKey.ToArray(), MacChainingValue.ToArray() ); - // Update sessions / states MacChainingValue + // Update the states MacChainingValue MacChainingValue = newMacChainingValue; // Send command - var eaCommand = new ExternalAuthenticateCommand(macdApdu.Data.ToArray()); - var externalAuthenticateResponseApdu = pipeline.Invoke( - eaCommand.CreateCommandApdu(), + var eaCommandMaced = new ExternalAuthenticateCommand(macdApdu.Data.ToArray()); + var eaResponseApdu = pipeline.Invoke( + eaCommandMaced.CreateCommandApdu(), typeof(ExternalAuthenticateCommand), typeof(ExternalAuthenticateResponse)); - var externalAuthenticateResponse = eaCommand.CreateResponseForApdu(externalAuthenticateResponseApdu); + var externalAuthenticateResponse = eaCommandMaced.CreateResponseForApdu(eaResponseApdu); externalAuthenticateResponse.ThrowIfFailed(); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs index f6bea53fc..ab164d1bc 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs @@ -33,31 +33,31 @@ public sealed class Scp11KeyParameters : ScpKeyParameters, IDisposable private KeyReference? _oceKeyReference; private ECPublicKeyParameters _pkSdEcka; private ECPrivateKeyParameters? _skOceEcka; - private X509Certificate2[]? _certificates; + private X509Certificate2[]? _oceCertificates; private bool _disposed; /// - /// The public key of the Security Domain Elliptic Curve Key Agreement (ECKA) key. - /// pkSdEcka is short for PublicKey SecurityDomain Elliptic Curve KeyAgreement Key + /// The public key of the Security Domain Elliptic Curve Key Agreement (ECKA) key (SCP11a/b/c). Required. + /// 'pkSdEcka' is short for PublicKey SecurityDomain Elliptic Curve KeyAgreement (Key) /// - public ECPublicKeyParameters PkSdEcka => _pkSdEcka; + public ECPublicKeyParameters PkSdEcka => _pkSdEcka; /// - /// The key reference of the off-card entity. Optional. - /// oceKeyReference is short for Off-Card Entity Key Reference + /// The key reference of the off-card entity (SCP11a/c). Optional. + /// 'oceKeyReference' is short for Off-Card Entity Key Reference /// public KeyReference? OceKeyReference => _oceKeyReference; /// - /// The private key of the off-card entity Elliptic Curve Key Agreement (ECKA) key. Optional. - /// skOceEcka is short for Secret Key Off-Card Entity Elliptic Curve KeyAgreement Key + /// The private key of the off-card entity Elliptic Curve Key Agreement (ECKA) key (SCP11a/c). Optional. + /// 'skOceEcka' is short for Secret Key Off-Card Entity Elliptic Curve KeyAgreement (Key) /// public ECPrivateKeyParameters? SkOceEcka => _skOceEcka; /// - /// The certificate chain for the off-card entity. This is used for SCP11a and SCP11c. Optional. //TODO Clarify which ones are for SCP11a and which ones are for SCP11c and which ones are for SCP11b + /// The certificate chain, containing the public key for the off-card entity (SCP11a/c). /// - public IReadOnlyList? Certificates => _certificates; + public IReadOnlyList? OceCertificates => _oceCertificates; /// /// Creates a new instance. @@ -75,49 +75,45 @@ public Scp11KeyParameters( ECPrivateKeyParameters? skOceEcka = null, IEnumerable? certificates = null) : base(keyReference) - { + { _pkSdEcka = pkSdEcka; _oceKeyReference = oceKeyReference; _skOceEcka = skOceEcka; - _certificates = certificates?.ToArray(); + _oceCertificates = certificates?.ToArray(); ValidateParameters(); } - /// - /// Initializes a new instance of the class - /// with the specified key reference and security domain elliptic curve key agreement key public key. - /// This is used to create SCP11B connections which are authenticated connections. - /// - /// The key reference. - /// The security domain elliptic curve key agreement key public key. - public Scp11KeyParameters(KeyReference keyReference, ECPublicKeyParameters pkSdEcka) + public Scp11KeyParameters( + KeyReference keyReference, + ECPublicKeyParameters pkSdEcka) : this(keyReference, pkSdEcka, null, null, null) { - + } + private void ValidateParameters() { switch (KeyReference.Id) { - case ScpKid.Scp11b: + case ScpKeyIds.Scp11B: if ( OceKeyReference != null || SkOceEcka != null || - Certificates?.Count > 0 + OceCertificates?.Count > 0 ) { throw new ArgumentException("Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); } break; - case ScpKid.Scp11a: - case ScpKid.Scp11c: + case ScpKeyIds.Scp11A: + case ScpKeyIds.Scp11C: if ( OceKeyReference == null || SkOceEcka == null || - Certificates?.Count == 0 + OceCertificates?.Count == 0 ) { throw new ArgumentException("Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); @@ -129,18 +125,22 @@ private void ValidateParameters() } } + + // TODO Is this needed? public void Dispose() { - if (!_disposed) + if (_disposed) { - CryptographicOperations.ZeroMemory(_skOceEcka?.Parameters.D); - _pkSdEcka = null!; - _oceKeyReference = null; - _skOceEcka = null; - _certificates = Array.Empty(); - - _disposed = true; + return; } + + CryptographicOperations.ZeroMemory(_skOceEcka?.Parameters.D); + _pkSdEcka = null!; + _oceKeyReference = null; + _skOceEcka = null; + _oceCertificates = Array.Empty(); + + _disposed = true; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index 0db6e81f1..d99c344a0 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -42,17 +42,16 @@ internal static Scp11State CreateScpState( Scp11KeyParameters keyParameters) { // Perform Security Operation, if needed (for Scp11a and Scp11c) - if (keyParameters.KeyReference.Id == ScpKid.Scp11a || keyParameters.KeyReference.Id == ScpKid.Scp11c) + if (keyParameters.KeyReference.Id == ScpKeyIds.Scp11A || keyParameters.KeyReference.Id == ScpKeyIds.Scp11C) { PerformSecurityOperation(pipeline, keyParameters); } - var securityDomainPublicKey = keyParameters.SecurityDomainEllipticCurveKeyAgreementKeyPublicKey; - var securityDomainPublicKeyCurve = securityDomainPublicKey.Curve; + var sdEckaPkParams = keyParameters.PkSdEcka.Parameters; // Generate a public and private key using the supplied curve var ekpOceEcka = CryptographyProviders.EcdhPrimitivesCreator() - .GenerateKeyPair(securityDomainPublicKeyCurve); + .GenerateKeyPair(sdEckaPkParams.Curve); // Create an encoded point of the ephemeral public key to send to the Yubikey byte[] ephemeralPublicKeyEncodedPointOceEcka = new byte[65]; @@ -78,7 +77,7 @@ internal static Scp11State CreateScpState( new TlvObject(EckaTag, ephemeralPublicKeyEncodedPointOceEcka) ); - var authenticateCommand = keyParameters.KeyReference.Id == ScpKid.Scp11b + var authenticateCommand = keyParameters.KeyReference.Id == ScpKeyIds.Scp11B ? new InternalAuthenticateCommand( keyParameters.KeyReference.VersionNumber, keyParameters.KeyReference.Id, hostAuthenticateTlvEncodedData) as IYubiKeyCommand @@ -102,14 +101,13 @@ internal static Scp11State CreateScpState( authenticateResponseTlvs[1].GetBytes().Span); // Yubikey X963KDF Receipt to match with our own X963KDF var skOceEcka = - keyParameters - .OffCardEntityEllipticCurveAgreementPrivateKey ?? // If set, we will use this for SCP11A and SCP11C. + keyParameters.SkOceEcka?.Parameters ?? // If set, we will use this for SCP11A and SCP11C. ekpOceEcka; // Otherwise, just use the newly created ephemeral key for SCP11b. var (encryptionKey, macKey, rMacKey, dekKey) = GetX963KDFKeyAgreementKeys( skOceEcka.Curve, - securityDomainPublicKey, + sdEckaPkParams, ekpOceEcka, skOceEcka, sdReceipt, @@ -235,38 +233,40 @@ private static ECParameters ExtractPublicKeyEcParameters(ReadOnlyMemory ep private static byte GetScpIdentifierByte(KeyReference keyReference) => keyReference.Id switch { - ScpKid.Scp11a => 0b01, - ScpKid.Scp11b => 0b00, - ScpKid.Scp11c => 0b11, + ScpKeyIds.Scp11A => 0b01, + ScpKeyIds.Scp11B => 0b00, + ScpKeyIds.Scp11C => 0b11, _ => throw new ArgumentException("Invalid SCP11 KID") }; private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyParameters keyParams) { // GPC v2.3 Amendment F (SCP11) v1.4 §7.5 - if (keyParams.OffCardEntityEllipticCurveAgreementPrivateKey == null) + if (keyParams.SkOceEcka == null) { throw new ArgumentNullException( - nameof(keyParams.OffCardEntityEllipticCurveAgreementPrivateKey), + nameof(keyParams.SkOceEcka), "SCP11a and SCP11c require a private key"); } - int n = keyParams.Certificates.Count - 1; - if (n < 0) + if (keyParams.OceCertificates == null || keyParams.OceCertificates.Count == 0) { throw new ArgumentException( - "SCP11a and SCP11c require a certificate chain", nameof(keyParams.Certificates)); + "SCP11a and SCP11c require a certificate chain", nameof(keyParams.OceCertificates)); } - var oceRef = keyParams.OffCardEntityKeyReference ?? new KeyReference(0, 0); + int n = keyParams.OceCertificates.Count - 1; + var oceRef = keyParams.OceKeyReference ?? new KeyReference(0, 0); for (int i = 0; i <= n; i++) { - byte[] certificates = keyParams.Certificates[i].RawData; - byte oceRefPadded = (byte)(oceRef.Id | (i < n ? 0x80 : 0x00)); // Append 0x80 if more certificates following + byte[] certificates = keyParams.OceCertificates[i].RawData; + byte oceRefInput = (byte)(oceRef.Id | (i < n + ? 0x80 + : 0x00)); // Append 0x80 if more certificates remain to be sent var securityOperationCommand = new SecurityOperationCommand( oceRef.VersionNumber, - oceRefPadded, + oceRefInput, certificates); // Send payload @@ -288,11 +288,7 @@ private static byte[] MergeArrays(params ReadOnlyMemory[] values) using var memoryStream = new MemoryStream(); foreach (var bytes in values) { -#if NETSTANDARD2_1_OR_GREATER - memoryStream.Write(bytes.Span); -#else memoryStream.Write(bytes.Span.ToArray(), 0, bytes.Length); -#endif } return memoryStream.ToArray(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyIds.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyIds.cs new file mode 100644 index 000000000..5034e741c --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyIds.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. + +namespace Yubico.YubiKey.Scp +{ + /// + /// Represents common key IDs for Secure Channel Protocol (SCP) keys. + ///
+ /// KID '10' for PK.CA-KLOC.ECDSA
+ /// KID '11' for SK.SD.ECKA used for SCP11a
+ /// KID '12' for the optional static Key-DEK used with SCP11a only
+ /// KID '13' for SK.SD.ECKA used for SCP11b
+ /// KID '14' for the optional static Key-DEK used with SCP11b only
+ /// KID '15' for SK.SD.ECKA used for SCP11c
+ /// KID '16' for the optional static Key-DEK used with SCP11c only
+ /// KID from '20' to '2F' for additional PK.CA-KLOC.ECDSA
+ ///
+ ///
+ /// See the GlobalPlatform Technology Card Specification v2.3 Amendment F §5.1 Cryptographic Keys for more information on the available KIDs. + public static class ScpKeyIds + { + /// + /// Key ID for SCP03 protocol + /// + public const byte Scp03 = 0x01; + + /// + /// Key ID '10' for the public key of the certificate authority, also known as 'PK.CA-KLOC.ECDSA'. Needs to be an ECDSA key. + /// + public const byte ScpCaPublicKey = 0x10; + + /// + /// Key ID '11' for SK.SD.ECKA used for SCP11a + /// + public const byte Scp11A = 0x11; + + /// + /// Key ID '13' for SK.SD.ECKA used for SCP11b + /// + public const byte Scp11B = 0x13; + + /// + /// Key ID '14' for the optional static Key-DEK (data encryption key) used with SCP11b only + /// + public const byte Scp11BOptionalDek = 0x14; + + /// + /// Key ID '15' for SK.SD.ECKA used for SCP11c + /// + public const byte Scp11C = 0x15; + + /// + /// Key ID '16' for the optional static Key-DEK (data encryption key) used with SCP11c only + /// + public const byte Scp11COptionalDek = 0x16; + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs deleted file mode 100644 index 46f184937..000000000 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpYubiKeyDevice.cs +++ /dev/null @@ -1,58 +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. - -namespace Yubico.YubiKey.Scp -{ - /// - /// TODO DO I EVEN NEED THIS CLASS? - /// - internal class ScpYubiKeyDevice : YubiKeyDevice - { - public ScpKeyParameters KeyParameters { get; private set; } - - public ScpYubiKeyDevice( - YubiKeyDevice device, - ScpKeyParameters keyParameters) - : base(device.GetSmartCardDevice(), - null, - null, - device) - { - KeyParameters = keyParameters; - } - - // internal override IYubiKeyConnection? Connect( - // YubiKeyApplication application, - // ScpKeyParameters scpKeys) - // { - // if (!HasSmartCard) - // { - // return null; - // } - // - // //Scp3 check - // - // if (scpKeys is Scp03KeyParameters scp03) - // { - // if (KeyParameters is Scp03KeyParameters deviceIsScp03 && // TODO Determine or set type of ScpDevice earlier? - // !deviceIsScp03.StaticKeys.AreKeysSame(scp03.StaticKeys)) - // { - // return null; - // } - // } - // - // return new ScpConnection(GetSmartCardDevice(), application, KeyParameters); - // } - } -} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 9105e7a6e..3835769da 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -26,7 +25,8 @@ using Yubico.Core.Tlv; using Yubico.YubiKey.Cryptography; using Yubico.YubiKey.Scp.Commands; -using Yubico.YubiKey.Scp03; +using DeleteKeyCommand = Yubico.YubiKey.Scp.Commands.DeleteKeyCommand; +using GetDataCommand = Yubico.YubiKey.Scp.Commands.GetDataCommand; namespace Yubico.YubiKey.Scp { @@ -89,6 +89,7 @@ namespace Yubico.YubiKey.Scp public sealed class SecurityDomainSession : ApplicationSession { #region Tags + private const byte EcKeyType = 0xF0; private const byte EcPublicKeyKeyType = 0xB0; private const byte EcPrivateKeyKeyType = 0xB1; @@ -98,12 +99,25 @@ public sealed class SecurityDomainSession : ApplicationSession private const byte KeyInformationTag = 0xE0; private const byte SerialsAllowListTag = 0x70; private const byte SerialTag = 0x93; - private const byte CardRecognitionDataTag = 0x66; + private const byte CardDataTag = 0x66; + private const byte CardRecognitionDataTag = 0x73; private const ushort CertificateStoreTag = 0xBF21; private const ushort CaKlocIdentifiersTag = 0xFF33; // Key Loading OCE Certificate private const ushort CaKlccIdentifiersTag = 0xFF34; // Key Loading Card Certificate + #endregion + /// + /// Get the encryptor to encrypt any data for an SCP command. + /// + /// + /// + /// An encryptor function that takes the plaintext as a parameter and + /// returns the encrypted data. + /// + /// + /// If the data encryption key has not been set on the session keys. + /// private EncryptDataFunc EncryptData { get @@ -163,8 +177,7 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey) /// /// /// The shared secret keys that will be used to authenticate the caller - /// and encrypt the communications. This constructor will make a deep - /// copy of the keys, it will not copy a reference to the object. //TODO Deep copy + /// and encrypt the communications. /// /// /// The yubiKey or scpKeys argument is null. @@ -177,19 +190,19 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeyPara /// /// Puts an SCP03 key set onto the YubiKey using the Security Domain. /// - /// The key reference identifying where to store the key. - /// The new SCP03 key set to store. + /// The key reference identifying where to store the key. + /// The new SCP03 key set to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the KID is not 0x01 for SCP03 key sets. /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. - public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) + public void PutKey(KeyReference keyReference, StaticKeys staticKeys, int replaceKvn) { - Logger.LogInformation("Importing SCP03 key set into KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Importing SCP03 key set into KeyReference {KeyReference}", keyReference); - if (keyRef.Id != ScpKid.Scp03) + if (keyReference.Id != ScpKeyIds.Scp03) { - throw new ArgumentException("KID must be 0x01 for SCP03 key sets"); + throw new ArgumentException("Key ID (KID) must be 0x01 for SCP03 key sets"); } using var dataStream = new MemoryStream(); @@ -198,8 +211,8 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) using var expectedKcvWriter = new BinaryWriter(expectedKcvStream); // Write KVN - dataWriter.Write(keyRef.VersionNumber); - expectedKcvWriter.Write(keyRef.VersionNumber); + dataWriter.Write(keyReference.VersionNumber); + expectedKcvWriter.Write(keyReference.VersionNumber); Span kcvInput = stackalloc byte[16]; ReadOnlySpan kvcZeroIv = stackalloc byte[16]; @@ -208,9 +221,9 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) // Process all keys foreach (var key in new[] { - newKeySet.ChannelEncryptionKey, - newKeySet.ChannelMacKey, - newKeySet.DataEncryptionKey + staticKeys.ChannelEncryptionKey, + staticKeys.ChannelMacKey, + staticKeys.DataEncryptionKey }) { // Key check value (KCV) is first 3 bytes of encrypted test vector @@ -236,37 +249,38 @@ public void PutKey(KeyReference keyRef, StaticKeys newKeySet, int replaceKvn) } ReadOnlyMemory commandData = dataStream.ToArray().AsMemory(); - byte p2 = (byte)(0x80 | keyRef.Id); // OR with 0x80 indicates that we're sending multiple keys + byte p2 = (byte)(0x80 | keyReference.Id); // OR with 0x80 indicates that we're sending multiple keys var command = new PutKeyCommand((byte)replaceKvn, p2, commandData); var response = Connection.SendCommand(command); - ThrowIfFailed(response); + response.ThrowIfFailed("Error when importing key"); var responseKcvData = response.GetData().Span; ReadOnlySpan expectedKcvData = expectedKcvStream.ToArray().AsSpan(); ValidateCheckSum(expectedKcvData, responseKcvData); - Logger.LogInformation("Successsfully put static keys for KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Successfully put static keys for Key Reference: {KeyReference}", keyReference); } /// /// Puts an ECC private key onto the YubiKey using the Security Domain. /// - /// The key reference identifying where to store the key. + /// The key reference identifying where to store the key. /// The ECC private key parameters to store. /// The key version number to replace, or 0 for a new key. - /// Thrown when the private key is not of type SECP256R1. + /// Thrown when the private key is not of type NIST P-256. /// Thrown when no secure session is established. - /// Thrown when the new key set's checksum failed to verify, or some other SCP related error - /// described in the exception message. - public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParameters, int replaceKvn) + /// Thrown when the new key set's checksum failed to verify, + /// or some other SCP related error described in the exception message. + /// + public void PutKey(KeyReference keyReference, ECPrivateKeyParameters privateKeyParameters, int replaceKvn) { - Logger.LogInformation("Importing SCP11 private key into KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Importing SCP11 private key into Key Reference: {KeyReference}", keyReference); var privateKey = privateKeyParameters.Parameters; if (privateKey.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) { - throw new ArgumentException("Private key must be of type SECP256R1"); + throw new ArgumentException("Private key must be of type NIST P-256"); } try @@ -276,7 +290,7 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet using var commandDataWriter = new BinaryWriter(commandDataStream); // Write the key version number - commandDataWriter.Write(keyRef.VersionNumber); + commandDataWriter.Write(keyReference.VersionNumber); // Convert the private key to bytes and encrypt it var privateKeyBytes = privateKey.D.AsMemory(); @@ -298,20 +312,20 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet commandDataWriter.Write((byte)0); // Create and send the command - var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, commandDataStream.ToArray()); + var command = new PutKeyCommand((byte)replaceKvn, keyReference.Id, commandDataStream.ToArray()); var response = Connection.SendCommand(command); - ThrowIfFailed(response); + response.ThrowIfFailed("Error when importing key"); // Get and validate the response var responseData = response.GetData(); - Span expectedResponseData = new[] { keyRef.VersionNumber }; + Span expectedResponseData = new[] { keyReference.VersionNumber }; ValidateCheckSum(responseData.Span, expectedResponseData); - Logger.LogInformation("Successsfully put private key for KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Successfully put private key for Key Reference: {KeyReference}", keyReference); } catch (Exception ex) { - Logger.LogError(ex, "Failed to put private key for KeyRef {KeyRef}", keyRef); + Logger.LogError(ex, "Failed to put private key for Key Reference: {KeyReference}", keyReference); throw; } } @@ -319,21 +333,21 @@ public void PutKey(KeyReference keyRef, ECPrivateKeyParameters privateKeyParamet /// /// Puts an ECC public key onto the YubiKey using the Security Domain. /// - /// The key reference identifying where to store the key. + /// The key reference identifying where to store the key. /// The ECC public key parameters to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the public key is not of type SECP256R1. /// Thrown when no secure session is established. /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. - public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameters, int replaceKvn) + public void PutKey(KeyReference keyReference, ECPublicKeyParameters publicKeyParameters, int replaceKvn) { - Logger.LogInformation("Importing SCP11 public key into KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Importing SCP11 public key into KeyReference: {KeyReference}", keyReference); var pkParams = publicKeyParameters.Parameters; if (pkParams.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) { - throw new ArgumentException("Private key must be of type SECP256R1"); + throw new ArgumentException("Private key must be of type NIST P-256"); } try @@ -342,74 +356,40 @@ public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameter using var commandDataWriter = new BinaryWriter(commandDataMs); // Write the key version number - commandDataWriter.Write(keyRef.VersionNumber); + commandDataWriter.Write(keyReference.VersionNumber); // Write the ECC public key - byte[] formatIdentifier = { 0x4 }; // Uncompressed point - var publicKeyRawData = - formatIdentifier - .Concat(pkParams.Q.X) - .Concat(pkParams.Q.Y).ToArray().AsSpan(); + var publicKeyTlvData = + new TlvObject(EcPublicKeyKeyType, publicKeyParameters.GetBytes().Span).GetBytes(); - byte[] publicKeyTlvData = new TlvObject(EcPublicKeyKeyType, publicKeyRawData).GetBytes().ToArray(); - commandDataWriter.Write(publicKeyTlvData); + commandDataWriter.Write(publicKeyTlvData.ToArray()); // Write the ECC parameters - var paramsTlv = new TlvObject(EcKeyType, new byte[] { 0 }).GetBytes(); + var paramsTlv = new TlvObject(EcKeyType, new byte[1]).GetBytes(); commandDataWriter.Write(paramsTlv.ToArray()); commandDataWriter.Write((byte)0); // Create and send the command byte[] commandData = commandDataMs.ToArray(); - var command = new PutKeyCommand((byte)replaceKvn, keyRef.Id, commandData); + var command = new PutKeyCommand((byte)replaceKvn, keyReference.Id, commandData); var response = Connection.SendCommand(command); - ThrowIfFailed(response); + response.ThrowIfFailed("Error when importing key"); // Get and validate the response var responseData = response.GetData(); - Span expectedResponseData = new[] { keyRef.VersionNumber }; + Span expectedResponseData = new[] { keyReference.VersionNumber }; ValidateCheckSum(responseData.Span, expectedResponseData); - Logger.LogInformation("Successsfully put public key for KeyRef {KeyRef}", keyRef); + Logger.LogInformation("Successfully put public key for KeyReference: {KeyReference}", keyReference); } catch (Exception ex) { - Logger.LogError(ex, "Failed to put public key for KeyRef {KeyRef}", keyRef); + Logger.LogError(ex, "Failed to put public key for KeyReference: {KeyReference}", keyReference); throw; } } - // /// - // /// Delete the key set with the given keyVersionNumber. If the key - // /// set to delete is the last SCP key set on the YubiKey, pass - // /// true as the isLastKey arg. - // /// - // /// - // /// The key set used to create the SCP session cannot be the key set to - // /// be deleted, unless both of the other key sets have been deleted, and - // /// you pass true for isLastKey. In this case, the key will - // /// be deleted but the SCP application on the YubiKey will be reset - // /// with the default key. - // /// - // /// - // /// The number specifying which key set to delete. - // /// - // /// - // /// If this key set is the last SCP key set on the YubiKey, pass - // /// true, otherwise, pass false. This arg has a default of - // /// false so if no argument is given, it will be false. - // /// - // /// Thrown when there was an SCP error, described in the exception message. - // public void DeleteKeySet(byte keyVersionNumber, bool isLastKey = false) - // { - // _log.LogInformation("Deleting an SCP key set from a YubiKey."); - // var command = new DeleteKeyCommand(keyVersionNumber, isLastKey); - // var response = Connection.SendCommand(command); - // ThrowIfFailed(response); - // _log.LogInformation("Successfully deleted {{KeyVersionNumber}}", keyVersionNumber); - // } - /// /// Delete one (or more) keys matching the specified criteria. /// @@ -417,7 +397,7 @@ public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameter /// All keys matching the given KID (Key ID) and/or KVN (Key Version Number) will be deleted, /// where 0 is treated as a wildcard. For SCP03 keys, they can only be deleted by KVN. /// - /// A reference to the key(s) to delete. + /// A reference to the key(s) to delete. /// Must be true if deleting the final key, false otherwise. /// /// Thrown when both KID and KVN are 0, or when attempting to delete SCP03 keys by KID. @@ -425,15 +405,17 @@ public void PutKey(KeyReference keyRef, ECPublicKeyParameters publicKeyParameter /// /// Thrown when the delete operation fails. /// - public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) + public void DeleteKey(KeyReference keyReference, bool deleteLast = false) { - if (keyRef.Id == 0 && keyRef.VersionNumber == 0) + if (keyReference.Id == 0 && keyReference.VersionNumber == 0) { throw new ArgumentException("At least one of KID, KVN must be nonzero"); } - byte kid = keyRef.Id; - byte kvn = keyRef.VersionNumber; + Logger.LogInformation("Deleting keys (KeyReference: {KeyReference})", keyReference); + + byte kid = keyReference.Id; + byte kvn = keyReference.VersionNumber; // Special handling for SCP03 keys (1, 2, 3) if (kid == 1 || kid == 2 || kid == 3) @@ -448,7 +430,7 @@ public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) } } - Logger.LogDebug("Deleting keys matching KeyRef {KeyRef}", keyRef); + Logger.LogDebug("Deleting keys matching keys (KeyReference: {KeyReference})", keyReference); // Build TLV list for command data var tlvList = new List(); @@ -465,10 +447,9 @@ public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) byte[] data = TlvObjects.EncodeList(tlvList); var command = new DeleteKeyCommand(data, deleteLast); var response = Connection.SendCommand(command); + response.ThrowIfFailed("Error deleting key"); - ThrowIfFailed(response); - - Logger.LogInformation("Keys deleted. KeyRef: ({KeyReference})", keyRef); + Logger.LogInformation("Keys deleted (KeyReference: {KeyReference})", keyReference); } /// @@ -479,36 +460,38 @@ public void DeleteKeySet(KeyReference keyRef, bool deleteLast = false) /// Yubico extension that tries to mimic the format of the GPC PUT KEY /// command. /// - /// The KID-KVN pair of the key that should be generated. + /// The KID-KVN pair of the key that should be generated. /// The key version number of the key set that should be replaced, or 0 to generate a new key pair. /// The parameters of the generated key, including the curve and the public point. /// Thrown when there was an SCP error, described in the exception message. - public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) + public ECPublicKeyParameters GenerateEcKey(KeyReference keyReference, byte replaceKvn) { Logger.LogInformation( - "Generating new key for {KeyRef}{ReplaceMessage}", - keyRef, + "Generating new key for {KeyReference}{ReplaceMessage}", + keyReference, replaceKvn == 0 ? string.Empty : $", replacing KVN=0x{replaceKvn:X2}"); // Create tlv data for the command - var paramsTlv = new TlvObject(EcKeyType, new byte[] { 0 }).GetBytes(); - byte[] commandData = new byte[paramsTlv.Length + 1]; - commandData[0] = keyRef.VersionNumber; - paramsTlv.CopyTo(commandData.AsMemory(1)); + var ecParamsTlv = new TlvObject(EcKeyType, new byte[1]).GetBytes(); + byte[] generateEcCommandData = new byte[ecParamsTlv.Length + 1]; + generateEcCommandData[0] = keyReference.VersionNumber; + ecParamsTlv.CopyTo(generateEcCommandData.AsMemory(1)); // Create and send the command - var command = new GenerateEcKeyCommand(replaceKvn, keyRef.Id, commandData); + var command = new GenerateEcKeyCommand(replaceKvn, keyReference.Id, generateEcCommandData); var response = Connection.SendCommand(command); - ThrowIfFailed(response); + response.ThrowIfFailed("Error generating key"); // Parse the response, extract the public point var tlvReader = new TlvReader(response.GetData()); var encodedPoint = tlvReader.ReadValue(EcPublicKeyKeyType).Span; // Create the ECParameters with the public point - var eccPublicKey = encodedPoint.CreateEcPublicKeyFromBytes(); + var eccPublicKey = encodedPoint.CreateECPublicKeyFromBytes(); + + Logger.LogInformation("Key generated (KeyReference: {KeyReference})", keyReference); return eccPublicKey; } @@ -516,61 +499,62 @@ public ECPublicKeyParameters GenerateEcKey(KeyReference keyRef, byte replaceKvn) /// Store the SKI (Subject Key Identifier) for the CA of a given key. /// Requires off-card entity verification. /// - /// A reference to the key for which to store the CA issuer. + /// A reference to the key for which to store the CA issuer. /// The Subject Key Identifier to store. - public void StoreCaIssuer(KeyReference keyRef, ReadOnlyMemory ski) + public void StoreCaIssuer(KeyReference keyReference, ReadOnlyMemory ski) { - Logger.LogDebug("Storing CA issuer SKI for {KeyRef}", keyRef); + Logger.LogDebug("Storing CA issuer SKI (KeyReference: {KeyReference})", keyReference); byte klcc = 0; // Key Loading Card Certificate - switch (keyRef.Id) + switch (keyReference.Id) { - case ScpKid.Scp11a: - case ScpKid.Scp11b: - case ScpKid.Scp11c: + case ScpKeyIds.Scp11A: + case ScpKeyIds.Scp11B: + case ScpKeyIds.Scp11C: klcc = 1; break; } // Create and serialize data - var data = new TlvObject( + var caIssuerData = new TlvObject( ControlReferenceTag, TlvObjects.EncodeList( new List { new TlvObject(0x80, new[] { klcc }), new TlvObject(0x42, ski.Span), - new TlvObject(0x83, keyRef.GetBytes) + new TlvObject(KidKvnTag, keyReference.GetBytes.Span) } )).GetBytes(); // Send store data command - StoreData(data); + StoreData(caIssuerData); - Logger.LogInformation("CA issuer SKI stored"); + Logger.LogInformation("CA issuer SKI stored (KeyReference: {KeyReference})", keyReference); } /// - /// Store a list of certificates associated with the given key reference. //TODO should document limitations of this command as well as other storedata command + /// Store a list of certificates associated with the given key reference using the GlobalPlatform STORE DATA command. /// - /// The key reference associated with the certificates. + /// The key reference associated with the certificates. /// The certificates to store. /// /// The certificates will be stored in the order they are provided in the list. + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. /// /// Thrown when certificatedata /// Thrown when there was an SCP error, described in the exception message. - public void StoreCertificates(KeyReference keyRef, IReadOnlyList certificates) + public void StoreCertificates(KeyReference keyReference, IReadOnlyList certificates) { - Logger.LogDebug("Storing certificate bundle for {KeyRef}", keyRef); + Logger.LogDebug("Storing certificate bundle (KeyReference: {KeyReference})", keyReference); // Write each certificate to a memory stream - using var certDataMs = new MemoryStream(); + using var ms = new MemoryStream(); foreach (var cert in certificates) { try { byte[] certTlvEncoded = cert.GetRawCertData(); // ASN.1 DER (TLV) encoded certificate - certDataMs.Write(certTlvEncoded, 0, certTlvEncoded.Length); + ms.Write(certTlvEncoded, 0, certTlvEncoded.Length); } catch (CryptographicException e) { @@ -580,38 +564,39 @@ public void StoreCertificates(KeyReference keyRef, IReadOnlyList certDataEncoded = TlvObjects.EncodeMany( - new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), - new TlvObject(CertificateStoreTag, certDataMs.ToArray()) + new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyReference.GetBytes.Span).GetBytes().Span), + new TlvObject(CertificateStoreTag, ms.ToArray()) ); StoreData(certDataEncoded); - Logger.LogInformation("Certificate bundle stored"); + Logger.LogInformation("Certificate bundle stored (KeyReference: {KeyReference})", keyReference); } /// - /// Stores an allowlist of certificate serial numbers for a specified key reference. + /// Stores an allowlist of certificate serial numbers for a specified key reference using the GlobalPlatform STORE DATA command. /// /// /// This method requires off-card entity verification. If an allowlist is not stored, any /// certificate signed by the CA can be used. + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. /// - /// A reference to the key for which the allowlist will be stored. + /// A reference to the key for which the allowlist will be stored. /// The list of certificate serial numbers (in hexadecimal string format) to be stored in the allowlist for the given . /// Thrown when a serial number cannot be encoded properly. /// Thrown when there was an SCP error, described in the exception message. - public void StoreAllowlist(KeyReference keyRef, IReadOnlyCollection serials) + public void StoreAllowlist(KeyReference keyReference, IReadOnlyCollection serials) { - Logger.LogDebug("Storing allow list for {KeyRef}", keyRef); + Logger.LogDebug("Storing allow list (KeyReference: {KeyReference})", keyReference); - using var serialDataMs = new MemoryStream(); + using var ms = new MemoryStream(); foreach (string? serial in serials) { try { byte[] serialAsBytes = Base16.DecodeText(serial); byte[] serialTlvEncoded = new TlvObject(SerialTag, serialAsBytes).GetBytes().ToArray(); - serialDataMs.Write(serialTlvEncoded, 0, serialTlvEncoded.Length); + ms.Write(serialTlvEncoded, 0, serialTlvEncoded.Length); } catch (CryptographicException e) { @@ -620,21 +605,21 @@ public void StoreAllowlist(KeyReference keyRef, IReadOnlyCollection seri } Memory serialsDataEncoded = TlvObjects.EncodeMany( - new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyRef.GetBytes).GetBytes().Span), - new TlvObject(SerialsAllowListTag, serialDataMs.ToArray()) + new TlvObject(ControlReferenceTag, new TlvObject(KidKvnTag, keyReference.GetBytes.Span).GetBytes().Span), + new TlvObject(SerialsAllowListTag, ms.ToArray()) ); StoreData(serialsDataEncoded); - Logger.LogInformation("Certificate bundle stored"); + Logger.LogInformation("Allow list stored (KeyReference: {KeyReference})", keyReference); } /// /// Clears the allow list for the given /// /// - /// The key reference that holds the allow list - public void ClearAllowList(KeyReference keyRef) => StoreAllowlist(keyRef, Array.Empty()); + /// The key reference that holds the allow list + public void ClearAllowList(KeyReference keyReference) => StoreAllowlist(keyReference, Array.Empty()); /// /// Stores data in the Security Domain or targeted Application on the YubiKey using the GlobalPlatform STORE DATA command. @@ -648,12 +633,8 @@ public void StoreAllowlist(KeyReference keyRef, IReadOnlyCollection seri /// - Requires BER-TLV formatted data (P1.b5-b4=10) /// - Does not provide encryption information (P1.b7-b6=00) /// - /// - /// Note that this command's behavior depends on the current security context: - /// - Outside a personalization session: Data is processed by the Security Domain - /// - During personalization (after INSTALL [for personalization]): Data is forwarded to the target Application - /// /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. + /// The makes use of this method to store data in the Security Domain. Such as the , , , and other data. /// /// /// The data to be stored, which must be formatted as BER-TLV structures according to ISO 8825. @@ -662,13 +643,13 @@ public void StoreAllowlist(KeyReference keyRef, IReadOnlyCollection seri /// Thrown when no secure connection is available or the security context is invalid. /// /// Thrown when there was an SCP error, described in the exception message. - public void StoreData(ReadOnlyMemory data) // TODO make test + public void StoreData(ReadOnlyMemory data) { Logger.LogInformation("Storing data with length:{Length}", data.Length); var command = new StoreDataCommand(data); var response = Connection.SendCommand(command); - ThrowIfFailed(response); + response.ThrowIfFailed("Error storing data"); } /// @@ -687,18 +668,21 @@ public IReadOnlyDictionary> GetKeyInformati foreach (var tlvObject in tlvList) { var value = TlvObjects.UnpackValue(0xC0, tlvObject.GetBytes().Span); - var keyRef = new KeyReference(value.Span[0], value.Span[1]); + var keyReference = 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) + var currentValue = value.Span[2..]; + while (!currentValue.IsEmpty) { - keyComponents.Add(value.Span[0], value.Span[1]); + keyComponents.Add(currentValue[0], currentValue[1]); + currentValue = currentValue[2..]; } - keyInformation.Add(keyRef, keyComponents); + keyInformation.Add(keyReference, keyComponents); } + Logger.LogInformation("Key information retrieved"); + return keyInformation; } @@ -706,20 +690,21 @@ public IReadOnlyDictionary> GetKeyInformati /// Retrieves the certificates associated with the given . /// /// The key reference for which the certificates should be retrieved. - /// A list of X.509 certificates associated with the key reference. + /// A list of X.509 certificates associated with the key reference. The leaf certificate is the last certificate in the list /// Thrown when there was an SCP error, described in the exception message. public IReadOnlyList GetCertificates(KeyReference keyReference) { - Logger.LogInformation("Getting certificates for key={KeyRef}", keyReference); + Logger.LogInformation("Getting certificates for key={KeyReference}", keyReference); var nestedTlv = new TlvObject( ControlReferenceTag, - new TlvObject(KidKvnTag, keyReference.GetBytes).GetBytes().Span + new TlvObject(KidKvnTag, keyReference.GetBytes.Span).GetBytes().Span ).GetBytes(); var certificateTlvData = GetData(CertificateStoreTag, nestedTlv); var certificateTlvList = TlvObjects.DecodeList(certificateTlvData.Span); + Logger.LogInformation("Certificates retrieved (KeyReference: {KeyReference})", keyReference); return certificateTlvList .Select(tlv => new X509Certificate2(tlv.GetBytes().ToArray())) .ToList(); @@ -733,8 +718,7 @@ public IReadOnlyList GetCertificates(KeyReference keyReference /// A dictionary of KeyReference and byte arrays representing the CA identifiers. /// Thrown when both kloc and klcc are false. /// Thrown when there was an SCP error, described in the exception message. - public IReadOnlyDictionary> - GetSupportedCaIdentifiers(bool kloc, bool klcc) // TODO make test + public IReadOnlyDictionary> GetSupportedCaIdentifiers(bool kloc, bool klcc) { if (!kloc && !klcc) { @@ -743,60 +727,75 @@ public IReadOnlyDictionary> Logger.LogDebug("Getting CA identifiers KLOC={Kloc}, KLCC={Klcc}", kloc, klcc); - var dataMs = new MemoryStream(); - + var ms = new MemoryStream(); if (kloc) { - try - { - var klocData = GetData(CaKlocIdentifiersTag); - dataMs.Write(klocData.Span.ToArray(), 0, klocData.Length); - } - catch - (SecureChannelException) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) TODO how get response status? + var response = ExecuteGetDataCommand(CaKlocIdentifiersTag); + switch (response.Status) { - // Ignore this specific exception + case ResponseStatus.Success: + var klocData = response.GetData(); + ms.Write(klocData.Span.ToArray(), 0, klocData.Length); + break; + case ResponseStatus.NoData: // A kloc might not be present + break; + default: + response.ThrowIfFailed("Error getting kloc data"); + break; } } if (klcc) { - try - { - var klccData = GetData(CaKlccIdentifiersTag); - dataMs.Write(klccData.Span.ToArray(), 0, klccData.Length); - } - catch (SecureChannelException) //when (/*e.StatusWord == SWConstants.ReferencedDataNotFound*/) TODO + var response = ExecuteGetDataCommand(CaKlccIdentifiersTag); + switch (response.Status) { - // Ignore this specific exception + case ResponseStatus.Success: + var klccData = response.GetData(); + ms.Write(klccData.Span.ToArray(), 0, klccData.Length); + break; + case ResponseStatus.NoData: // A klcc might not be present + break; + default: + response.ThrowIfFailed("Error getting klcc data"); + break; } } - var tlvs = TlvObjects.DecodeList(dataMs.ToArray()); - var identifiers = new Dictionary>(); - - var tlvsSpan = tlvs.ToArray().AsSpan(); - while (!tlvsSpan.IsEmpty) + var caIdentifiers = new Dictionary>(); + var caTlvObjects = TlvObjects.DecodeList(ms.ToArray()).ToArray().AsSpan(); + while (!caTlvObjects.IsEmpty) { - var current = tlvsSpan[0]; - var next = tlvsSpan[1]; + var caIdentifierTlv = caTlvObjects[0]; + var keyReferenceTlv = caTlvObjects[1]; - var refData = next.GetBytes().Span; - var keyRef = new KeyReference(refData[0], refData[1]); - identifiers[keyRef] = current.GetBytes(); + var keyReferenceData = keyReferenceTlv.GetBytes().Span; + var keyReference = new KeyReference(keyReferenceData[0], keyReferenceData[1]); + caIdentifiers.Add(keyReference, caIdentifierTlv.GetBytes()); - tlvsSpan = tlvsSpan[..2]; + caTlvObjects = caTlvObjects[2..]; } - return identifiers; + Logger.LogInformation("CA identifiers retrieved"); + return caIdentifiers; } - public Memory GetCardRecognitionData() // TODO Ask Dain // TODO make test + /// + /// Retrieves the card recognition data from the YubiKey device. + /// + /// The card recognition data as a byte array. + /// + /// The card recognition data is a TLV (Tag-Length-Value) encoded structure that contains information about the card. + /// See GlobalPlatform Technology Card Specification v2.3.1 §H.2 Structure of Card Recognition Data for more information. + /// + public ReadOnlyMemory GetCardRecognitionData() { - Logger.LogInformation("Getting card recognition deta"); + Logger.LogInformation("Getting card recognition data"); + + var tlvData = GetData(CardDataTag).Span; + var cardRecognitionData = TlvObjects.UnpackValue(CardRecognitionDataTag, tlvData); - var tlvData = GetData(CardRecognitionDataTag).Span; - var cardRecognitionData = TlvObjects.UnpackValue(0x73, tlvData); + Logger.LogInformation("Card recognition data retrieved"); return cardRecognitionData; } @@ -813,10 +812,12 @@ public Memory GetCardRecognitionData() // TODO Ask Dain // TODO make test /// Thrown when there was an SCP error, described in the exception message. public ReadOnlyMemory GetData(int tag, ReadOnlyMemory? data = null) { - var command = new GetDataCommand(tag, data); - var response = Connection.SendCommand(command); - ThrowIfFailed(response); + Logger.LogInformation("Getting data for tag {Tag}", tag); + + var response = ExecuteGetDataCommand(tag, data); + response.ThrowIfFailed("Error getting data"); + Logger.LogDebug("Data for tag {Tag} retrieved", tag); return response.GetData(); } @@ -830,14 +831,14 @@ public void Reset() Logger.LogInformation("Resetting all SCP keys"); var keys = GetKeyInformation().Keys; - foreach (var keyRef in keys) // Reset is done by blocking all available keys + foreach (var keyReference in keys) // Reset is done by blocking all available keys { byte ins; - var overridenKeyRef = keyRef; + var overridenKeyRef = keyReference; - switch (keyRef.Id) + switch (keyReference.Id) { - case ScpKid.Scp03: + case ScpKeyIds.Scp03: // SCP03 uses KID=0, we use KVN=0 to allow deleting the default keys // which have an invalid KVN (0xff). overridenKeyRef = new KeyReference(0, 0); @@ -846,11 +847,11 @@ public void Reset() case 0x02: case 0x03: continue; // Skip these as they are deleted by 0x01 - case ScpKid.Scp11a: - case ScpKid.Scp11c: + case ScpKeyIds.Scp11A: + case ScpKeyIds.Scp11C: ins = ExternalAuthenticateCommand.GpExternalAuthenticateIns; break; - case ScpKid.Scp11b: + case ScpKeyIds.Scp11B: ins = InternalAuthenticateCommand.GpInternalAuthenticateIns; break; default: // 0x10, 0x20-0x2F @@ -883,23 +884,17 @@ public void Reset() Logger.LogInformation("SCP keys reset"); } - private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySpan expectedResponseData) + private GetDataCommandResponse ExecuteGetDataCommand(int tag, ReadOnlyMemory? data = null) { - if (!CryptographicOperations.FixedTimeEquals(responseData, expectedResponseData)) - { - throw new SecureChannelException(ExceptionMessages.ChecksumError); - } + var command = new GetDataCommand(tag, data); + return Connection.SendCommand(command); } - private static void ThrowIfFailed(ScpResponse response) + private static void ValidateCheckSum(ReadOnlySpan responseData, ReadOnlySpan expectedResponseData) { - if (response.Status != ResponseStatus.Success) + if (!CryptographicOperations.FixedTimeEquals(responseData, expectedResponseData)) { - throw new SecureChannelException( - string.Format( - CultureInfo.CurrentCulture, - ExceptionMessages.YubiKeyOperationFailed, - response.StatusMessage)); + throw new SecureChannelException(ExceptionMessages.ChecksumError); } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs index 351e9c16b..3952ee38a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs @@ -31,7 +31,7 @@ internal class SessionKeys : IDisposable /// Gets the session RMAC key. /// public ReadOnlyMemory RmacKey => _rmacKey; - + /// /// Gets the session data encryption key. /// @@ -51,8 +51,8 @@ internal class SessionKeys : IDisposable /// The session RMAC key. /// The session data encryption key. Optional. public SessionKeys( - Memory macKey, - Memory encryptionKey, + Memory macKey, + Memory encryptionKey, Memory rmacKey, Memory dataEncryptionKey) { @@ -60,7 +60,7 @@ public SessionKeys( _encryptionKey = encryptionKey; _rmacKey = rmacKey; _dataEncryptionKey = dataEncryptionKey; - + _disposed = false; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs new file mode 100644 index 000000000..e09241507 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs @@ -0,0 +1,234 @@ +// 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.Security.Cryptography; + +namespace Yubico.YubiKey.Scp +{ + /// + /// Represents a triple of SCP03 static keys shared with the device. + /// + /// + /// See also the User's Manual entry on + /// SCP03. + /// + /// These are the three secret keys that only the device and remote user + /// know. Clients must supply these to communicate securely with a remote + /// device. + /// + /// + /// Systems often derive and assign these keys using a diversification + /// function keyed with a 'master key' and run on the 'DivData' of each + /// device. + /// + /// + public class StaticKeys : IDisposable + { + private const int KeySizeBytes = 16; + + private const byte MinimumKvnValue = 1; + private const byte MaximumKvnValue = 3; + private const byte DefaultKvnValue = 0xff; + + private byte _keyVersionNumber; + + private readonly byte[] _macKey = new byte[KeySizeBytes]; + private readonly byte[] _encKey = new byte[KeySizeBytes]; + private readonly byte[] _dekKey = new byte[KeySizeBytes]; + + private bool _disposed; + + /// + /// The number that identifies the key set. Unless specified by the + /// caller, this class will assume the Key Version Number is 1, or else + /// 255 if the default keys are used. + /// + /// + /// When the SDK makes an SCP03 Connection with a YubiKey, it will + /// specify the Key Version Number. In that way, the YubiKey will know + /// which keys to use to complete the handshake. + /// + /// A YubiKey can store up to three sets of SCP03 keys. You can think of + /// it as if the YubiKey contains three slots (1, 2, and 3) for SCP03 + /// keys. Each set (slot) is specified by a number, which the standard + /// calls the Key Version Number. On a YubiKey, the only numbers allowed + /// to be a Key Version Number are 255, 1, 2, and 3. + /// + /// + /// Most YubiKeys are manufactured with a default set of SCP03 keys in + /// slot 1. Slots 2 and 3 are empty. The initial, default set of keys in + /// slot 1 is given the Key Version Number 255. If you replace those + /// keys, the replacement must be specified as number 1. If you want to + /// add key sets to the other two slots, you must use the numbers 2 and + /// 3. Note that you cannot set the two empty slots until the initial, + /// default keys are replaced. + /// + /// + /// If the Key Version Number to use is not the value this class uses by + /// default, then set this value after constructing the object. + /// + /// + public byte KeyVersionNumber + { + get => _keyVersionNumber; + + set + { + if (value != DefaultKvnValue && (value < MinimumKvnValue || value > MaximumKvnValue)) + { + throw new ArgumentException(ExceptionMessages.InvalidScp03Kvn); + } + + _keyVersionNumber = value; + } + } + + /// + /// AES128 shared secret key used to calculate the Session-MAC key. Also + /// called the 'DMK' or 'Key-MAC' in some specifications. + /// + public ReadOnlyMemory ChannelMacKey => _macKey; + + /// + /// AES128 shared secret key used to calculate the Session-ENC key. Also + /// called the 'DAK' or 'Key-ENC' in some specifications. + /// + public ReadOnlyMemory ChannelEncryptionKey => _encKey; + + /// + /// AES128 shared secret key used to wrap secrets. Also called the 'DEK' + /// in some specifications. + /// + public ReadOnlyMemory DataEncryptionKey => _dekKey; + + /// + /// Constructs an instance given the supplied keys. This class will + /// consider these keys to be the key set with the Key Version Number of + /// 1. If the key version number should be something else, set the + /// property after calling the constructor. + /// + /// + /// This class will copy the input key data, not just a reference. You + /// can overwrite the input buffers as soon as the StaticKeys + /// object is created. + /// + /// 16-byte AES128 shared secret key + /// 16-byte AES128 shared secret key + /// 16-byte AES128 shared secret key + public StaticKeys(ReadOnlyMemory channelMacKey, + ReadOnlyMemory channelEncryptionKey, + ReadOnlyMemory dataEncryptionKey) + { + if (channelMacKey.Length != KeySizeBytes) + { + throw new ArgumentException(ExceptionMessages.IncorrectStaticKeyLength, nameof(channelMacKey)); + } + + if (channelEncryptionKey.Length != KeySizeBytes) + { + throw new ArgumentException(ExceptionMessages.IncorrectStaticKeyLength, nameof(channelEncryptionKey)); + } + + if (dataEncryptionKey.Length != KeySizeBytes) + { + throw new ArgumentException(ExceptionMessages.IncorrectStaticKeyLength, nameof(dataEncryptionKey)); + } + + SetKeys(channelMacKey, channelEncryptionKey, dataEncryptionKey); + KeyVersionNumber = 1; + + _disposed = false; + } + + /// + /// Constructs an instance using the well-known default values; using + /// these provides no security. This class will consider these keys to + /// be the key set with the Key Version Number of 255 (0xFF). + /// + public StaticKeys() + { + var DefaultKey = new ReadOnlyMemory( + new byte[] + { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f + }); + + SetKeys(DefaultKey, DefaultKey, DefaultKey); + KeyVersionNumber = 255; + + _disposed = false; + } + + private void SetKeys(ReadOnlyMemory channelMacKey, + ReadOnlyMemory channelEncryptionKey, + ReadOnlyMemory dataEncryptionKey) + { + channelMacKey.CopyTo(_macKey.AsMemory()); + channelEncryptionKey.CopyTo(_encKey.AsMemory()); + dataEncryptionKey.CopyTo(_dekKey.AsMemory()); + } + + // Get a copy (deep clone) of this object. + internal StaticKeys GetCopy() => + new StaticKeys(ChannelMacKey, ChannelEncryptionKey, DataEncryptionKey) + { + KeyVersionNumber = KeyVersionNumber + }; + + /// + /// Determine if the contents of each key is the same for both objects. + /// If so, this method will return true. + /// + public bool AreKeysSame(StaticKeys? compareKeys) + { + if (compareKeys is null) + { + return false; + } + + return + ChannelEncryptionKey.Span.SequenceEqual(compareKeys.ChannelEncryptionKey.Span) && + ChannelMacKey.Span.SequenceEqual(compareKeys.ChannelMacKey.Span) && + DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span); + } + + /// + /// Releases any unmanaged resources and overwrites any sensitive data. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases any unmanaged resources and overwrites any sensitive data. + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + CryptographicOperations.ZeroMemory(_macKey.AsSpan()); + CryptographicOperations.ZeroMemory(_encKey.AsSpan()); + CryptographicOperations.ZeroMemory(_dekKey.AsSpan()); + + _disposed = true; + } + } + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs index a048389d6..551fd174f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelMac.cs @@ -74,7 +74,7 @@ public static void VerifyRmac(byte[] response, byte[] rmacKey, byte[] macChainin cmacObj.CmacInit(rmacKey); cmacObj.CmacUpdate(macInp); cmacObj.CmacFinal(cmac); - + var calculatedRmac = cmac.AsSpan(0, 8); if (!CryptographicOperations.FixedTimeEquals(recvdRmac, calculatedRmac)) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs index 2208f22e8..c459f8338 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Connection.cs @@ -32,13 +32,13 @@ internal class Scp03Connection : SmartCardConnection, IScp03YubiKeyConnection public Scp03Connection( ISmartCardDevice smartCardDevice, YubiKeyApplication yubiKeyApplication, - StaticKeys scp03Keys) + Scp03.StaticKeys scp03Keys) : base(smartCardDevice, yubiKeyApplication, null) { _scp03ApduTransform = SetObject(yubiKeyApplication, scp03Keys); } - public Scp03Connection(ISmartCardDevice smartCardDevice, byte[] applicationId, StaticKeys scp03Keys) + public Scp03Connection(ISmartCardDevice smartCardDevice, byte[] applicationId, Scp03.StaticKeys scp03Keys) : base(smartCardDevice, YubiKeyApplication.Unknown, applicationId) { var setError = YubiKeyApplication.Unknown; @@ -56,7 +56,7 @@ public Scp03Connection(ISmartCardDevice smartCardDevice, byte[] applicationId, S private Scp03ApduTransform SetObject( YubiKeyApplication setError, - StaticKeys scp03Keys) + Yubico.YubiKey.Scp03.StaticKeys scp03Keys) { var scp03ApduTransform = new Scp03ApduTransform(GetPipeline(), scp03Keys); IApduTransform apduPipeline = scp03ApduTransform; @@ -79,7 +79,7 @@ private Scp03ApduTransform SetObject( return scp03ApduTransform; } - public StaticKeys GetScp03Keys() => _scp03ApduTransform.Scp03Keys; + public Scp03.StaticKeys GetScp03Keys() => _scp03ApduTransform.Scp03Keys; protected override void Dispose(bool disposing) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs index f985f0cf8..5613ffa8d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03Session.cs @@ -251,7 +251,7 @@ public void PutKeySet(StaticKeys newKeySet) { throw new ArgumentNullException(nameof(newKeySet)); } - + var command = new PutKeyCommand(Connection.GetScp03Keys(), newKeySet); var response = Connection.SendCommand(command); if (response.Status != ResponseStatus.Success) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs index 548f9c895..a4f427007 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Scp03YubiKeyDevice.cs @@ -17,6 +17,7 @@ namespace Yubico.YubiKey { + [Obsolete("This class is obsolete and will be removed in a future release.")] internal class Scp03YubiKeyDevice : YubiKeyDevice { public StaticKeys StaticKeys { get; private set; } @@ -27,7 +28,7 @@ public Scp03YubiKeyDevice( : base( device.GetSmartCardDevice(), null, - null, + null, device) { StaticKeys = staticKeys.GetCopy(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs index 9f4317f68..ec02a2893 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/SecureChannelException.cs @@ -20,7 +20,7 @@ namespace Yubico.YubiKey.Scp03 /// Represents errors that occur during encoding or decoding data for SCP03. ///
#pragma warning disable CA1064 // Exceptions should be public -[Obsolete("Use new ChannelEncryption instead")] + [Obsolete("Use new ChannelEncryption instead")] internal class SecureChannelException : Exception #pragma warning restore CA1064 // Exceptions should be public { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs index 63357ac94..7a903eabc 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs @@ -14,6 +14,7 @@ using System; using System.Security.Cryptography; +using Yubico.YubiKey.Scp; namespace Yubico.YubiKey.Scp03 { @@ -34,6 +35,7 @@ namespace Yubico.YubiKey.Scp03 /// device. /// /// + [Obsolete("Use new Static Keys")] public class StaticKeys : IDisposable { private const int KeySizeBytes = 16; @@ -187,6 +189,9 @@ internal StaticKeys GetCopy() => KeyVersionNumber = KeyVersionNumber }; + internal Scp03KeyParameters ConvertToScp03KeyParameters() => + new Scp03KeyParameters(ScpKeyIds.Scp03, 0xFF, ConvertFromLegacy()); + /// /// Determine if the contents of each key is the same for both objects. /// If so, this method will return true. @@ -198,12 +203,17 @@ public bool AreKeysSame(StaticKeys? compareKeys) return false; } - return - ChannelEncryptionKey.Span.SequenceEqual(compareKeys.ChannelEncryptionKey.Span) && + return ChannelEncryptionKey.Span.SequenceEqual(compareKeys.ChannelEncryptionKey.Span) && ChannelMacKey.Span.SequenceEqual(compareKeys.ChannelMacKey.Span) && DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span); } + private Scp.StaticKeys ConvertFromLegacy() => + new Scp.StaticKeys(ChannelMacKey, ChannelEncryptionKey, DataEncryptionKey) + { + // KeyVersionNumber = KeyVersionNumber + }; + /// /// Releases any unmanaged resources and overwrites any sensitive data. /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs index b364877f0..9b90fd367 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs @@ -136,10 +136,10 @@ protected void SetPipeline(IApduTransform apduPipeline) // The application is set to Oath by enum or by application id private bool IsOath => - _yubiKeyApplication == YubiKeyApplication.Oath || + _yubiKeyApplication == YubiKeyApplication.Oath || (_applicationId != null && _applicationId.SequenceEqual( - YubiKeyApplication.Oath.GetIso7816ApplicationId())); + YubiKeyApplication.Oath.GetIso7816ApplicationId())); private IApduTransform AddResponseChainingTransform(IApduTransform pipeline) => IsOath @@ -171,9 +171,9 @@ private void SelectApplication() CultureInfo.CurrentCulture, ExceptionMessages.SmartCardPipelineSetupFailed, responseApdu.SW)) - { - SW = responseApdu.SW - }; + { + SW = responseApdu.SW + }; } // Set the instance property SelectApplicationData diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs index 4ecab241e..466d5ad75 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs @@ -14,8 +14,7 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using Yubico.Core.Buffers; namespace Yubico.YubiKey { @@ -32,7 +31,7 @@ public enum YubiKeyApplication InterIndustry = 8, OtpNdef = 9, YubiHsmAuth = 10, - Scp03 = 11, + Scp03 = 11, // TODO This should be removed in future SecurityDomain = 12 } @@ -47,29 +46,13 @@ internal static class YubiKeyApplicationExtensions private static readonly byte[] PivAppId = { 0xa0, 0x00, 0x00, 0x03, 0x08 }; private static readonly byte[] OtpNdef = { 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; private static readonly byte[] YubiHsmAuthId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01 }; - private static readonly byte[] Scp03AuthId = { 0xA0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; private static readonly byte[] SecurityDomainAppId = { 0xa0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; - public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application) => - application switch - { - YubiKeyApplication.Management => ManagementAppId, - YubiKeyApplication.Otp => OtpAppId, - YubiKeyApplication.FidoU2f => FidoU2fAppId, - YubiKeyApplication.Fido2 => Fido2AppId, - YubiKeyApplication.Oath => OathAppId, - YubiKeyApplication.OpenPgp => OpenPgpAppId, - YubiKeyApplication.Piv => PivAppId, - YubiKeyApplication.OtpNdef => OtpNdef, - YubiKeyApplication.YubiHsmAuth => YubiHsmAuthId, - YubiKeyApplication.Scp03 => Scp03AuthId, - YubiKeyApplication.SecurityDomain => SecurityDomainAppId, + public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application) => Iso7816ApplicationIds.ContainsKey(application) + ? Iso7816ApplicationIds[application].ToArray() + : throw new NotSupportedException(ExceptionMessages.ApplicationIdNotFound); - _ => throw new NotSupportedException(ExceptionMessages.ApplicationIdNotFound), - }; - - public static ReadOnlyDictionary> ApplicationIds => - new ReadOnlyDictionary>( + public static IReadOnlyDictionary> Iso7816ApplicationIds => new Dictionary> { { YubiKeyApplication.Management, ManagementAppId }, @@ -81,13 +64,19 @@ public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application { YubiKeyApplication.Piv, PivAppId }, { YubiKeyApplication.OtpNdef, OtpNdef }, { YubiKeyApplication.YubiHsmAuth, YubiHsmAuthId }, - { YubiKeyApplication.Scp03, Scp03AuthId }, + { YubiKeyApplication.Scp03, SecurityDomainAppId }, // Todo check if it can be removed non breaking { YubiKeyApplication.SecurityDomain, SecurityDomainAppId } - }); - - public static YubiKeyApplication GetById(ReadOnlySpan applicationId) + }; + + /// + /// Gets the associated with the given applicationId. + /// + /// The application id as a byte array. + /// The associated . + /// No YubiKey application found with the given application id. + public static YubiKeyApplication GetYubiKeyApplication(this ReadOnlySpan applicationId) { - foreach (var kvp in ApplicationIds) + foreach (var kvp in Iso7816ApplicationIds) { if (kvp.Value.Span.SequenceEqual(applicationId)) { @@ -95,7 +84,10 @@ public static YubiKeyApplication GetById(ReadOnlySpan applicationId) } } - throw new ArgumentException(nameof(applicationId)); //TODO better exp + throw new ArgumentException($"No YubiKey application found with application id: {Base16.EncodeBytes(applicationId)}", nameof(applicationId)); } + + /// + public static YubiKeyApplication GetYubiKeyApplication(this byte[] applicationId) => GetYubiKeyApplication(applicationId.AsSpan()); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs index 66a519f55..093b7254d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Instance.cs @@ -22,7 +22,6 @@ using Yubico.Core.Logging; using Yubico.YubiKey.DeviceExtensions; using Yubico.YubiKey.Scp; -using Yubico.YubiKey.Scp03; using MgmtCmd = Yubico.YubiKey.Management.Commands; namespace Yubico.YubiKey @@ -106,7 +105,7 @@ public partial class YubiKeyDevice : IYubiKeyDevice internal bool IsNfcDevice { get; private set; } internal Transport LastActiveTransport; internal ISmartCardDevice GetSmartCardDevice() => _smartCardDevice!; - + private ISmartCardDevice? _smartCardDevice; private IHidDevice? _hidFidoDevice; private IHidDevice? _hidKeyboardDevice; @@ -118,7 +117,6 @@ public partial class YubiKeyDevice : IYubiKeyDevice private readonly ILogger _log = Log.GetLogger(); - /// public Transport AvailableTransports { @@ -193,6 +191,7 @@ public YubiKeyDevice( _smartCardDevice = smartCardDevice; _hidFidoDevice = hidFidoDevice; _hidKeyboardDevice = hidKeyboardDevice; + _yubiKeyInfo = yubiKeyDeviceInfo; IsNfcDevice = smartCardDevice?.IsNfcTransport() ?? false; LastActiveTransport = GetTransportIfOnlyDevice(); // Must be after setting the three device fields. @@ -208,7 +207,7 @@ public YubiKeyDevice( /// The device does not have the same ParentDeviceId, or /// The device is not of a recognizable type. /// - public void Merge(IDevice device) // TODO Consider INTERNAL + internal void Merge(IDevice device) { if (!((IYubiKeyDevice)this).HasSameParentDevice(device)) { @@ -223,7 +222,7 @@ public void Merge(IDevice device) // TODO Consider INTERNAL /// /// /// - public void Merge(IDevice device, IYubiKeyDeviceInfo info) // TODO Consider INTERNAL + internal void Merge(IDevice device, IYubiKeyDeviceInfo info) { // First merge the devices MergeDevice(device); @@ -239,152 +238,71 @@ public void Merge(IDevice device, IYubiKeyDeviceInfo info) // TODO Consider INTE } } - #region obsoletewip /// - [Obsolete("Use new Scp")] - public IYubiKeyConnection Connect(byte[] applicationId) - { - var application = YubiKeyApplicationExtensions.GetById(applicationId); - return ConnectionFactory.CreateNonScpConnection(application); - } + public IYubiKeyConnection Connect(byte[] applicationId) => Connect(applicationId.GetYubiKeyApplication()); /// - [Obsolete("Use new Scp")] - public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication application, StaticKeys scp03Keys) - => (IScp03YubiKeyConnection)ConnectionFactory.CreateScpConnection(application, scp03Keys); + public virtual IYubiKeyConnection Connect(YubiKeyApplication application) => + ConnectionFactory.CreateConnection(application); /// - [Obsolete("Use new Scp")] - public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) - { - var application = YubiKeyApplicationExtensions.GetById(applicationId); - return (IScp03YubiKeyConnection)ConnectionFactory.CreateScpConnection(application, scp03Keys); - } - - [Obsolete("Use new Scp")] - internal virtual IYubiKeyConnection? Connect( - YubiKeyApplication? application, - byte[]? applicationId, - StaticKeys scp03Keys) // TODO Test that this works - { - application ??= YubiKeyApplicationExtensions.GetById(applicationId); - // return Connect((YubiKeyApplication)application, scp03Keys); - return ConnectionFactory.CreateScpConnection((YubiKeyApplication)application, scp03Keys); - } - - [Obsolete("Obsolete")] - public virtual IYubiKeyConnection Connect(YubiKeyApplication application, StaticKeys scp03Keys) - => ConnectionFactory.CreateScpConnection(application, scp03Keys); - - #endregion - - /// - public virtual IYubiKeyConnection Connect(YubiKeyApplication application) - => ConnectionFactory.CreateNonScpConnection(application); - - /// - public virtual IYubiKeyConnection Connect(YubiKeyApplication application, ScpKeyParameters keyParameters) - => ConnectionFactory.CreateScpConnection(application, keyParameters); + public virtual IScpYubiKeyConnection Connect( + byte[] applicationId, + ScpKeyParameters keyParameters) => + Connect(applicationId.GetYubiKeyApplication(), keyParameters); /// - public IScpYubiKeyConnection ConnectScp(YubiKeyApplication application, ScpKeyParameters keyParameters) - => (IScpYubiKeyConnection)ConnectionFactory.CreateScpConnection(application, keyParameters); //TODO is safe? + public virtual IScpYubiKeyConnection Connect( + YubiKeyApplication application, + ScpKeyParameters keyParameters) => + ConnectionFactory.CreateScpConnection(application, keyParameters); /// - public IScpYubiKeyConnection ConnectScp(byte[] applicationId, ScpKeyParameters keyParameters) //TODO Decide if to keep or not - { - var application = YubiKeyApplicationExtensions.GetById(applicationId); - return (IScpYubiKeyConnection)ConnectionFactory.CreateScpConnection(application, keyParameters); //TODO safe? - } + public bool TryConnect( + byte[] applicationId, + [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection) => + TryConnect(applicationId.GetYubiKeyApplication(), out connection); - #region obsoletewip /// - [Obsolete("Use new Scp")] - public bool TryConnectScp03( + public bool TryConnect( YubiKeyApplication application, - StaticKeys scp03Keys, - [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection) + [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection) { - var attemptedConnection = ConnectionFactory.CreateScpConnection(application, scp03Keys); - if (attemptedConnection is IScp03YubiKeyConnection scp03Connection) + try { - connection = scp03Connection; + connection = ConnectionFactory.CreateConnection(application); return true; } - - connection = null; - return false; - } - - /// - [Obsolete("Use new Scp")] - public bool TryConnectScp03( - byte[] applicationId, - StaticKeys scp03Keys, - [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection) - { - var application = YubiKeyApplicationExtensions.GetById(applicationId); - var attemptedConnection = ConnectionFactory.CreateScpConnection(application, scp03Keys); - if (attemptedConnection is IScp03YubiKeyConnection scp03Connection) + catch (Exception ex) { - connection = scp03Connection; - return true; + _log.LogError(ex, "Failed to connect to YubiKey"); } connection = null; return false; } - #endregion - - /// - [Obsolete("Use corresponding YubiKeyApplication method")] - public bool TryConnect( + /// + public bool TryConnect( byte[] applicationId, - [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection) - { - var application = YubiKeyApplicationExtensions.GetById(applicationId); - connection = ConnectionFactory.CreateNonScpConnection(application); - return true; //TODO is this safe? will it throw? - } + ScpKeyParameters keyParameters, + [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection) => + TryConnect(applicationId.GetYubiKeyApplication(), keyParameters, out connection); /// public bool TryConnect( - YubiKeyApplication application, - [MaybeNullWhen(returnValue: false)] out IYubiKeyConnection connection) // TODO Consider making nullable again - { - connection = ConnectionFactory.CreateNonScpConnection(application); - return true; //TODO is this safe? will it throw? - } - - /// - public bool TryConnectScp( YubiKeyApplication application, ScpKeyParameters keyParameters, [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection) { - var attemptedConnection = ConnectionFactory.CreateScpConnection(application, keyParameters); - if (attemptedConnection is IScpYubiKeyConnection scpConnection) + try { - connection = scpConnection; + connection = ConnectionFactory.CreateScpConnection(application, keyParameters); return true; } - - connection = null; - return false; - } - - public bool TryConnectScp( - byte[] applicationId, - ScpKeyParameters keyParameters, - [MaybeNullWhen(returnValue: false)] out IScpYubiKeyConnection connection) - { - var application = YubiKeyApplicationExtensions.GetById(applicationId); - var attemptedConnection = ConnectionFactory.CreateScpConnection(application, keyParameters); - if (attemptedConnection is IScpYubiKeyConnection scpConnection) + catch (Exception ex) { - connection = scpConnection; - return true; + _log.LogError(ex, "Failed to connect to YubiKey"); } connection = null; @@ -725,7 +643,7 @@ public void DeviceReset() connection?.Dispose(); } } - + /////////////////////////////////////////// PRIVATE ////////////////////////////////////////////////////////////////// private IYubiKeyResponse SendConfiguration(MgmtCmd.SetDeviceInfoBaseCommand baseCommand) @@ -787,7 +705,7 @@ private IYubiKeyResponse SendConfiguration(MgmtCmd.SetLegacyDeviceConfigBase bas connection?.Dispose(); } } - + private void MergeDevice(IDevice device) { switch (device) @@ -1072,5 +990,77 @@ private Transport GetTransportIfOnlyDevice() return Transport.None; } + + /// + [Obsolete("Use new Scp")] + public bool TryConnectScp03( + YubiKeyApplication application, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, + [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection) + { + try + { + connection = ConnectionFactory.CreateScpConnection(application, scp03Keys); + return true; + } + catch (Exception ex) + { + _log.LogError(ex, "Failed to connect to YubiKey"); + } + + connection = null; + return false; + } + + /// + [Obsolete("Use new Scp")] + public bool TryConnectScp03( + byte[] applicationId, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, + [MaybeNullWhen(returnValue: false)] out IScp03YubiKeyConnection connection) + { + try + { + connection = ConnectionFactory.CreateScpConnection(applicationId.GetYubiKeyApplication(), scp03Keys); + return true; + } + catch (Exception ex) + { + _log.LogError(ex, "Failed to connect to YubiKey"); + } + + connection = null; + return false; + } + + /// + [Obsolete("Use new Scp")] + public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication application, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) => + (IScp03YubiKeyConnection)ConnectionFactory.CreateScpConnection(application, scp03Keys); + + /// + [Obsolete("Use new Scp")] + public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) => + ConnectionFactory.CreateScpConnection( + applicationId.GetYubiKeyApplication(), scp03Keys); + + [Obsolete("Use new Scp")] + internal virtual IYubiKeyConnection? Connect( + YubiKeyApplication? application, + byte[]? applicationId, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys) + { + var app = application ?? + applicationId?.GetYubiKeyApplication() ?? + throw new ArgumentNullException(nameof(applicationId)); + + return ConnectionFactory.CreateScpConnection(app, scp03Keys); + } + + [Obsolete("Obsolete")] + public virtual IYubiKeyConnection Connect( + YubiKeyApplication application, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys) => + ConnectionFactory.CreateScpConnection(application, scp03Keys); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs deleted file mode 100644 index 5a258aefc..000000000 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs +++ /dev/null @@ -1,122 +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 Yubico.YubiKey.Scp03; - -namespace Yubico.YubiKey -{ - /// - /// A static class containing extension methods for YubiKeyDevice objects, - /// such as . - /// - public static class YubiKeyDeviceExtensions - { - /// - /// Use this method to make sure any Smart Card communication is - /// conducted using SCP03. If the input device is already set for - /// SCP03, this method will verify that the input staticKeys are - /// the same as those used to build the device and if they are, - /// simply return the input device. - /// - /// - /// This method of making an SCP03 connection is deprecated. See the - /// User's Manual entry on SCP03 for - /// information on how best to make an SCP03 connection.. - /// - /// The YubiKey itself is represented by an instance of - /// . After choosing the YubiKey to use, you - /// can specify that Smart Card communications be conduced using SCP03. - /// The key set you supply will be the keys used to encrypt and - /// authenticate/verify. That key set must already be loaded onto the - /// YubiKey. - /// - /// - /// For example, use SCP03 in a PivSession. - /// - /// if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - /// { - /// // error, can't find YubiKey - /// } - /// yubiKeyDevice = (YubiKeyDevice)yubiKeyDevice.WithScp03(scp03Keys); - /// using (var pivSession = new pivSession(yubiKeyDevice)) - /// { - /// . . . - /// } - /// - /// - /// - /// - /// The underlying YubiKey device for which SCP03 communications are to - /// be used. - /// - /// - /// The symmetric key set to use. This call will copy the keys (not just - /// a reference). - /// - /// - /// A wrapped that uses SCP03. - /// - /// - /// The device or scp03Keys arg is null. - /// - /// - /// The YubiKey device does not have an available smart card interface or - /// it does not support SCP03. - /// - [Obsolete("The WithScp03 extension will be deprecated, please specify SCP03 during the Connect call.", error: false)] - public static IYubiKeyDevice WithScp03(this YubiKeyDevice device, StaticKeys scp03Keys) => - GetScp03Device(device, scp03Keys); - - /// - /// This is the same as WithScp03, except it returns an - /// Scp03YubiKeyDevice instead of an IYubiKeyDevice. - /// Internally, we want an instance of this class, not a non-specific - /// object. - /// - internal static Scp03YubiKeyDevice GetScp03Device(this IYubiKeyDevice device, StaticKeys scp03Keys) - { - if (device is null) - { - throw new ArgumentNullException(nameof(device)); - } - if (scp03Keys is null) - { - throw new ArgumentNullException(nameof(scp03Keys)); - } - - if (device is Scp03YubiKeyDevice scp03Device) - { - if (scp03Device.StaticKeys.AreKeysSame(scp03Keys)) - { - return scp03Device; - } - - throw new ArgumentException(ExceptionMessages.Scp03KeyMismatch); - } - - if (device is YubiKeyDevice yubiKeyDevice) - { - if (!yubiKeyDevice.HasSmartCard) - { - throw new NotSupportedException(ExceptionMessages.CcidNotSupported); - } - - return new Scp03YubiKeyDevice(yubiKeyDevice, scp03Keys); - } - - throw new NotSupportedException(ExceptionMessages.NotSupportedByYubiKeyVersion); - } - } -} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs index 20376bfd4..58a5fc62b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs @@ -28,7 +28,7 @@ namespace Yubico.YubiKey public class YubiKeyDeviceInfo : IYubiKeyDeviceInfo { private const byte FipsMask = 0x80; - private const byte SkyMask = 0x40; + private const byte SkyMask = 0x40; private const byte FormFactorMask = unchecked((byte)~(FipsMask | SkyMask)); private static readonly FirmwareVersion _fipsInclusiveLowerBound = diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs index 6694936e2..e10b21e7e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyFeature.cs @@ -63,7 +63,7 @@ public enum YubiKeyFeature /// The ability to communicate using Secure Channel Protocol 3 (SCP03). ///
Scp03, - + /// /// The ability to communicate using Secure Channel Protocol 3 (SCP03) for OATH credentials. /// 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/Oath/OathSessionTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs index 6e18aca18..1bc2a988f 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs @@ -27,9 +27,9 @@ public void ResetOathApplication(StandardTestDevice testDeviceType) { IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var oathSession = new OathSession(testDevice, Scp03KeyParameters.DefaultKey)) //TODO follow Dains advice to reset the session + using (var oathSession = new OathSession(testDevice)) { - oathSession.ResetApplication(); // + oathSession.ResetApplication(); IList data = oathSession.GetCredentials(); Assert.True(oathSession._oathData.Challenge.IsEmpty); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs index 13db59558..dda84ae52 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs @@ -16,6 +16,7 @@ using Xunit; using Yubico.Core.Devices.Hid; using Yubico.YubiKey.Otp.Operations; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.Otp @@ -31,7 +32,7 @@ public void ConfigureStaticPassword_Succeeds(StandardTestDevice testDeviceType) { IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - using (var otpSession = new OtpSession(testDevice)) + using (var otpSession = new OtpSession(testDevice, Scp03KeyParameters.DefaultKey)) { if (otpSession.IsLongPressConfigured) { 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 ee651db39..d609e9feb 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs @@ -38,17 +38,17 @@ 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) + using var pivSession = useScp03 + ? new PivSession(testDevice, Scp03KeyParameters.DefaultKey) : new PivSession(testDevice); - + var collectorObj = new Simple39KeyCollector(); pivSession.KeyCollector = collectorObj.Simple39KeyCollectorDelegate; var result = pivSession.GenerateKeyPair(PivSlot.Retired12, expectedAlgorithm); Assert.Equal(expectedAlgorithm, result.Algorithm); } - + [SkippableTheory(typeof(NotSupportedException))] [InlineData(PivAlgorithm.Rsa1024)] [InlineData(PivAlgorithm.Rsa2048)] diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs index 0c9fc3086..a4c7f474e 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GetPutDataTests.cs @@ -29,7 +29,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 +45,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 +61,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 +87,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 +95,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,8 +114,8 @@ 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); -// TODO + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + // TODO #pragma warning disable CS0618 // Type or member is obsolete using (var pivSession = new PivSession(testDevice, new StaticKeys())) #pragma warning restore CS0618 // Type or member is obsolete @@ -124,13 +124,13 @@ public void Chuid_Auth_Req(StandardTestDevice testDeviceType) // 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 @@ -150,7 +150,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); } @@ -159,10 +159,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); } } @@ -178,7 +178,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)) { @@ -188,13 +188,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 @@ -216,7 +216,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); } @@ -227,10 +227,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); } } @@ -244,7 +244,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)) { @@ -252,10 +252,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. @@ -273,21 +273,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)) { @@ -295,7 +282,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 @@ -306,10 +293,7 @@ public void Printed_Auth_Req(StandardTestDevice testDeviceType) getDataResponse = pivSession.Connection.SendCommand(getDataCommand); Assert.Equal(ResponseStatus.AuthenticationRequired, getDataResponse.Status); } -//TODO -#pragma warning disable CS0618 // Type or member is obsolete - using (var pivSession = new PivSession(testDevice, newKeys)) -#pragma warning restore CS0618 // Type or member is obsolete + using (var pivSession = new PivSession(testDevice)) { // Verify the PIN pivSession.KeyCollector = PinOnlyKeyCollectorDelegate; @@ -318,7 +302,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); } @@ -329,7 +313,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; @@ -346,7 +330,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); } @@ -355,7 +339,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; @@ -373,10 +357,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); } } @@ -390,7 +374,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)) { @@ -398,13 +382,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 @@ -424,7 +408,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); } @@ -433,10 +417,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); } } @@ -449,7 +433,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)) { @@ -457,13 +441,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 @@ -483,7 +467,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); } @@ -492,10 +476,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); } } @@ -509,7 +493,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)) { @@ -517,7 +501,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 @@ -538,7 +522,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); } @@ -548,7 +532,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; @@ -565,7 +549,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); } @@ -574,7 +558,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; @@ -592,10 +576,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); } } @@ -609,7 +593,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)) { @@ -617,7 +601,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 @@ -638,7 +622,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); } @@ -648,7 +632,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; @@ -665,7 +649,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); } @@ -674,7 +658,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; @@ -692,10 +676,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); } } @@ -709,7 +693,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)) { @@ -717,7 +701,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 @@ -738,7 +722,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); } @@ -748,7 +732,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; @@ -765,7 +749,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); } @@ -774,7 +758,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; @@ -792,10 +776,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); } } @@ -809,7 +793,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)) { @@ -819,7 +803,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. @@ -841,7 +825,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)) { @@ -850,7 +834,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); } @@ -860,7 +844,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; @@ -877,7 +861,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); } @@ -886,10 +870,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); } } @@ -903,7 +887,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(); @@ -911,7 +895,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); } @@ -921,7 +905,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; @@ -938,7 +922,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); } @@ -947,10 +931,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); } } @@ -963,19 +947,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 @@ -995,7 +979,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); } @@ -1003,10 +987,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 de9b95a0d..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,11 +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)); -//TODO using var pivSession = useScp03 -#pragma warning disable CS0618 // Type or member is obsolete - ? new PivSession(testDevice, new StaticKeys()) -#pragma warning restore CS0618 // Type or member is obsolete + ? new PivSession(testDevice, Scp03KeyParameters.DefaultKey) : new PivSession(testDevice); var collectorObj = new Simple39KeyCollector(); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs similarity index 52% rename from Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs rename to Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs index e5cff0be9..a8e87a02e 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/DeleteKeyCommandTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs @@ -15,10 +15,9 @@ using System; using Xunit; using Yubico.YubiKey.Scp; -using Yubico.YubiKey.Scp03; using Yubico.YubiKey.TestUtilities; -namespace Yubico.YubiKey.Scp +namespace Yubico.YubiKey.Scp03.Commands { [Trait(TraitTypes.Category, TestCategories.Simple)] public class DeleteKeyCommandTests @@ -38,7 +37,7 @@ public void DeleteKey_One_Succeeds(StandardTestDevice testDeviceType) 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) + var currentKeys = new Scp03.StaticKeys(key2, key1, key3) { KeyVersionNumber = 3 }; @@ -52,6 +51,33 @@ public void DeleteKey_One_Succeeds(StandardTestDevice testDeviceType) Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); Assert.Equal(ResponseStatus.Success, rsp.Status); } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5Fips)] + [InlineData(StandardTestDevice.Fw5)] + public void DeleteKey_One_Succeeds2(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 Scp03KeyParameters(ScpKid.Scp03, 3, new StaticKeys(key1, key2, key3)); + var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); + var isValid = testDevice.TryConnectScp(YubiKeyApplication.Scp03, currentKeys, out var connection); + + Assert.True(isValid); + Assert.NotNull(connection); + + var cmd = new Scp03.Commands.DeleteKeyCommand(1, false); + var rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5Fips)] @@ -67,7 +93,9 @@ public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) 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) +#pragma warning disable CS0618 // Type or member is obsolete + var currentKeys = new Scp03.StaticKeys(key2, key1, key3) +#pragma warning restore CS0618 // Type or member is obsolete { KeyVersionNumber = 3 }; @@ -81,43 +109,42 @@ public void DeleteKey_Two_Succeeds(StandardTestDevice testDeviceType) Assert.True(isValid); Assert.NotNull(connection); - var cmd = new Scp.Commands.DeleteKeyCommand(2, false); - - var rsp = connection!.SendCommand(cmd); + var cmd = new Scp03.Commands.DeleteKeyCommand(2, false); + Scp03.Commands.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); -// //TODO -// #pragma warning disable CS0618 // Type or member is obsolete -// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -// #pragma warning restore CS0618 // Type or member is obsolete -// -// Assert.True(isValid); -// Assert.NotNull(connection); -// -// var cmd = new DeleteKeyCommand(3, true); -// var 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); + //TODO +#pragma warning disable CS0618 // Type or member is obsolete + var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); +#pragma warning restore CS0618 // Type or member is obsolete + + Assert.True(isValid); + Assert.NotNull(connection); + + var cmd = new Scp03.Commands.DeleteKeyCommand(3, true); + Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); + Assert.Equal(ResponseStatus.Success, rsp.Status); + } } } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index e8e196e00..cccbf9295 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -29,21 +29,34 @@ namespace Yubico.YubiKey.Scp [Trait(TraitTypes.Category, TestCategories.Simple)] public class Scp03Tests { + private IYubiKeyDevice GetDevice( + StandardTestDevice desiredDeviceType, + Transport transport = Transport.All, + FirmwareVersion? minimumFirmwareVersion = null) + => IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, minimumFirmwareVersion); + private readonly ReadOnlyMemory _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; - private IYubiKeyDevice Device { get; set; } public Scp03Tests() { - Device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, - minimumFirmwareVersion: FirmwareVersion.V5_3_0); + ResetAllowedDevices(); + } - using var session = new SecurityDomainSession(Device); - session.Reset(); + private static void ResetAllowedDevices() + { + // Reset all attached allowed devices + foreach (var availableDevice in IntegrationTestDeviceEnumeration.GetTestDevices()) + { + using var session = new SecurityDomainSession(availableDevice); + session.Reset(); + } } - [Fact] - public void TestImportKey() + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_TestImportKey(StandardTestDevice desiredDeviceType) { byte[] sk = { @@ -51,34 +64,37 @@ public void TestImportKey() 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, }; + var testDevice = GetDevice(desiredDeviceType); var newKeyParams = Scp03KeyParameters.FromStaticKeys(new StaticKeys(sk, sk, sk)); // Authenticate with default key, then replace default key - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); } - using (_ = new SecurityDomainSession(Device, newKeyParams)) + using (_ = new SecurityDomainSession(testDevice, newKeyParams)) { } // Default key should not work now and throw an exception Assert.Throws(() => { - using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + using (_ = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { } }); } - - [Fact] - public void PutKey_WithPublicKey_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_PutKey_WithPublicKey_Succeeds(StandardTestDevice desiredDeviceType) { - var keyReference = new KeyReference(0x10, 0x3); + var keyReference = new KeyReference(ScpKeyIds.ScpCaPublicKey, 0x3); + var testDevice = GetDevice(desiredDeviceType, Transport.All, FirmwareVersion.V5_7_2); - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); var publicKey = new ECPublicKeyParameters(ecdsa); @@ -88,121 +104,54 @@ public void PutKey_WithPublicKey_Succeeds() Assert.True(keyInformation.ContainsKey(keyReference)); } - [Fact] - public void TestDeleteKey() - { - var keyRef1 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); - var keyRef2 = new Scp03KeyParameters(ScpKid.Scp03, 0x55, RandomStaticKeys()); - - // Auth with default key, then replace default key - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) - { - session.PutKey(keyRef1.KeyReference, keyRef1.StaticKeys, 0); - } - - // Authenticate with key1, then add additional key, keyref2 - using (var session = new SecurityDomainSession(Device, keyRef1)) - { - session.PutKey(keyRef2.KeyReference, keyRef2.StaticKeys, 0); - } - - // Authenticate with key2, delete key 1 - using (var session = new SecurityDomainSession(Device, keyRef2)) - { - session.DeleteKey(keyRef1.KeyReference); - } - - // Authenticate with key 1, - // Should throw because we just deleted it - Assert.Throws(() => - { - using (_ = new SecurityDomainSession(Device, keyRef1)) - { - } - }); - - using (var session = new SecurityDomainSession(Device, keyRef2)) - { - session.DeleteKey(keyRef2.KeyReference, true); - } - - // Try to authenticate with key 2, - // Should throw because we just deleted the last key - Assert.Throws(() => - { - using (_ = new SecurityDomainSession(Device, keyRef2)) - { - } - }); - } - - [Fact] - public void TestReplaceKey() - { - var keyRef1 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); - var keyRef2 = new Scp03KeyParameters(ScpKid.Scp03, 0x10, RandomStaticKeys()); - - // Authenticate with default key, then replace default key - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) - { - session.PutKey(keyRef1.KeyReference, keyRef1.StaticKeys, 0); - } - // Authenticate with key1, then add additional key, keyref2 - using (var session = new SecurityDomainSession(Device, keyRef1)) - { - session.PutKey(keyRef2.KeyReference, keyRef2.StaticKeys, keyRef1.KeyReference.VersionNumber); - } - - // Authentication with new key 2 should succeed - using (_ = new SecurityDomainSession(Device, keyRef2)) - { - } - - // The ssession throws a SecureChannelException if the attempted key is incorrect -- - // But only if its not the default key. If it is the default key, it will throw an ArgumentException - Assert.Throws( - () => - { - using (_ = new SecurityDomainSession(Device, keyRef1)) - { - } - }); - } - - [Fact] - public void AuthenticateWithWrongKey_Should_ThrowException() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_AuthenticateWithWrongKey_Should_ThrowException(StandardTestDevice desiredDeviceType) { + var testDevice = GetDevice(desiredDeviceType); var incorrectKeys = RandomStaticKeys(); var keyRef = Scp03KeyParameters.FromStaticKeys(incorrectKeys); // Authentication with incorrect key should throw Assert.Throws(() => { - using (var session = new SecurityDomainSession(Device, keyRef)) { }; + using (var session = new SecurityDomainSession(testDevice, keyRef)) { } }); // Authentication with default key should succeed - using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + using (_ = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { - } } - [Fact] - public void GetInformation_WithDefaultKey_Returns_DefaultKey() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_GetInformation_WithDefaultKey_Returns_DefaultKey(StandardTestDevice desiredDeviceType) { - using var session = new SecurityDomainSession(Device); + var testDevice = GetDevice(desiredDeviceType); + using var session = new SecurityDomainSession(testDevice); var result = session.GetKeyInformation(); - Assert.NotEmpty(result); - Assert.Equal(4, result.Count); + 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); } - [Fact] - public void Connect_GetInformation_WithDefaultKey_Returns_DefaultKey() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_Connect_GetKeyInformation_WithDefaultKey_Returns_DefaultKey(StandardTestDevice desiredDeviceType) { - using var connection = Device.Connect(YubiKeyApplication.SecurityDomain); + var testDevice = GetDevice(desiredDeviceType); + 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(); @@ -223,27 +172,41 @@ public void Connect_GetInformation_WithDefaultKey_Returns_DefaultKey() keyInformation.Add(keyRef, keyComponents); } + Assert.NotEmpty(keyInformation); - Assert.Equal(4, keyInformation.Keys.Count); - Assert.Equal(0xFF, keyInformation.Keys.First().VersionNumber); + 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); + } } - [Fact] - public void GetCertificates_ReturnsCerts() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_GetCertificates_ReturnsCerts(StandardTestDevice desiredDeviceType) { - Skip.IfNot(Device.FirmwareVersion >= FirmwareVersion.V5_7_2); + var testDevice = GetDevice(desiredDeviceType, Transport.All, FirmwareVersion.V5_7_2); - using var session = new SecurityDomainSession(Device); + using var session = new SecurityDomainSession(testDevice); - var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); var certificateList = session.GetCertificates(keyReference); Assert.NotEmpty(certificateList); } - [Fact] - public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_Reset_Restores_SecurityDomainKeys_To_FactoryKeys(StandardTestDevice desiredDeviceType) { + var testDevice = GetDevice(desiredDeviceType); byte[] sk = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, @@ -251,83 +214,90 @@ public void Reset_Restores_SecurityDomainKeys_To_FactoryKeys() }; var newKeyParams = new Scp03KeyParameters( - ScpKid.Scp03, + ScpKeyIds.Scp03, 0x01, new StaticKeys(sk, sk, sk)); - // TODO assumeFalse("SCP03 not supported over NFC on FIPS capable devices", + // assumeFalse("SCP03 not supported over NFC on FIPS capable devices", TODO // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + 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(Device, newKeyParams)) + using (var session = new SecurityDomainSession(testDevice, newKeyParams)) { session.GetKeyInformation(); } - // Default key should not work now and throw an exception Assert.Throws(() => { - using (_ = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + using (_ = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { } }); - using (var session = new SecurityDomainSession(Device)) + 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(Device, Scp03KeyParameters.DefaultKey)) + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { _ = session.GetKeyInformation(); } } - [Fact] - public void Scp03_GetSupportedCaIdentifiers_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_GetSupportedCaIdentifiers_Succeeds(StandardTestDevice desiredDeviceType) { - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + var testDevice = GetDevice(desiredDeviceType, Transport.All, FirmwareVersion.V5_7_2); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); + var result = session.GetSupportedCaIdentifiers(true, true); Assert.NotEmpty(result); } - - [Fact] - public void Scp03_GetCardRecognitionData_Succeeds() + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_GetCardRecognitionData_Succeeds(StandardTestDevice desiredDeviceType) { - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + var testDevice = GetDevice(desiredDeviceType); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); var result = session.GetCardRecognitionData(); Assert.True(result.Length > 0); } - - [Fact] - public void Scp03_GetData_Succeeds() + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_GetData_Succeeds(StandardTestDevice desiredDeviceType) { - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + var testDevice = GetDevice(desiredDeviceType); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); var result = session.GetData(0x66); // Card Data Assert.True(result.Length > 0); } - [Theory] + [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] - public void PivSession_TryVerifyPinAndGetMetaData_Succeeds( - StandardTestDevice testDeviceType) + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds(StandardTestDevice desiredDeviceType) { - var device = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - Assert.True(device.FirmwareVersion >= FirmwareVersion.V5_3_0); - Assert.True(device.HasFeature(YubiKeyFeature.Scp03)); + var testDevice = GetDevice(desiredDeviceType); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); + Assert.True(testDevice.HasFeature(YubiKeyFeature.Scp03)); - using var pivSession = new PivSession(device, Scp03KeyParameters.DefaultKey); + using var pivSession = new PivSession(testDevice, Scp03KeyParameters.DefaultKey); - var result = pivSession.TryVerifyPin( - new ReadOnlyMemory(new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }), - out _); + var result = pivSession.TryVerifyPin(_defaultPin, out _); Assert.True(result); @@ -335,13 +305,15 @@ public void PivSession_TryVerifyPinAndGetMetaData_Succeeds( Assert.Equal(3, metadata.RetryCount); } - [Fact] - public void Device_Connect_With_Application_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_Device_Connect_With_Application_Succeeds(StandardTestDevice desiredDeviceType) { - var device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); + var testDevice = GetDevice(desiredDeviceType); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); - using var connection = device.Connect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey); + using var connection = testDevice.Connect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey); Assert.NotNull(connection); var cmd = new VerifyPinCommand(_defaultPin); @@ -349,13 +321,15 @@ public void Device_Connect_With_Application_Succeeds() Assert.Equal(ResponseStatus.Success, rsp.Status); } - [Fact] - public void Device_Connect_ApplicationId_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_Device_Connect_ApplicationId_Succeeds(StandardTestDevice desiredDeviceType) { - var device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); + var testDevice = GetDevice(desiredDeviceType); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); - using IYubiKeyConnection connection = device.Connect( + using IYubiKeyConnection connection = testDevice.Connect( YubiKeyApplication.Piv.GetIso7816ApplicationId(), Scp03KeyParameters.DefaultKey); Assert.NotNull(connection); @@ -365,13 +339,15 @@ public void Device_Connect_ApplicationId_Succeeds() Assert.Equal(ResponseStatus.Success, rsp.Status); } - [Fact] - public void Device_TryConnect_With_Application_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_Device_TryConnect_With_Application_Succeeds(StandardTestDevice desiredDeviceType) { - var device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); + var testDevice = GetDevice(desiredDeviceType); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); - var isValid = device.TryConnect( + var isValid = testDevice.TryConnect( YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); @@ -386,13 +362,15 @@ public void Device_TryConnect_With_Application_Succeeds() } } - [Fact] - public void Device_TryConnect_With_ApplicationId_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp03_Device_TryConnect_With_ApplicationId_Succeeds(StandardTestDevice desiredDeviceType) { - var device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, FirmwareVersion.V5_3_0); + var testDevice = GetDevice(desiredDeviceType); + Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); - var isValid = device.TryConnect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, + var isValid = testDevice.TryConnect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); using (connection) 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 index 5b15871ee..0172f1d75 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -22,8 +22,12 @@ 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 ECCurve = System.Security.Cryptography.ECCurve; using ECPoint = System.Security.Cryptography.ECPoint; @@ -35,39 +39,109 @@ namespace Yubico.YubiKey.Scp public class Scp11Tests { private const byte OceKid = 0x010; - private IYubiKeyDevice Device { get; set; } + + private IYubiKeyDevice GetDevice( + StandardTestDevice desiredDeviceType, + Transport transport = Transport.SmartCard, + FirmwareVersion? minimumFirmwareVersion = null) => + IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, + minimumFirmwareVersion ?? FirmwareVersion.V5_7_2); public Scp11Tests() { - Device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, - minimumFirmwareVersion: FirmwareVersion.V5_7_2); + ResetAllowedDevices(); + } + + private static void ResetAllowedDevices() + { + // Reset all attached allowed devices + 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_PivSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_EncryptedChannel_Parameters(testDevice, keyReference); + + using var session = new PivSession(testDevice, keyParams); + session.ResetApplication(); + + var collectorObj = new Simple39KeyCollector(); + session.KeyCollector = collectorObj.Simple39KeyCollectorDelegate; + var isVerified = session.TryVerifyPin(); + Assert.True(isVerified); + + var result = session.GenerateKeyPair(PivSlot.Retired12, PivAlgorithm.EccP256); + Assert.Equal(PivAlgorithm.EccP256, result.Algorithm); + } + + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_OathSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_EncryptedChannel_Parameters(testDevice, keyReference); + + using (var resetSession = new OathSession(testDevice, keyParams)) + { + resetSession.ResetApplication(); + } - using var session = new SecurityDomainSession(Device); - session.Reset(); + using var session = new OathSession(testDevice, keyParams); + var collectorObj = new SimpleOathKeyCollector(); + session.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + + session.SetPassword(); + Assert.True(session.IsPasswordProtected); } - [Fact] - public void Scp11b_PutKey_WithPublicKey_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_OtpSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) { - var keyReference = new KeyReference(0x10, 0x3); + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_EncryptedChannel_Parameters(testDevice, keyReference); - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); - using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + using var otpSession = new OtpSession(testDevice, keyParams); + if (otpSession.IsLongPressConfigured) + { + otpSession.DeleteSlot(Slot.LongPress); + } - var publicKey = new ECPublicKeyParameters(ecdsa); - session.PutKey(keyReference, publicKey, 0); + var configObj = otpSession.ConfigureStaticPassword(Slot.LongPress); + var generatedPassword = new Memory(new char[16]); + configObj = configObj.WithKeyboard(KeyboardLayout.en_US); + configObj = configObj.GeneratePassword(generatedPassword); - var keyInformation = session.GetKeyInformation(); - Assert.True(keyInformation.ContainsKey(keyReference)); + configObj.Execute(); ; } - [Fact] - public void Scp11b_Authenticate_Succeeds() // Works + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_Establish_Connection_Succeeds( + StandardTestDevice desiredDeviceType) { + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + IReadOnlyCollection certificateList; - var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); - using (var session = new SecurityDomainSession(Device)) + using (var session = new SecurityDomainSession(testDevice)) { certificateList = session.GetCertificates(keyReference); } @@ -76,24 +150,27 @@ public void Scp11b_Authenticate_Succeeds() // Works var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); - // Try to create authenticated session using key params and public key from yubikey - using (var session = new SecurityDomainSession(Device, keyParams)) + using (var session = new SecurityDomainSession(testDevice, keyParams)) { var result = session.GetKeyInformation(); Assert.NotEmpty(result); } } - [Fact] - public void Scp11b_Import_Succeeds() // Works + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_Import_Succeeds( + StandardTestDevice desiredDeviceType) { - var keyReference = new KeyReference(ScpKid.Scp11b, 0x2); - var ecDsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x2); // Start authenticated session with default key - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); // Import private key + var ecDsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); var privateKey = new ECPrivateKeyParameters(ecDsa); session.PutKey(keyReference, privateKey, 0); @@ -101,23 +178,31 @@ public void Scp11b_Import_Succeeds() // Works Assert.NotEmpty(result); } - [Fact] - public void GetCertificates_IsNotEmpty() // Works + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_GetCertificates_IsNotEmpty( + StandardTestDevice desiredDeviceType) { - using var session = new SecurityDomainSession(Device); + var testDevice = GetDevice(desiredDeviceType); + using var session = new SecurityDomainSession(testDevice); - var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); var certificateList = session.GetCertificates(keyReference); Assert.NotEmpty(certificateList); } - [Fact] - public void Scp11b_StoreCertificates_CanBeRetrieved() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11b_StoreCertificates_CanBeRetrieved( + StandardTestDevice desiredDeviceType) { - var keyReference = new KeyReference(ScpKid.Scp11b, 0x1); + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); var oceCertificates = GetOceCertificates(Scp11TestData.OceCerts.Span); session.StoreCertificates(keyReference, oceCertificates.Bundle); @@ -129,13 +214,17 @@ public void Scp11b_StoreCertificates_CanBeRetrieved() Assert.Equal(oceThumbprint, result[0].Thumbprint); } - [Fact] - public void Scp11a_GenerateEcKey_Succeeds() // Works + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11a_GenerateEcKey_Succeeds( + StandardTestDevice desiredDeviceType) { - var keyReference = new KeyReference(ScpKid.Scp11a, 0x3); + var testDevice = GetDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11A, 0x3); // Start authenticated session - using var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey); + using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); // Generate a new EC key var generatedKey = session.GenerateEcKey(keyReference, 0); @@ -151,19 +240,23 @@ public void Scp11a_GenerateEcKey_Succeeds() // Works Assert.NotNull(ecdsa); } - [Fact] - public void Scp11a_WithAllowList_AllowsApprovedSerials() + [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; // Means this is not containing the correct OCECerts? - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + Scp11KeyParameters keyParams; + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { - keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + keyParams = LoadKeys(session, ScpKeyIds.Scp11A, kvn); } - using (var session = new SecurityDomainSession(Device, keyParams)) + using (var session = new SecurityDomainSession(testDevice, keyParams)) { var serials = new List { @@ -176,33 +269,37 @@ public void Scp11a_WithAllowList_AllowsApprovedSerials() session.StoreAllowlist(oceKeyRef, serials); } - using (var session = new SecurityDomainSession(Device, keyParams)) + using (var session = new SecurityDomainSession(testDevice, keyParams)) { - session.DeleteKeySet(new KeyReference(ScpKid.Scp11a, kvn)); + session.DeleteKey(new KeyReference(ScpKeyIds.Scp11A, kvn)); } } - [Fact] - public void Scp11a_WithAllowList_BlocksUnapprovedSerials() // Works + [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(Device, Scp03KeyParameters.DefaultKey)) + 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(Device, scp03KeyParams)) + using (var session = new SecurityDomainSession(testDevice, scp03KeyParams)) { // Make space for new key - session.DeleteKeySet(new KeyReference(ScpKid.Scp11b, 0x01), false); + session.DeleteKey(new KeyReference(ScpKeyIds.Scp11B, 0x01), false); // Load SCP11a keys - scp11KeyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + scp11KeyParams = LoadKeys(session, ScpKeyIds.Scp11A, kvn); // Create list of serial numbers var serials = new List @@ -219,61 +316,69 @@ public void Scp11a_WithAllowList_BlocksUnapprovedSerials() // Works // This is the test. Authenticate with SCP11a should throw. Assert.Throws(() => { - using (var session = new SecurityDomainSession(Device, scp11KeyParams)) + using (var session = new SecurityDomainSession(testDevice, scp11KeyParams)) { // ... Authenticated } }); // Reset the allow list - using (var session = new SecurityDomainSession(Device, scp03KeyParams)) + using (var session = new SecurityDomainSession(testDevice, scp03KeyParams)) { session.ClearAllowList(oceKeyRef); } // Now, with the allowlist removed, authenticate with SCP11a should now succeed - using (var session = new SecurityDomainSession(Device, scp11KeyParams)) + using (var session = new SecurityDomainSession(testDevice, scp11KeyParams)) { // ... Authenticated } } - [Fact] - public void Scp11a_Authenticate_Succeeds() // Works + [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(ScpKid.Scp11a, kvn); + var keyRef = new KeyReference(ScpKeyIds.Scp11A, kvn); // Start authenticated session with default key Scp11KeyParameters keyParams; - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { - keyParams = LoadKeys(session, ScpKid.Scp11a, kvn); + keyParams = LoadKeys(session, ScpKeyIds.Scp11A, kvn); } // Start authenticated session using new key params and public key from yubikey - using (var session = new SecurityDomainSession(Device, keyParams)) + using (var session = new SecurityDomainSession(testDevice, keyParams)) { - session.DeleteKeySet(keyRef); + session.DeleteKey(keyRef); } } - [Fact] - public void Scp11c_Authenticate_Succeeds() + [SkippableTheory(typeof(DeviceNotFoundException))] + [InlineData(StandardTestDevice.Fw5)] + [InlineData(StandardTestDevice.Fw5Fips)] + public void Scp11c_Authenticate_Succeeds( + StandardTestDevice desiredDeviceType) { + var testDevice = GetDevice(desiredDeviceType); const byte kvn = 0x03; - var keyReference = new KeyReference(ScpKid.Scp11c, kvn); + var keyReference = new KeyReference(ScpKeyIds.Scp11C, kvn); Scp11KeyParameters keyParams; - using (var session = new SecurityDomainSession(Device, Scp03KeyParameters.DefaultKey)) + using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { - keyParams = LoadKeys(session, ScpKid.Scp11c, kvn); + keyParams = LoadKeys(session, ScpKeyIds.Scp11C, kvn); } Assert.Throws(() => { - using var session = new SecurityDomainSession(Device, keyParams); - session.DeleteKeySet(keyReference); + using var session = new SecurityDomainSession(testDevice, keyParams); + session.DeleteKey(keyReference); }); } @@ -308,7 +413,8 @@ private Scp11KeyParameters LoadKeys( // Store the key identifier with the referenced off card entity on the Yubikey session.StoreCaIssuer(oceRef, ski); - var (certChain, privateKey) = GetOceCertificateChainAndPrivateKey(); + var (certChain, privateKey) = + GetOceCertificateChainAndPrivateKey(Scp11TestData.Oce, Scp11TestData.OcePassword); // Now we have the EC private key parameters and cert chain return new Scp11KeyParameters( @@ -320,11 +426,13 @@ private Scp11KeyParameters LoadKeys( ); } - private static (List certChain, ECParameters privateKey) GetOceCertificateChainAndPrivateKey() + private static (List certChain, ECParameters privateKey) GetOceCertificateChainAndPrivateKey( + ReadOnlyMemory ocePkcs12, + ReadOnlyMemory ocePassword) { - // Load the OCE PKCS12 using Bouncy Castle - using var pkcsStream = new MemoryStream(Scp11TestData.Oce.ToArray()); - var pkcs12Store = new Pkcs12Store(pkcsStream, Scp11TestData.OcePassword.ToArray()); + // 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(); @@ -333,32 +441,31 @@ private static (List certChain, ECParameters privateKey) GetOc throw new InvalidOperationException("No private key entry found in PKCS12"); } - // 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"); - } - + // 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), - (string)null!, // no password needed for public cert - X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet - ); - }); - - var certs = ScpCertificates.From(x509Certs); + .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)); } @@ -449,7 +556,6 @@ private static Scp03KeyParameters ImportScp03Key( // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); // todo - var scp03Ref = new KeyReference(0x01, 0x01); var staticKeys = new StaticKeys( GetRandomBytes(16), @@ -471,80 +577,28 @@ private static Memory GetRandomBytes( return hostChallenge.ToArray(); } - } + /// + /// This is a copy of Scp11b_Authenticate_Succeeds test + /// + /// + /// + /// + private static Scp11KeyParameters Get_Scp11b_EncryptedChannel_Parameters( + IYubiKeyDevice testDevice, + KeyReference keyReference) + { + IReadOnlyCollection certificateList; + using (var session = new SecurityDomainSession(testDevice)) + { + certificateList = session.GetCertificates(keyReference); + } - 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(); + var leaf = certificateList.Last(); + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); + 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 index 20c1956ef..87197bab2 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/ScpCertificates.cs @@ -25,7 +25,7 @@ public static ScpCertificates From(IEnumerable? certificates) return new ScpCertificates(null, Array.Empty(), null); } - var certList = new List(certificates); + var certList = certificates.ToList(); X509Certificate2? ca = null; byte[]? seenSerial = null; @@ -79,13 +79,13 @@ public static ScpCertificates From(IEnumerable? certificates) X509Certificate2? leaf = null; if (ordered.Count > 0) { - // var lastCert = ordered[^1]; todo - // var keyUsage = lastCert.Extensions.OfType().FirstOrDefault()?.KeyUsages ?? Array.Empty(); - // if (keyUsage.Length > 4 && keyUsage[4]) - // { - // leaf = lastCert; - // ordered.RemoveAt(ordered.Count - 1); - // } + 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); 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 2be7c725d..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/Commands/PutKeyCommandTests.cs +++ /dev/null @@ -1,158 +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.Scp03; -// 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); -// //TODO -// -// #pragma warning disable CS0618 // Type or member is obsolete -// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -// #pragma warning restore CS0618 // Type or member is obsolete -// -// 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); -// //TODO -// #pragma warning disable CS0618 // Type or member is obsolete -// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -// #pragma warning restore CS0618 // Type or member is obsolete -// -// 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); -// //TODO -// #pragma warning disable CS0618 // Type or member is obsolete -// var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -// #pragma warning restore CS0618 // Type or member is obsolete -// -// 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 a917c5594..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/PutDeleteKeyTests.cs +++ /dev/null @@ -1,157 +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; -#pragma warning disable CS0618 // Type or member is obsolete - -namespace Yubico.YubiKey.Scp03 -{ - // These may require that DeleteKeyCommandTests have been run first. - [TestCaseOrderer(PriorityOrderer.TypeName, PriorityOrderer.AssembyName)] - public class PutDeleteTests - { - [Fact] - [TestPriority(3)] - [Obsolete("Obsolete")] - 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)] - [Obsolete("Obsolete")] - 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)] - [Obsolete("Obsolete")] - public void DeleteKey_Succeeds() - { - using StaticKeys staticKeys = GetKeySet(3); - IYubiKeyDevice device = IntegrationTestDeviceEnumeration.GetTestDevice( - Transport.SmartCard, - FirmwareVersion.V5_3_0); - //TODO - - 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 e313bb03f..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp03/SimpleSessionTests.cs +++ /dev/null @@ -1,139 +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.Scp; -using Yubico.YubiKey.TestUtilities; - -namespace Yubico.YubiKey.Scp03 -{ - [Trait(TraitTypes.Category, TestCategories.Simple)] - 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] - [Obsolete("Obsolete")] - 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] - [Obsolete("Obsolete")] - 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] - [Obsolete("Obsolete")] - 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] - [Obsolete("Obsolete")] - public void TryConnectScp03_AlgorithmId_Succeeds() - { - var 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.True(isValid); - Assert.NotNull(connection); - var cmd = new VerifyPinCommand(_pin); - VerifyPinResponse rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - } - - [Fact] - public void TryConnectScp_AlgorithmId_Succeeds() - { - var device = IntegrationTestDeviceEnumeration.GetTestDevice(Transport.SmartCard, FirmwareVersion.V5_3_0); - var isValid = device.TryConnectScp(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, out var connection); - using (connection) - { - Assert.True(isValid); - Assert.NotNull(connection); - var cmd = new VerifyPinCommand(_pin); - var rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - } - } -} 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/Fido2/Fido2SessionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs index 5e5ecd4f3..41e1cee5b 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs @@ -43,7 +43,7 @@ void Constructor_ValidYubiKeyDevice_Succeeds() var mockResponse = new GetInfoResponse(new ResponseApdu(Fido2InfoTests.GetSampleEncoded(), SWConstants.Success)); _ = mockConnection - .Setup(c => c.SendCommand(It.IsAny>())) + .Setup(c => c.SendCommand(It.IsAny>(), It.IsAny())) .Returns(mockResponse); _ = mockYubiKey @@ -63,7 +63,7 @@ void Constructor_GivenValidYubiKeyDevice_ConnectsToFido2Application() var mockResponse = new GetInfoResponse(new ResponseApdu(Fido2InfoTests.GetSampleEncoded(), SWConstants.Success)); _ = mockConnection - .Setup(c => c.SendCommand(It.IsAny>())) + .Setup(c => c.SendCommand(It.IsAny>(), It.IsAny())) .Returns(mockResponse); _ = mockYubiKey @@ -83,7 +83,7 @@ void GetAuthenticatorInfo_SendsGetInfoCommand() var mockResponse = new GetInfoResponse(new ResponseApdu(Fido2InfoTests.GetSampleEncoded(), SWConstants.Success)); _ = mockConnection - .Setup(c => c.SendCommand(It.IsAny>())) + .Setup(c => c.SendCommand(It.IsAny>(),It.IsAny())) .Returns(mockResponse); _ = mockYubiKey @@ -94,7 +94,7 @@ void GetAuthenticatorInfo_SendsGetInfoCommand() //session.AuthenticatorInfo; - mockConnection.Verify(c => c.SendCommand(It.IsAny())); + mockConnection.Verify(c => c.SendCommand(It.IsAny(),It.IsAny())); } } } diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs index c4ebb4e40..068224ddb 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs @@ -81,7 +81,7 @@ public void Invoke_CommandApduWithoutData_InvokesNextTransformWithSameApdu() // Assert mockTransform.Verify( x => - x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny())); + x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny(),It.IsAny())); } [Fact] @@ -91,7 +91,7 @@ public void Invoke_CommandApduWithoutData_ReturnsNextTransformResponseAsIs() var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny())) .Returns(expectedResponse); var transform = new CommandChainingTransform(mockTransform.Object); @@ -118,7 +118,7 @@ public void Invoke_CommandApduWithSmallData_InvokesNextTransformWithSameApdu() // Assert mockTransform.Verify( x => - x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny())); + x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny(),It.IsAny())); } [Fact] @@ -128,7 +128,7 @@ public void Invoke_CommandApduWithSmallData_ReturnsNextTransformResponseAsIs() var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny())) .Returns(expectedResponse); var transform = new CommandChainingTransform(mockTransform.Object); @@ -152,7 +152,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_OrsHex10ToClaOnAllExceptLast() var commandApdu = new CommandApdu { Data = Enumerable.Repeat(0xFF, 16).ToArray() }; _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((a, b, c) => observedCla.Add(a.Cla)); // Act @@ -179,7 +179,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_AllOtherApduPropertiesRemainUn }; _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((a, b, c) => observedApdus.Add(a)); // Act @@ -208,7 +208,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_SplitsDataAcrossInvokeCalls() }; _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((a, b, c) => observedApdus.Add(a.Data.ToArray())); // Act diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs index 224e435ab..89f884cf2 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs @@ -71,7 +71,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Arrange var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ResponseApdu(new byte[] { 0x90, 0x00 })); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -80,7 +80,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] @@ -90,7 +90,7 @@ public void Invoke_SuccessfulResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -108,7 +108,7 @@ public void Invoke_FailedResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -127,7 +127,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -137,7 +137,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -150,7 +150,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -160,7 +160,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() // Assert mockTransform.Verify(x => - x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny()), Times.Once); + x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -172,7 +172,7 @@ public void Invoke_BytesAvailble_ConcatsAllBuffers() var response2 = new ResponseApdu(new byte[] { 5, 6, 7, 8, SW1Constants.Success, 0x00 }); byte[] expectedData = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -192,7 +192,7 @@ public void Invoke_BytesAvailable_ReturnsLastStatusWord() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs index b91c3f99b..2a4b80636 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs @@ -71,7 +71,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Arrange var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ResponseApdu(new byte[] { 0x90, 0x00 })); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -80,7 +80,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] @@ -90,7 +90,7 @@ public void Invoke_SuccessfulResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -108,7 +108,7 @@ public void Invoke_FailedResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -127,7 +127,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -137,7 +137,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -150,7 +150,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -160,7 +160,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() // Assert mockTransform.Verify(x => - x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny()), Times.Once); + x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -172,7 +172,7 @@ public void Invoke_BytesAvailble_ConcatsAllBuffers() var response2 = new ResponseApdu(new byte[] { 5, 6, 7, 8, SW1Constants.Success, 0x00 }); byte[] expectedData = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -192,7 +192,7 @@ public void Invoke_BytesAvailable_ReturnsLastStatusWord() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs index 52cc1f711..ad939c734 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs @@ -24,29 +24,36 @@ namespace Yubico.YubiKey.Pipelines { - [Obsolete("This class is obsolete and will be removed in a future release.")] + [Obsolete("TODO Migrate?")] public class PipelineFixture : IApduTransform { - public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) + public ResponseApdu Invoke( + CommandApdu command, + Type commandType, + Type responseType, + bool encrypt = false) { if (command is null) { throw new ArgumentNullException(nameof(command)); } - if (command.AsByteArray().SequenceEqual(new SelectApplicationCommand(YubiKeyApplication.Piv).CreateCommandApdu().AsByteArray())) + 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")); + 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"))) + else if (command.AsByteArray() + .SequenceEqual(Hex.HexToBytes("84FD0000181CE4E3D8F32D986A886DDBC90C8DB22553C2C04391250CCE"))) { return new ResponseApdu(Hex.HexToBytes("5F67E9E059DF3C52809DC9F6DDFBEF3E4C45691B2C8CDDD89000")); } @@ -57,25 +64,28 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT // 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) + + 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]; @@ -114,7 +124,8 @@ public void Invoke_GivenPriorSetup_CorrectlyEncodesCommand() pipeline.Setup(fakeRng); // Act - ResponseApdu responseApdu = pipeline.Invoke(new VersionCommand().CreateCommandApdu(), typeof(object), typeof(object)); + ResponseApdu responseApdu = + pipeline.Invoke(new VersionCommand().CreateCommandApdu(), typeof(object), typeof(object)); var versionResponse = new VersionResponse(responseApdu); FirmwareVersion fwv = versionResponse.GetData(); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs similarity index 100% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelEncryptionTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs similarity index 100% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/ChannelMacTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/Scp03ResponseTests.cs similarity index 84% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/Scp03ResponseTests.cs index 616595bef..d658c2b69 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/Scp03ResponseTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/Scp03ResponseTests.cs @@ -15,17 +15,17 @@ using System; using Xunit; using Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp.Commands; -namespace Yubico.YubiKey.Scp03.Commands +namespace Yubico.YubiKey.Scp0.Commands { - [Obsolete("This class is obsolete and will be removed in a future release.")] - 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. } @@ -36,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); @@ -49,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); @@ -62,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 100% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/DerivationTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs similarity index 100% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/PaddingTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs 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 5f0025e16..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateCommandTests.cs +++ /dev/null @@ -1,80 +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 -{ - [Obsolete("This class is obsolete and will be removed in a future release.")] - 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 7b3cca4b6..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/ExternalAuthenticateResponseTests.cs +++ /dev/null @@ -1,58 +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 -{ - [Obsolete("This class is obsolete and will be removed in a future release.")] - 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 ea6507a43..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateCommandTests.cs +++ /dev/null @@ -1,83 +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 -{ - [Obsolete("This class is obsolete and will be removed in a future release.")] - 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 bec58a375..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponseTests.cs +++ /dev/null @@ -1,112 +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 -{ - [Obsolete("This class is obsolete and will be removed in a future release.")] - 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 77132cde8..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp03/SessionTests.cs +++ /dev/null @@ -1,126 +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; -using Yubico.YubiKey.Scp03.Commands; - -namespace Yubico.YubiKey.Scp -{ - [Obsolete("Replaced by SecurityDomainSession")] - 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 index 9d49e0516..5c2c5bc46 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs @@ -41,16 +41,16 @@ public void GetIso7816ApplicationId_ThrowsForUnsupportedApplication() [Fact] public void ApplicationIds_ReturnsReadOnlyDictionary() { - var result = YubiKeyApplicationExtensions.ApplicationIds; - Assert.IsType>>(result); - } + var result = YubiKeyApplicationExtensions.Iso7816ApplicationIds; + Assert.IsType>>(result); + } [Fact] public void ApplicationIds_ContainsAllApplications() { - var result = YubiKeyApplicationExtensions.ApplicationIds; - Assert.Equal(11, result.Count); - Assert.Contains(YubiKeyApplication.Management, result.Keys); + 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 @@ -62,15 +62,15 @@ public void ApplicationIds_ContainsAllApplications() [InlineData(new byte[] { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }, YubiKeyApplication.FidoU2f)] public void GetById_ReturnsCorrectApplication(byte[] applicationId, YubiKeyApplication expectedApplication) { - var result = YubiKeyApplicationExtensions.GetById(applicationId); - Assert.Equal(expectedApplication, result); + 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.GetById(unknownId)); - } + Assert.Throws(() => YubiKeyApplicationExtensions.GetYubiKeyApplication(unknownId)); + } } } diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs index 0101756ef..41d9718a5 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs @@ -59,7 +59,7 @@ public HollowConnection(YubiKeyApplication yubikeyApplication, FirmwareVersion f _firmwareVersion = firmwareVersion; } - public TResponse SendCommand(IYubiKeyCommand yubiKeyCommand) + public TResponse SendCommand(IYubiKeyCommand yubiKeyCommand, bool encrypt = false) where TResponse : IYubiKeyResponse { if (yubiKeyCommand is SelectApplicationCommand) diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs index db3e97cc9..91d44178e 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs @@ -15,7 +15,7 @@ using System; using Yubico.Core.Devices; using Yubico.YubiKey.Scp; -using StaticKeys = Yubico.YubiKey.Scp03.StaticKeys; + namespace Yubico.YubiKey.TestUtilities { @@ -151,30 +151,30 @@ public IYubiKeyConnection Connect(YubiKeyApplication yubiKeyApplication) return connection; } - + public IYubiKeyConnection Connect(byte[] applicationId) { throw new NotImplementedException(); } - + [Obsolete("Obsolete")] - public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys) + public IScp03YubiKeyConnection ConnectScp03(YubiKeyApplication yubikeyApplication, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) { throw new NotImplementedException(); } [Obsolete("Obsolete")] - public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, StaticKeys scp03Keys) + public IScp03YubiKeyConnection ConnectScp03(byte[] applicationId, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) { throw new NotImplementedException(); } - public IScpYubiKeyConnection ConnectScp(YubiKeyApplication yubikeyApplication, ScpKeyParameters scp03Keys) + public IScpYubiKeyConnection Connect(YubiKeyApplication yubikeyApplication, ScpKeyParameters scp03Keys) { throw new NotImplementedException(); } - public IScpYubiKeyConnection ConnectScp(byte[] applicationId, ScpKeyParameters scp03Keys) + public IScpYubiKeyConnection Connect(byte[] applicationId, ScpKeyParameters scp03Keys) { throw new NotImplementedException(); } @@ -196,7 +196,7 @@ public bool TryConnect( [Obsolete("Obsolete")] public bool TryConnectScp03( YubiKeyApplication application, - StaticKeys scp03Keys, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, out IScp03YubiKeyConnection connection) { throw new NotImplementedException(); @@ -205,13 +205,13 @@ public bool TryConnectScp03( [Obsolete("Obsolete")] public bool TryConnectScp03( byte[] applicationId, - StaticKeys scp03Keys, + Yubico.YubiKey.Scp03.StaticKeys scp03Keys, out IScp03YubiKeyConnection connection) { throw new NotImplementedException(); } - public bool TryConnectScp( + public bool TryConnect( YubiKeyApplication application, ScpKeyParameters keyParameters, out IScpYubiKeyConnection connection) @@ -219,7 +219,7 @@ public bool TryConnectScp( throw new NotImplementedException(); } - public bool TryConnectScp( + public bool TryConnect( byte[] applicationId, ScpKeyParameters keyParameters, out IScpYubiKeyConnection connection) @@ -227,22 +227,12 @@ public bool TryConnectScp( throw new NotImplementedException(); } - public IScpYubiKeyConnection ConnectScp(YubiKeyApplication yubikeyApplication, StaticKeys scp03Keys) - { - throw new NotImplementedException(); - } - - public IScpYubiKeyConnection ConnectScp(byte[] applicationId, StaticKeys scp03Keys) - { - throw new NotImplementedException(); - } - /// bool IYubiKeyDevice.HasSameParentDevice(IDevice other) { return false; } - + public void SetEnabledNfcCapabilities(YubiKeyCapabilities yubiKeyCapabilities) => throw new NotImplementedException(); 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 /// From 81799ca12e22bbc78c8c27be515bad03cef63935 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Mon, 18 Nov 2024 15:29:07 +0100 Subject: [PATCH 08/53] feat(scp): supports apps over SCP support OATH and SCP support OTP and SCP support YubiHSM and SCP --- .../src/Yubico/YubiKey/ApplicationSession.cs | 10 +- .../YubiKey/Cryptography/ECKeyParameters.cs | 7 + .../src/Yubico/YubiKey/FirmwareVersion.cs | 3 +- .../src/Yubico/YubiKey/IApduTransform.cs | 2 +- .../src/Yubico/YubiKey/IYubiKeyConnection.cs | 11 +- .../YubiKey/Oath/OathSession.Credential.cs | 6 +- .../YubiKey/Oath/OathSession.Password.cs | 5 +- .../src/Yubico/YubiKey/Otp/OtpSession.cs | 11 +- .../Pipelines/CommandChainingTransform.cs | 2 +- .../YubiKey/Piv/PivSession.Attestation.cs | 6 +- .../Yubico/YubiKey/Piv/PivSession.Crypto.cs | 4 +- .../Yubico/YubiKey/Piv/PivSession.KeyPairs.cs | 4 +- .../YubiKey/Piv/PivSession.ManagementKey.cs | 14 +- .../Yubico/YubiKey/Piv/PivSession.Msroots.cs | 2 +- .../Yubico/YubiKey/Piv/PivSession.Objects.cs | 10 +- .../src/Yubico/YubiKey/Piv/PivSession.Pin.cs | 30 ++-- .../Yubico/YubiKey/Piv/PivSession.Pinonly.cs | 6 +- .../src/Yubico/YubiKey/Scp/ChannelMac.cs | 108 ------------- .../Commands/ExternalAuthenticateCommand.cs | 2 +- .../Scp/Commands/GenerateEcKeyCommand.cs | 27 ++-- .../YubiKey/Scp/Commands/GetDataCommand.cs | 62 +++++--- .../Scp/Commands/InitializeUpdateCommand.cs | 2 +- .../Scp/Commands/InitializeUpdateResponse.cs | 2 +- .../InternalAuthenticateCommand.cs | 24 ++- .../YubiKey/Scp/Commands/PutKeyResponse.cs | 10 +- .../YubiKey/Scp/Commands/ResetCommand.cs | 2 +- .../YubiKey/Scp/Commands/ScpResponse.cs | 31 ++-- .../Scp/Commands/SecurityOperationCommand.cs | 6 +- .../YubiKey/Scp/Commands/StoreDataCommand.cs | 45 ++++-- .../Scp/{ => Helpers}/ChannelEncryption.cs | 0 .../Yubico/YubiKey/Scp/Helpers/ChannelMac.cs | 109 +++++++++++++ .../YubiKey/Scp/{ => Helpers}/Derivation.cs | 21 ++- .../YubiKey/Scp/{ => Helpers}/Padding.cs | 8 +- .../YubiKey/Scp/IScpYubiKeyConnection.cs | 4 +- .../src/Yubico/YubiKey/Scp/KeyReference.cs | 5 +- .../Yubico/YubiKey/Scp/Scp03KeyParameters.cs | 32 +++- .../src/Yubico/YubiKey/Scp/Scp03State.cs | 1 + .../src/Yubico/YubiKey/Scp/ScpConnection.cs | 1 - .../Yubico/YubiKey/Scp/ScpKeyParameters.cs | 5 +- .../src/Yubico/YubiKey/Scp/ScpKid.cs | 24 --- .../src/Yubico/YubiKey/Scp/StaticKeys.cs | 62 +------- .../src/Yubico/YubiKey/Scp03/StaticKeys.cs | 4 +- .../YubiKey/YubiHsmAuth/YubiHsmAuthSession.cs | 53 +------ .../src/Yubico/YubiKey/YubiKeyResponse.cs | 7 +- .../YubiKey/Otp/ConfigureStaticTests.cs | 57 ------- .../Yubico/YubiKey/Piv/GenerateTests.cs | 2 +- .../Scp/Commands/DeleteKeyCommandTests.cs | 150 ------------------ .../Yubico/YubiKey/Scp/Scp11Tests.cs | 18 +-- .../YubiHsmAuth/SessionCredentialTests.cs | 26 ++- .../YubiKey/YubiHsmAuth/YhaTestUtilities.cs | 4 +- .../Yubico/YubiKey/Fido2/Fido2SessionTests.cs | 10 +- .../CommandChainingTransformTests.cs | 14 +- .../OathResponseChainingTransformTests.cs | 20 +-- .../ResponseChainingTransformTests.cs | 20 +-- .../Pipelines/Scp03ApduTransformTests.cs | 3 +- .../YubiKey/Scp/ChannelEncryptionTests.cs | 1 + .../Yubico/YubiKey/Scp/ChannelMacTests.cs | 1 + .../Yubico/YubiKey/Scp/DerivationTests.cs | 2 + .../unit/Yubico/YubiKey/Scp/PaddingTests.cs | 1 + .../Yubico/YubiKey/YubikeyApplicationTests.cs | 74 ++++----- .../YubiKey/TestUtilities/HollowConnection.cs | 2 +- .../TestUtilities/HollowYubiKeyDevice.cs | 10 ++ .../IntegrationTestDeviceEnumeration.cs | 26 ++- .../TestUtilities/TestDeviceSelection.cs | 67 ++++++-- 64 files changed, 570 insertions(+), 728 deletions(-) delete mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs rename Yubico.YubiKey/src/Yubico/YubiKey/Scp/{ => Commands}/InternalAuthenticateCommand.cs (60%) rename Yubico.YubiKey/src/Yubico/YubiKey/Scp/{ => Helpers}/ChannelEncryption.cs (100%) create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs rename Yubico.YubiKey/src/Yubico/YubiKey/Scp/{ => Helpers}/Derivation.cs (90%) rename Yubico.YubiKey/src/Yubico/YubiKey/Scp/{ => Helpers}/Padding.cs (92%) delete mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs delete mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.cs delete mode 100644 Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs index 01f521925..f2961c323 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs @@ -33,7 +33,7 @@ public abstract class ApplicationSession : IDisposable /// applications will ignore this, but it can be used to call Commands /// directly. ///
- public IYubiKeyConnection Connection { get; } + public IYubiKeyConnection Connection { get; protected set; } /// /// Gets the parameters used for establishing a Secure Channel Protocol (SCP) connection. @@ -42,6 +42,8 @@ public abstract class ApplicationSession : IDisposable protected ILogger Logger { get; } protected IYubiKeyDevice YubiKey { get; } + public YubiKeyApplication Application { get; } + private bool _disposed; @@ -62,11 +64,13 @@ protected ApplicationSession( { Logger = logger; YubiKey = device ?? throw new ArgumentNullException(nameof(device)); + Application = application; + KeyParameters = keyParameters; - Connection = GetConnection(YubiKey, application, KeyParameters); + Connection = GetConnection(YubiKey, Application, KeyParameters); } - private IYubiKeyConnection GetConnection( + protected IYubiKeyConnection GetConnection( IYubiKeyDevice yubiKey, YubiKeyApplication application, ScpKeyParameters? keyParameters) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs index d100fd218..94d4f54ff 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECKeyParameters.cs @@ -22,6 +22,13 @@ namespace Yubico.YubiKey.Cryptography /// public abstract class ECKeyParameters { + /// + /// Gets the EC parameters associated with this key. + /// + /// + /// These parameters include the curve information and key data. + /// For NIST P-256 keys, this includes the curve definition and public point coordinates. + /// public ECParameters Parameters { get; } protected ECKeyParameters(ECParameters parameters) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs b/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs index 24e0ee42a..dff6d9b13 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs @@ -43,8 +43,9 @@ public class FirmwareVersion : IComparable, IComparable, IEquat internal static readonly FirmwareVersion V5_4_2 = new FirmwareVersion(5, 4, 2); internal static readonly FirmwareVersion V5_4_3 = new FirmwareVersion(5, 4, 3); internal static readonly FirmwareVersion V5_6_0 = new FirmwareVersion(5, 6, 0); + internal static readonly FirmwareVersion V5_6_3 = new FirmwareVersion(5, 6, 3); internal static readonly FirmwareVersion V5_7_0 = new FirmwareVersion(5, 7, 0); - internal static readonly FirmwareVersion V5_7_2 = new FirmwareVersion(5, 7, 2); // Scp11 + internal static readonly FirmwareVersion V5_7_2 = new FirmwareVersion(5, 7, 2); #endregion diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs index a4eb849df..189859e7a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IApduTransform.cs @@ -28,7 +28,7 @@ public interface IApduTransform /// /// Passes the supplied command into the pipeline, and returns the final response. /// - ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType, bool encrypt = false); + ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType); /// /// Sets up the pipeline; should be called only once, before any `Invoke` calls. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs index 760e4967c..2629987cd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyConnection.cs @@ -16,9 +16,18 @@ namespace Yubico.YubiKey { + /// + /// Represents a connection to a YubiKey device, enabling the sending of commands and retrieval of responses. + /// public interface IYubiKeyConnection : IDisposable { - TResponse SendCommand(IYubiKeyCommand yubiKeyCommand, bool encrypt = false) + /// + /// Sends a command to the YubiKey device and returns the response. + /// + /// The type of response expected from the YubiKey device. + /// The command to be sent to the YubiKey device. + /// The response received from the YubiKey device. + TResponse SendCommand(IYubiKeyCommand yubiKeyCommand) where TResponse : IYubiKeyResponse; /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Credential.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Credential.cs index 262fdbb1a..5a3caf2af 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Credential.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Credential.cs @@ -217,13 +217,13 @@ public void AddCredential(Credential credential) } if (credential.RequiresTouch == true && - !_yubiKeyDevice.HasFeature(YubiKeyFeature.OathTouchCredential)) + !YubiKey.HasFeature(YubiKeyFeature.OathTouchCredential)) { throw new InvalidOperationException(ExceptionMessages.TouchNotSupported); } if (credential.Algorithm == HashAlgorithm.Sha512 && - !_yubiKeyDevice.HasFeature(YubiKeyFeature.OathSha512)) + !YubiKey.HasFeature(YubiKeyFeature.OathSha512)) { throw new InvalidOperationException(ExceptionMessages.SHA512NotSupported); } @@ -446,7 +446,7 @@ public Credential RemoveCredential( /// public void RenameCredential(Credential credential, string? newIssuer, string newAccount) { - if (!_yubiKeyDevice.HasFeature(YubiKeyFeature.OathRenameCredential)) + if (!YubiKey.HasFeature(YubiKeyFeature.OathRenameCredential)) { throw new InvalidOperationException(ExceptionMessages.RenameCommandNotSupported); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs index 03f13b193..59ac81b7f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Oath/OathSession.Password.cs @@ -322,10 +322,9 @@ public bool TrySetPassword(ReadOnlyMemory currentPassword, ReadOnlyMemory< } } - var setPasswordResponse = Connection.SendCommand(new SetPasswordCommand(newPassword, _oathData)); - if (setPasswordResponse.Status == ResponseStatus.Success) + var setPasswordResponse = Connection.SendCommand(new SetPasswordCommand(newPassword, _oathData)); + if (setPasswordResponse.Status == ResponseStatus.Success) { - using var unauthenticatedConnection = _yubiKeyDevice.Connect(YubiKeyApplication.Oath); var selectOathResponse = Connection.SendCommand(new SelectOathCommand()); _oathData = selectOathResponse.GetData(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs index f4f28c622..5659c19b2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs @@ -76,7 +76,7 @@ public sealed class OtpSession : ApplicationSession, IOtpSession /// An instance of a class that implements . /// An instance of containing the /// parameters for the SCP03 key. If , the default parameters will be used. - public OtpSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) + public OtpSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Otp, keyParameters) { // Getting the OTP status allows the user to read the OTP status on the OtpSession object. @@ -251,9 +251,9 @@ public NdefDataReader ReadNdefTag() Connection.Dispose(); ReadNdefDataResponse response; - using (var tempConnection = YubiKey.Connect(YubiKeyApplication.OtpNdef)) // TODO Why cant we do this instead of swap connection, open another? + using (Connection = YubiKey.Connect(YubiKeyApplication.OtpNdef)) { - var selectResponse = tempConnection.SendCommand(new SelectNdefDataCommand() { FileID = NdefFileId.Ndef }); + var selectResponse = Connection.SendCommand(new SelectNdefDataCommand() { FileID = NdefFileId.Ndef }); if (selectResponse.Status != ResponseStatus.Success) { throw new InvalidOperationException( @@ -263,11 +263,10 @@ public NdefDataReader ReadNdefTag() selectResponse.StatusMessage)); } - response = tempConnection.SendCommand(new ReadNdefDataCommand()); + response = Connection.SendCommand(new ReadNdefDataCommand()); } - // Connection = YubiKey.Connect(YubiKeyApplication.Otp); - + Connection = GetConnection(YubiKey, YubiKeyApplication.Otp, KeyParameters); return new NdefDataReader(response.GetData().Span); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs index 745191eb3..077ec08ed 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/CommandChainingTransform.cs @@ -67,7 +67,7 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT responseApdu = _pipeline.Invoke(partialApdu, commandType, responseType); } - return responseApdu!; // Covered by Debug.Assert above. TODO What debug assert?? + return responseApdu!; // Covered by Debug.Assert above. } public void Setup() => _pipeline.Setup(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Attestation.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Attestation.cs index a59f67f24..3b99f9fea 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Attestation.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Attestation.cs @@ -114,7 +114,7 @@ public sealed partial class PivSession : IDisposable /// public X509Certificate2 CreateAttestationStatement(byte slotNumber) { - if (_yubiKeyDevice.HasFeature(YubiKeyFeature.PivAttestation)) + if (YubiKey.HasFeature(YubiKeyFeature.PivAttestation)) { // This call will throw an exception if the slot number is incorrect. var command = new CreateAttestationStatementCommand(slotNumber); @@ -181,7 +181,7 @@ public X509Certificate2 CreateAttestationStatement(byte slotNumber) /// public X509Certificate2 GetAttestationCertificate() { - if (_yubiKeyDevice.HasFeature(YubiKeyFeature.PivAttestation)) + if (YubiKey.HasFeature(YubiKeyFeature.PivAttestation)) { var command = new GetDataCommand(AttestationCertTag); var response = Connection.SendCommand(command); @@ -364,7 +364,7 @@ public void ReplaceAttestationKeyAndCertificate(PivPrivateKey privateKey, X509Ce // Return the DER encoding of the certificate. private byte[] CheckVersionKeyAndCertRequirements(PivPrivateKey privateKey, X509Certificate2 certificate) { - if (!_yubiKeyDevice.HasFeature(YubiKeyFeature.PivAttestation)) + if (!YubiKey.HasFeature(YubiKeyFeature.PivAttestation)) { throw new NotSupportedException( string.Format( diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs index 31a5262cf..bf282abd2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Crypto.cs @@ -147,7 +147,7 @@ public byte[] Sign(byte slotNumber, ReadOnlyMemory dataToSign) // This will verify the slot number and dataToSign length. If one or // both are incorrect, the call will throw an exception. var signCommand = new AuthenticateSignCommand(dataToSign, slotNumber); - _yubiKeyDevice.ThrowIfUnsupportedAlgorithm(signCommand.Algorithm); + YubiKey.ThrowIfUnsupportedAlgorithm(signCommand.Algorithm); return PerformPrivateKeyOperation( slotNumber, @@ -399,7 +399,7 @@ private byte[] PerformPrivateKeyOperation( // Metadata will give us our answer, but that feature is // available only on YubiKeys beginning with version 5.3. - if (_yubiKeyDevice.HasFeature(YubiKeyFeature.PivMetadata)) + if (YubiKey.HasFeature(YubiKeyFeature.PivMetadata)) { var metadataCommand = new GetMetadataCommand(slotNumber); var metadataResponse = Connection.SendCommand(metadataCommand); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs index 1d520b0d1..56cd763fa 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.KeyPairs.cs @@ -132,7 +132,7 @@ public PivPublicKey GenerateKeyPair(byte slotNumber, PivPinPolicy pinPolicy = PivPinPolicy.Default, PivTouchPolicy touchPolicy = PivTouchPolicy.Default) { - _yubiKeyDevice.ThrowIfUnsupportedAlgorithm(algorithm); + YubiKey.ThrowIfUnsupportedAlgorithm(algorithm); if (ManagementKeyAuthenticated == false) { @@ -247,7 +247,7 @@ public void ImportPrivateKey(byte slotNumber, throw new ArgumentNullException(nameof(privateKey)); } - _yubiKeyDevice.ThrowIfUnsupportedAlgorithm(privateKey.Algorithm); + YubiKey.ThrowIfUnsupportedAlgorithm(privateKey.Algorithm); if (ManagementKeyAuthenticated == false) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.ManagementKey.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.ManagementKey.cs index 12a7a2071..ec6ff918b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.ManagementKey.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.ManagementKey.cs @@ -238,7 +238,7 @@ public sealed partial class PivSession : IDisposable /// public bool TryAuthenticateManagementKey(bool mutualAuthentication = true) { - _log.LogInformation( + Logger.LogInformation( $"Try to authenticate the management key: {(mutualAuthentication ? "mutual" : "single")} auth."); var currentPinOnlyMode = TryAuthenticatePinOnly(true); @@ -310,7 +310,7 @@ private bool TryAuthenticateWithKeyCollector(bool mutualAuthentication) /// public void AuthenticateManagementKey(bool mutualAuthentication = true) { - _log.LogInformation( + Logger.LogInformation( $"Authenticate the management key: {(mutualAuthentication ? "mutual" : "single")} auth."); if (TryAuthenticateManagementKey(mutualAuthentication) == false) @@ -651,7 +651,7 @@ public bool TryChangeManagementKey(PivTouchPolicy touchPolicy = PivTouchPolicy.D /// public bool TryChangeManagementKey(PivTouchPolicy touchPolicy, PivAlgorithm newKeyAlgorithm) { - _log.LogInformation("Try to change the management key, touch policy = {TouchPolicy}, algorithm = {PivALgorithm}.", + Logger.LogInformation("Try to change the management key, touch policy = {TouchPolicy}, algorithm = {PivALgorithm}.", touchPolicy.ToString(), newKeyAlgorithm.ToString()); CheckManagementKeyAlgorithm(newKeyAlgorithm, true); @@ -761,7 +761,7 @@ public void ChangeManagementKey(PivTouchPolicy touchPolicy = PivTouchPolicy.Defa /// public void ChangeManagementKey(PivTouchPolicy touchPolicy, PivAlgorithm newKeyAlgorithm) { - _log.LogInformation("Change the management key, touch policy = {TouchPolicy}, algorithm = {PivAlgorithm}.", + Logger.LogInformation("Change the management key, touch policy = {TouchPolicy}, algorithm = {PivAlgorithm}.", touchPolicy.ToString(), newKeyAlgorithm.ToString()); if (TryChangeManagementKey(touchPolicy, newKeyAlgorithm) == false) @@ -914,7 +914,7 @@ private bool TryForcedChangeManagementKey(ReadOnlyMemory currentKey, return true; } - _log.LogInformation($"Failed to set management key. Message: {response.StatusMessage}"); + Logger.LogInformation($"Failed to set management key. Message: {response.StatusMessage}"); } @@ -960,7 +960,7 @@ private void CheckManagementKeyAlgorithm(PivAlgorithm algorithm, bool checkMode) case PivAlgorithm.Aes128: case PivAlgorithm.Aes192: case PivAlgorithm.Aes256: - isValid = _yubiKeyDevice.HasFeature(YubiKeyFeature.PivAesManagementKey); + isValid = YubiKey.HasFeature(YubiKeyFeature.PivAesManagementKey); break; @@ -1057,7 +1057,7 @@ private bool TryAuthenticateManagementKey(bool mutualAuthentication, ManagementKeyAuthenticated = true; } - _log.LogInformation($"Failed to authenticate management key. Message: {completeResponse.StatusMessage}"); + Logger.LogInformation($"Failed to authenticate management key. Message: {completeResponse.StatusMessage}"); return ManagementKeyAuthenticated; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Msroots.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Msroots.cs index 472ca4c47..7bd60af95 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Msroots.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Msroots.cs @@ -325,7 +325,7 @@ private Span GetSpanFromStream(Stream contents, out int maxLength) private int CheckWriteLength(string contentsName, long length) { int maxLength = OldMaximumObjectLength; - if (_yubiKeyDevice.FirmwareVersion.Major >= 4) + if (YubiKey.FirmwareVersion.Major >= 4) { maxLength = NewMaximumObjectLength; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Objects.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Objects.cs index f0645e045..7471e2259 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Objects.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Objects.cs @@ -69,7 +69,7 @@ public sealed partial class PivSession : IDisposable /// public T ReadObject() where T : PivDataObject, new() { - _log.LogInformation("Read PivObject " + typeof(T) + "."); + Logger.LogInformation("Read PivObject " + typeof(T) + "."); var returnValue = new T(); if (TryReadObject(returnValue)) @@ -152,7 +152,7 @@ public sealed partial class PivSession : IDisposable /// public bool TryReadObject(out T pivDataObject) where T : PivDataObject, new() { - _log.LogInformation("Try to read PivObject " + typeof(T) + "."); + Logger.LogInformation("Try to read PivObject " + typeof(T) + "."); pivDataObject = new T(); return TryReadObject(pivDataObject); @@ -207,7 +207,7 @@ public sealed partial class PivSession : IDisposable /// public T ReadObject(int dataTag) where T : PivDataObject, new() { - _log.LogInformation("Read PivObject " + typeof(T) + " using dataTag 0x{0:X8}.", dataTag); + Logger.LogInformation("Read PivObject " + typeof(T) + " using dataTag 0x{0:X8}.", dataTag); var returnValue = new T { DataTag = dataTag @@ -299,7 +299,7 @@ public sealed partial class PivSession : IDisposable /// public bool TryReadObject(int dataTag, out T pivDataObject) where T : PivDataObject, new() { - _log.LogInformation("Try to read PivObject " + typeof(T) + "."); + Logger.LogInformation("Try to read PivObject " + typeof(T) + "."); pivDataObject = new T { DataTag = dataTag @@ -384,7 +384,7 @@ public void WriteObject(PivDataObject pivDataObject) { throw new ArgumentNullException(nameof(pivDataObject)); } - _log.LogInformation("Write PivObject " + pivDataObject.GetType() + " to data tag 0x{0:X8}."); + Logger.LogInformation("Write PivObject " + pivDataObject.GetType() + " to data tag 0x{0:X8}."); byte[] dataToStore = Array.Empty(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs index ddc647f95..4a18037a3 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pin.cs @@ -119,7 +119,7 @@ public sealed partial class PivSession : IDisposable /// public bool TryVerifyPin() { - _log.LogInformation("Try to verify the PIV PIN with KeyCollector."); + Logger.LogInformation("Try to verify the PIV PIN with KeyCollector."); if (KeyCollector is null) { @@ -184,7 +184,7 @@ public bool TryVerifyPin() /// public void VerifyPin() { - _log.LogInformation("Verify the PIV PIN."); + Logger.LogInformation("Verify the PIV PIN."); if (TryVerifyPin() == false) { @@ -279,7 +279,7 @@ public void VerifyPin() /// public bool TryVerifyPin(ReadOnlyMemory pin, out int? retriesRemaining) { - _log.LogInformation("Try to verify the PIV PIN with supplied PIN."); + Logger.LogInformation("Try to verify the PIV PIN with supplied PIN."); retriesRemaining = null; PinVerified = false; @@ -404,7 +404,7 @@ public bool TryVerifyPin(ReadOnlyMemory pin, out int? retriesRemaining) /// public void ChangePinAndPukRetryCounts(byte newRetryCountPin, byte newRetryCountPuk) { - _log.LogInformation("Change the PIV PIN and PUK retry counts: {PinCount}, {PukCount}.", newRetryCountPin, + Logger.LogInformation("Change the PIV PIN and PUK retry counts: {PinCount}, {PukCount}.", newRetryCountPin, newRetryCountPuk); // This will validate the input. @@ -525,7 +525,7 @@ public bool TryChangePinAndPukRetryCounts(ReadOnlyMemory managementKey, byte newRetryCountPuk, out int? retriesRemaining) { - _log.LogInformation( + Logger.LogInformation( "Try to change the PIV PIN and PUK retry counts: {PinCount}, {PukCount} with supplied mgmt key and PIN.", newRetryCountPin, newRetryCountPuk); @@ -665,7 +665,7 @@ public bool TryChangePinAndPukRetryCounts(ReadOnlyMemory managementKey, /// public bool TryChangePin() { - _log.LogInformation("Try to change the PIV PIN with KeyCollector."); + Logger.LogInformation("Try to change the PIV PIN with KeyCollector."); if (TryGetChangePinMode(ReadOnlyMemory.Empty, out var mode, out _)) { @@ -699,7 +699,7 @@ public bool TryChangePin() /// public void ChangePin() { - _log.LogInformation("Change the PIV PIN."); + Logger.LogInformation("Change the PIV PIN."); if (!TryChangePin()) { @@ -779,7 +779,7 @@ public void ChangePin() public bool TryChangePin(ReadOnlyMemory currentPin, ReadOnlyMemory newPin, out int? retriesRemaining) { - _log.LogInformation("Try to change the PIV PIN with supplied PINs."); + Logger.LogInformation("Try to change the PIV PIN with supplied PINs."); if (TryGetChangePinMode(currentPin, out var mode, out retriesRemaining)) { @@ -905,7 +905,7 @@ public bool TryChangePin(ReadOnlyMemory currentPin, ReadOnlyMemory n /// public bool TryChangePuk() { - _log.LogInformation("Try to change the PIV PUK with KeyCollector."); + Logger.LogInformation("Try to change the PIV PUK with KeyCollector."); return TryChangeReference(KeyEntryRequest.ChangePivPuk, ChangePinOrPuk, PivPinOnlyMode.None); } @@ -934,7 +934,7 @@ public bool TryChangePuk() /// public void ChangePuk() { - _log.LogInformation("Change the PIV PUK."); + Logger.LogInformation("Change the PIV PUK."); if (TryChangeReference(KeyEntryRequest.ChangePivPuk, ChangePinOrPuk, PivPinOnlyMode.None) == false) { @@ -990,7 +990,7 @@ public void ChangePuk() public bool TryChangePuk(ReadOnlyMemory currentPuk, ReadOnlyMemory newPuk, out int? retriesRemaining) { - _log.LogInformation("Try to change the PIV PUK with supplied PUKs."); + Logger.LogInformation("Try to change the PIV PUK with supplied PUKs."); var command = new ChangeReferenceDataCommand(PivSlot.Puk, currentPuk, newPuk); var response = Connection.SendCommand(command); @@ -1116,7 +1116,7 @@ public bool TryChangePuk(ReadOnlyMemory currentPuk, ReadOnlyMemory n /// public bool TryResetPin() { - _log.LogInformation("Try to reset the PIV PIN using the PIV PUK with KeyCollector."); + Logger.LogInformation("Try to reset the PIV PIN using the PIV PUK with KeyCollector."); if (TryGetChangePinMode(ReadOnlyMemory.Empty, out var pinOnlyMode, out _)) { @@ -1155,7 +1155,7 @@ public bool TryResetPin() /// public void ResetPin() { - _log.LogInformation("Reset the PIV PIN using the PIV PUK."); + Logger.LogInformation("Reset the PIV PIN using the PIV PUK."); if (TryChangeReference(KeyEntryRequest.ResetPivPinWithPuk, ResetPin, PivPinOnlyMode.None) == false) { @@ -1225,7 +1225,7 @@ public void ResetPin() /// public bool TryResetPin(ReadOnlyMemory puk, ReadOnlyMemory newPin, out int? retriesRemaining) { - _log.LogInformation("Try to reset the PIV PIN using the PIV PUK with supplied PUK and PIN."); + Logger.LogInformation("Try to reset the PIV PIN using the PIV PUK with supplied PUK and PIN."); var command = new ResetRetryCommand(puk, newPin); var response = Connection.SendCommand(command); @@ -1383,7 +1383,7 @@ private void UpdateAdminData() { if (!ManagementKeyAuthenticated) { - _log.LogDebug("Unauthenticated attempt to update AdminData failed."); + Logger.LogDebug("Unauthenticated attempt to update AdminData failed."); return; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pinonly.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pinonly.cs index 1bbc20e9b..de76782f4 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pinonly.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Pinonly.cs @@ -86,7 +86,7 @@ public sealed partial class PivSession : IDisposable /// public PivPinOnlyMode GetPinOnlyMode() { - _log.LogInformation("Get the PIV PIN-only mode of a YubiKey based on AdminData."); + Logger.LogInformation("Get the PIV PIN-only mode of a YubiKey based on AdminData."); var pinOnlyMode = PivPinOnlyMode.PinProtectedUnavailable | PivPinOnlyMode.PinDerivedUnavailable; @@ -189,7 +189,7 @@ public PivPinOnlyMode GetPinOnlyMode() /// public PivPinOnlyMode TryRecoverPinOnlyMode() { - _log.LogInformation("Try to authenticate using PIN-only."); + Logger.LogInformation("Try to authenticate using PIN-only."); var pinOnlyMode = TryAuthenticatePinOnly(false); @@ -746,7 +746,7 @@ private PivPinOnlyMode GetPinDerivedStatus( /// public void SetPinOnlyMode(PivPinOnlyMode pinOnlyMode, PivAlgorithm mgmtKeyAlgorithm) { - _log.LogInformation( + Logger.LogInformation( "Set a YubiKey to PIV PIN-only mode: {PivPinOnlyMode}, mgmt key alg = {PivAlgorithm}.", pinOnlyMode.ToString(), mgmtKeyAlgorithm.ToString()); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs deleted file mode 100644 index 3bd2ee570..000000000 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelMac.cs +++ /dev/null @@ -1,108 +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.Security.Cryptography; -using Yubico.Core.Cryptography; -using Yubico.Core.Iso7816; -using Yubico.YubiKey; -using Yubico.YubiKey.Cryptography; -using Yubico.YubiKey.Scp; - -internal static class ChannelMac -{ - public static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( - CommandApdu apdu, - ReadOnlySpan macKey, - ReadOnlySpan macChainingValue) - { - if (macChainingValue.Length != 16) - { - throw new ArgumentException(ExceptionMessages.UnknownScpError, nameof(macChainingValue)); - } - - var apduWithLongerLen = AddDataToApdu(apdu, new byte[8]); - byte[] apduBytesWithZeroMac = apduWithLongerLen.AsByteArray(); - - byte[] macInp = new byte[16 + apduBytesWithZeroMac.Length - 8]; - macChainingValue.CopyTo(macInp); - apduBytesWithZeroMac.AsSpan(0, apduBytesWithZeroMac.Length - 8).CopyTo(macInp.AsSpan(16)); - - byte[] newMacChainingValue = new byte[16]; - using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); - cmacObj.CmacInit(macKey); - cmacObj.CmacUpdate(macInp); - cmacObj.CmacFinal(newMacChainingValue); - - var macdApdu = AddDataToApdu(apdu, newMacChainingValue.AsSpan(0, 8)); - return (macdApdu, newMacChainingValue); - } - - public static void VerifyRmac(ReadOnlySpan response, ReadOnlySpan rmacKey, ReadOnlySpan macChainingValue) - { - if (response.Length < 8) - { - throw new SecureChannelException(ExceptionMessages.InsufficientResponseLengthToVerifyRmac); - } - - if ((response.Length - 8) % 16 != 0) - { - throw new SecureChannelException(ExceptionMessages.IncorrectResponseLengthToDecrypt); - } - - int respDataLen = response.Length - 8; - var recvdRmac = response[^8..]; - - Span macInp = stackalloc byte[16 + respDataLen + 2]; - macChainingValue.CopyTo(macInp); - response[..respDataLen].CopyTo(macInp[16..]); - - macInp[16 + respDataLen] = SW1Constants.Success; - macInp[16 + respDataLen + 1] = SWConstants.Success & 0xFF; - - using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); - Span cmac = stackalloc byte[16]; - cmacObj.CmacInit(rmacKey); - cmacObj.CmacUpdate(macInp); - cmacObj.CmacFinal(cmac); - - if (!CryptographicOperations.FixedTimeEquals(recvdRmac, cmac.Slice(0, 8))) - { - throw new SecureChannelException(ExceptionMessages.IncorrectRmac); - } - } - - private static CommandApdu AddDataToApdu(CommandApdu apdu, ReadOnlySpan data) - { - var newApdu = new CommandApdu - { - Cla = apdu.Cla, - Ins = apdu.Ins, - P1 = apdu.P1, - P2 = apdu.P2 - }; - - int currentDataLength = apdu.Data.Length; - byte[] newData = new byte[currentDataLength + data.Length]; - - if (!apdu.Data.IsEmpty) - { - apdu.Data.Span.CopyTo(newData); - } - - data.CopyTo(newData.AsSpan(currentDataLength)); - newApdu.Data = newData; - return newApdu; - } -} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs index 82730d69f..0cb89629c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateCommand.cs @@ -23,9 +23,9 @@ namespace Yubico.YubiKey.Scp.Commands internal class ExternalAuthenticateCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + internal const byte GpExternalAuthenticateIns = 0x82; private const byte GpExternalAuthenticateCla = 0x80; - private const byte GpExternalAuthenticateIns = 0x82; private const byte GpHighestSecurityLevel = 0x33; private readonly ReadOnlyMemory _data; private readonly byte _keyVersionNumber; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs index e55e3344c..2236d2997 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GenerateEcKeyCommand.cs @@ -19,26 +19,29 @@ namespace Yubico.YubiKey.Scp.Commands { internal class GenerateEcKeyCommand : IYubiKeyCommand { - public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + internal const byte GpGenerateKeyIns = 0xF1; + private readonly ReadOnlyMemory _data; private readonly byte _keyVersionNumber; - private readonly byte _kid; + private readonly byte _keyId; - public GenerateEcKeyCommand(byte keyVersionNumber, byte kid, ReadOnlyMemory data) + public GenerateEcKeyCommand(byte keyVersionNumber, byte keyId, ReadOnlyMemory data) { _data = data; _keyVersionNumber = keyVersionNumber; - _kid = kid; + _keyId = keyId; } - public CommandApdu CreateCommandApdu() => new CommandApdu - { - Cla = 0x80, - Ins = 0xF1, - P1 = _keyVersionNumber, - P2 = _kid, - Data = _data - }; + public CommandApdu CreateCommandApdu() => + new CommandApdu + { + Cla = 0x80, + Ins = GpGenerateKeyIns, + P1 = _keyVersionNumber, + P2 = _keyId, + Data = _data + }; public GenerateEcKeyResponse CreateResponseForApdu(ResponseApdu responseApdu) => new GenerateEcKeyResponse(responseApdu); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs index 711ec1b4a..858d9ae5b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/GetDataCommand.cs @@ -13,49 +13,63 @@ // limitations under the License. using System; -using System.Collections.Generic; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp.Commands { /// - /// Use this command to delete one of the SCP03 key sets on the YubiKey. + /// This class is used to get data from the YubiKey associated with the given tag. + /// For getting data in the Security Domain, it is recommended to use the methods provided by instead, such as + /// , , , and . + /// /// /// - /// See the User's Manual entry on SCP03. - /// - /// This will execute the Delete Command. That is, there is a general purpose - /// command that can delete various elements, including keys. However, this - /// class can build the general purpose delete command in a way that it will - /// only be able to delete keys. - /// /// - /// Note that if all three key sets are deleted, then the first key set (the - /// key set with a KeyVersionNumber of 1) will be the default key set. + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. /// /// + /// The encoded tlv data retrieved from the YubiKey. + /// Thrown when there was an SCP error, described in the exception message. internal class GetDataCommand : IYubiKeyCommand { - private const byte INS_GET_DATA = 0xCA; + internal const byte GetDataIns = 0xCA; private readonly int _tag; private readonly ReadOnlyMemory _data; public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + /// + /// Initializes a new instance of the class. + /// + /// The tag of the data to retrieve from the YubiKey. + /// + /// Optional data to be used in the GetData command. This might be used for + /// certain YubiKey applications, such as the OpenPGP application. + /// + /// + /// For getting data in the Security Domain, + /// it is recommended to use the methods provided by instead, such as + /// , , , and . + /// + /// + /// See GlobalPlatform Technology Card Specification v2.3.1 §11 APDU Command Reference for more information. + /// + /// public GetDataCommand(int tag, ReadOnlyMemory? data = null) { _tag = tag; _data = data ?? ReadOnlyMemory.Empty; } - public CommandApdu CreateCommandApdu() => new CommandApdu - { - Cla = 0, - Ins = INS_GET_DATA, - P1 = (byte)(_tag >> 8), - P2 = (byte)(_tag & 0xFF), - Data = _data - }; + public CommandApdu CreateCommandApdu() => + new CommandApdu + { + Cla = 0, + Ins = GetDataIns, + P1 = (byte)(_tag >> 8), + P2 = (byte)(_tag & 0xFF), + Data = _data + }; public GetDataCommandResponse CreateResponseForApdu(ResponseApdu responseApdu) => new GetDataCommandResponse(responseApdu); @@ -63,10 +77,18 @@ public GetDataCommandResponse CreateResponseForApdu(ResponseApdu responseApdu) = internal class GetDataCommandResponse : ScpResponse, IYubiKeyResponseWithData> { + /// + /// The response to the GetData command. + /// + /// The response APDU from the YubiKey. public GetDataCommandResponse(ResponseApdu responseApdu) : base(responseApdu) { } + /// + /// Gets the data retrieved from the YubiKey. + /// + /// The data retrieved from the YubiKey. public ReadOnlyMemory GetData() => ResponseApdu.Data; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs index e1b8f3b7b..a6773d570 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs @@ -23,8 +23,8 @@ namespace Yubico.YubiKey.Scp.Commands internal class InitializeUpdateCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + internal const byte GpInitializeUpdateIns = 0x50; private const byte GpInitializeUpdateCla = 0x84; - private const byte GpInitializeUpdateIns = 0x50; private readonly ReadOnlyMemory _hostChallenge; private readonly int _keyVersionNumber; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs index d6ac6c9f8..c4559eb54 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateResponse.cs @@ -28,7 +28,7 @@ internal class InitializeUpdateResponse : ScpResponse public IReadOnlyCollection CardCryptogram { get; protected set; } /// - /// Constructs an InitializeUpdateResponse based on a ResponseApdu received from the YubiKey. + /// Constructs an InitializeUpdateResponse based on a ResponseApdu received from the YubiKey after an SCP handshake ('INITIALIZE_UPDATE'). /// /// The ResponseApdu that corresponds to the issuance of /// this command. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InternalAuthenticateCommand.cs similarity index 60% rename from Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs rename to Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InternalAuthenticateCommand.cs index b437f5b28..18b323da2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/InternalAuthenticateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InternalAuthenticateCommand.cs @@ -13,18 +13,29 @@ // limitations under the License. using Yubico.Core.Iso7816; -using Yubico.YubiKey.Scp.Commands; -namespace Yubico.YubiKey.Scp +namespace Yubico.YubiKey.Scp.Commands { + /// + /// Represents the second command in the SCP03 and SCP11a/c authentication handshakes, 'INTERNAL_AUTHENTICATE' + /// + /// + /// Clients should not generally build this manually. See for more. + /// internal class InternalAuthenticateCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; - + internal const byte GpInternalAuthenticateIns = 0x88; private readonly byte _keyReferenceVersionNumber; private readonly byte _keyReferenceId; private readonly byte[] _data; + /// + /// Initializes a new instance of the class. + /// + /// The version number of the key reference. + /// The ID of the key reference. + /// The data to be used for internal authentication. public InternalAuthenticateCommand(byte keyReferenceVersionNumber, byte keyReferenceId, byte[] data) { _keyReferenceVersionNumber = keyReferenceVersionNumber; @@ -36,7 +47,7 @@ public CommandApdu CreateCommandApdu() => new CommandApdu { Cla = 0x80, - Ins = 0x88, + Ins = GpInternalAuthenticateIns, P1 = _keyReferenceVersionNumber, P2 = _keyReferenceId, Data = _data @@ -48,6 +59,11 @@ public InternalAuthenticateResponse CreateResponseForApdu(ResponseApdu responseA internal class InternalAuthenticateResponse : ScpResponse { + /// + /// Creates a new from the provided . + /// + /// The to create the response from. + /// A new . public InternalAuthenticateResponse(ResponseApdu responseApdu) : base(responseApdu) { } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs index dc7695674..b85da7831 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyResponse.cs @@ -18,12 +18,16 @@ namespace Yubico.YubiKey.Scp.Commands { /// - /// The response to putting or replacing SCP03 keys on the YubiKey. + /// The response to putting or replacing SCP keys on the YubiKey. /// internal class PutKeyResponse : ScpResponse, IYubiKeyResponseWithData> { private readonly byte[] _checksum; + /// + /// Initializes a new instance of the class. + /// + /// The response from the YubiKey. public PutKeyResponse(ResponseApdu responseApdu) : base(responseApdu) { @@ -31,6 +35,10 @@ public PutKeyResponse(ResponseApdu responseApdu) responseApdu.Data.CopyTo(_checksum); } + /// + /// Gets the checksum of the stored key. + /// + /// The checksum of the stored key. public ReadOnlyMemory GetData() => _checksum; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs index 6aa555e25..0e94e64c6 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs @@ -24,7 +24,7 @@ namespace Yubico.YubiKey.Scp.Commands /// internal class ResetCommand : IYubiKeyCommand { - public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; private readonly byte[] _data; private readonly byte _keyVersionNumber; private readonly byte _kid; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs index 2fee52d01..0ef2c5475 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ScpResponse.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Diagnostics; using Yubico.Core.Iso7816; @@ -19,30 +20,36 @@ namespace Yubico.YubiKey.Scp.Commands { internal class ScpResponse : YubiKeyResponse { + /// + /// Initializes a new instance of the class. + /// + /// The ResponseApdu from the YubiKey. + /// responseApdu public ScpResponse(ResponseApdu responseApdu) : base(responseApdu) { - } - public virtual new ResponseStatus Status => StatusWord switch - { - SWConstants.Success => ResponseStatus.Success, - _ => ResponseStatus.Failed - }; - - public virtual void ThrowIfFailed(string? message = null) + public void ThrowIfFailed(string? message = null, bool includeStatusWord = true) { - switch (StatusWord) + switch (Status) { - case SWConstants.Success: + case ResponseStatus.Success: Debug.Assert(Status == ResponseStatus.Success); return; + case ResponseStatus.Failed: + case ResponseStatus.RetryWithTouch: + case ResponseStatus.AuthenticationRequired: + case ResponseStatus.ConditionsNotSatisfied: + case ResponseStatus.NoData: default: - throw new SecureChannelException(AddStatusWord(message ?? StatusMessage)); + throw new SecureChannelException( + includeStatusWord + ? AddStatusWord(message ?? StatusMessage) + : message ?? StatusMessage); } - string AddStatusWord(string message) => $"{message} (StatusWord: {StatusWord})"; + string AddStatusWord(string originalMessage) => $"{originalMessage} (StatusWord: {StatusWord})"; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs index bab94092b..7f70d88fb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs @@ -31,10 +31,10 @@ namespace Yubico.YubiKey.Scp.Commands internal class SecurityOperationCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; - + internal const byte GpPerformSecurityOperationIns = 0x2A; + private readonly ReadOnlyMemory _oceCertificates; private readonly byte _oceRefVersionNumber; private readonly byte _oceKeyId; - private readonly ReadOnlyMemory _oceCertificates; /// /// Initializes a new instance of the class. @@ -62,7 +62,7 @@ public CommandApdu CreateCommandApdu() => new CommandApdu { Cla = 0x80, - Ins = 0x2A, + Ins = GpPerformSecurityOperationIns, P1 = _oceRefVersionNumber, P2 = _oceKeyId, Data = _oceCertificates diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs index cb0334234..ea58e4813 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs @@ -13,28 +13,42 @@ // limitations under the License. using System; -using System.Collections.Generic; using Yubico.Core.Iso7816; namespace Yubico.YubiKey.Scp.Commands { - - /// - /// TODO + /// Implements the GlobalPlatform STORE DATA command for transferring data to the Security Domain or Applications. /// + /// + /// This is an internal implementation of the STORE DATA command. For storing data in the Security Domain, + /// it is recommended to use the methods provided by instead, such as + /// , , , and . + /// This command supports single block transfer with BER-TLV formatted data according to ISO 8825. + /// internal class StoreDataCommand : IYubiKeyCommand { - private const byte InsStoreData = 0xE2; + private const byte GpStoreDataIns = 0xE2; private readonly ReadOnlyMemory _data; public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + /// + /// Initializes a new instance of the class, with the given data to be stored. + /// + /// + /// This command supports single block transfer with BER-TLV formatted data according to ISO 8825. + /// For storing data in the Security Domain, + /// it is recommended to use the methods provided by instead, such as + /// , , , and . + /// + /// + /// The data to store, which must be formatted as BER-TLV structures according to ISO 8825. public StoreDataCommand(ReadOnlyMemory data) { _data = data; } - + // The default constructor explicitly defined. We don't want it to be // used. private StoreDataCommand() @@ -42,19 +56,18 @@ private StoreDataCommand() throw new NotImplementedException(); } - public CommandApdu CreateCommandApdu() => new CommandApdu - { - Cla = 0, - Ins = InsStoreData, - P1 = 0x90, - P2 = 0x00, - Data = _data - }; + public CommandApdu CreateCommandApdu() => + new CommandApdu + { + Cla = 0, + Ins = GpStoreDataIns, + P1 = 0x90, + P2 = 0x00, + Data = _data + }; public StoreDataCommandResponse CreateResponseForApdu(ResponseApdu responseApdu) => new StoreDataCommandResponse(responseApdu); - - } internal class StoreDataCommandResponse : ScpResponse, IYubiKeyResponseWithData> diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs similarity index 100% rename from Yubico.YubiKey/src/Yubico/YubiKey/Scp/ChannelEncryption.cs rename to Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs new file mode 100644 index 000000000..267b35b38 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs @@ -0,0 +1,109 @@ +// 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.Security.Cryptography; +using Yubico.Core.Cryptography; +using Yubico.Core.Iso7816; +using Yubico.YubiKey.Cryptography; + +namespace Yubico.YubiKey.Scp.Helpers +{ + internal static class ChannelMac + { + public static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( + CommandApdu apdu, + ReadOnlySpan macKey, + ReadOnlySpan macChainingValue) + { + if (macChainingValue.Length != 16) + { + throw new ArgumentException(ExceptionMessages.UnknownScpError, nameof(macChainingValue)); + } + + var apduWithLongerLen = AddDataToApdu(apdu, new byte[8]); + byte[] apduBytesWithZeroMac = apduWithLongerLen.AsByteArray(); + + byte[] macInp = new byte[16 + apduBytesWithZeroMac.Length - 8]; + macChainingValue.CopyTo(macInp); + apduBytesWithZeroMac.AsSpan(0, apduBytesWithZeroMac.Length - 8).CopyTo(macInp.AsSpan(16)); + + byte[] newMacChainingValue = new byte[16]; + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + cmacObj.CmacInit(macKey); + cmacObj.CmacUpdate(macInp); + cmacObj.CmacFinal(newMacChainingValue); + + var macdApdu = AddDataToApdu(apdu, newMacChainingValue.AsSpan(0, 8)); + return (macdApdu, newMacChainingValue); + } + + public static void VerifyRmac(ReadOnlySpan response, ReadOnlySpan rmacKey, ReadOnlySpan macChainingValue) + { + if (response.Length < 8) + { + throw new SecureChannelException(ExceptionMessages.InsufficientResponseLengthToVerifyRmac); + } + + if ((response.Length - 8) % 16 != 0) + { + throw new SecureChannelException(ExceptionMessages.IncorrectResponseLengthToDecrypt); + } + + int respDataLen = response.Length - 8; + var recvdRmac = response[^8..]; + + Span macInp = stackalloc byte[16 + respDataLen + 2]; + macChainingValue.CopyTo(macInp); + response[..respDataLen].CopyTo(macInp[16..]); + + macInp[16 + respDataLen] = SW1Constants.Success; + macInp[16 + respDataLen + 1] = SWConstants.Success & 0xFF; + + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + Span cmac = stackalloc byte[16]; + cmacObj.CmacInit(rmacKey); + cmacObj.CmacUpdate(macInp); + cmacObj.CmacFinal(cmac); + + if (!CryptographicOperations.FixedTimeEquals(recvdRmac, cmac.Slice(0, 8))) + { + throw new SecureChannelException(ExceptionMessages.IncorrectRmac); + } + } + + private static CommandApdu AddDataToApdu(CommandApdu apdu, ReadOnlySpan data) + { + var newApdu = new CommandApdu + { + Cla = apdu.Cla, + Ins = apdu.Ins, + P1 = apdu.P1, + P2 = apdu.P2 + }; + + int currentDataLength = apdu.Data.Length; + byte[] newData = new byte[currentDataLength + data.Length]; + + if (!apdu.Data.IsEmpty) + { + apdu.Data.Span.CopyTo(newData); + } + + data.CopyTo(newData.AsSpan(currentDataLength)); + newApdu.Data = newData; + return newApdu; + } + } +} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs similarity index 90% rename from Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs rename to Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs index 560ea85fa..f3712487b 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Derivation.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs @@ -16,17 +16,16 @@ using System.Security.Cryptography; using Yubico.Core.Cryptography; using Yubico.YubiKey.Cryptography; -using Yubico.YubiKey.Scp03; -namespace Yubico.YubiKey.Scp +namespace Yubico.YubiKey.Scp.Helpers { internal static class Derivation { - public const byte DDC_SENC = 0x04; - public const byte DDC_SMAC = 0x06; - public const byte DDC_SRMAC = 0x07; - public const byte DDC_CARD_CRYPTOGRAM = 0x00; - public const byte DDC_HOST_CRYPTOGRAM = 0x01; + internal const byte DDC_SENC = 0x04; + internal const byte DDC_SMAC = 0x06; + internal const byte DDC_SRMAC = 0x07; + internal const byte DDC_CARD_CRYPTOGRAM = 0x00; + internal const byte DDC_HOST_CRYPTOGRAM = 0x01; // Derive a key from the challenges. // This method only supports deriving a 64- or 128-bit result based on @@ -64,18 +63,18 @@ public static Memory Derive( cmacObj.CmacUpdate(macInp); cmacObj.CmacFinal(cmac); - if (outputLenBits == 128) // Output is a 128 bit key + if (outputLenBits == 128) // Output is a 128-bit key { return cmac.ToArray(); } - // Output is a cryptogram - byte[] smallerResult = new byte[8]; + // Else, output is a cryptogram + Span smallerResult = stackalloc byte[8]; cmac[..8].CopyTo(smallerResult); CryptographicOperations.ZeroMemory(cmac); - return smallerResult; + return smallerResult.ToArray(); } public static Memory DeriveCryptogram( diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs similarity index 92% rename from Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs rename to Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs index da9699d44..1c61ab063 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Padding.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs @@ -13,9 +13,8 @@ // limitations under the License. using System; -using System.Linq; -namespace Yubico.YubiKey.Scp +namespace Yubico.YubiKey.Scp.Helpers { internal static class Padding { @@ -26,11 +25,6 @@ internal static class Padding /// The padded payload. public static Memory PadToBlockSize(ReadOnlySpan payload) { - if(payload == null) - { - throw new ArgumentNullException(nameof(payload)); - } - int paddedLen = ((payload.Length / 16) + 1) * 16; Span padded = stackalloc byte[paddedLen]; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs index 07713c062..44d8f4937 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs @@ -18,14 +18,14 @@ namespace Yubico.YubiKey.Scp /// The connection class that can perform SCP03 operations will implement not /// only , but this interface as well. /// - public interface IScpYubiKeyConnection : IYubiKeyConnection // TODO Why do I need this? + public interface IScpYubiKeyConnection : IYubiKeyConnection { /// /// Return a reference to the SCP key set used to make the connection. /// public ScpKeyParameters KeyParameters { get; } - internal DataEncryptor? DataEncryptor + internal EncryptDataFunc EncryptDataFunc { get; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs index 472523c8e..db9a6b48c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs @@ -35,8 +35,9 @@ public class KeyReference /// /// Initializes a new instance of the class. /// - /// The ID of the key. - /// The version number of the key. + /// The ID of the key (KID). + /// The version number of the key (KVN). + /// See the GlobalPlatform Technology Card Specification v2.3 Amendment F §5.1 Cryptographic Keys for more information on the available KIDs. public KeyReference( byte id, byte versionNumber) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs index f57a0c51c..e2d368ded 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs @@ -16,8 +16,16 @@ namespace Yubico.YubiKey.Scp { - public class Scp03KeyParameters : ScpKeyParameters + /// + /// Represents the parameters for a Secure Channel Protocol 03 (SCP03) key. + /// + public sealed class Scp03KeyParameters : ScpKeyParameters, IDisposable { + private const int DefaultKvn = 0xFF; + + /// + /// The static keys shared with the device when initiating the connection. + /// public StaticKeys StaticKeys { get; } /// @@ -25,7 +33,7 @@ public class Scp03KeyParameters : ScpKeyParameters /// a Secure Channel Protocol 03 (SCP03) key. /// /// A reference to the key. - /// The static keys shared with the device. + /// The static keys shared with the device when initiating the connection. /// /// Thrown if is greater than 3, which is an invalid Key ID /// for SCP03. @@ -34,12 +42,12 @@ public Scp03KeyParameters( KeyReference keyReference, StaticKeys staticKeys) : base(keyReference) { - if (keyReference.Id > 3) + if (keyReference.Id < 1 || keyReference.Id > 3) { - throw new ArgumentException("Invalid KID for SCP03", nameof(keyReference.Id)); + throw new ArgumentException("Invalid KID for SCP03. Kid must be between 1 and 3", nameof(keyReference.Id)); } - StaticKeys = staticKeys; + StaticKeys = staticKeys.GetCopy(); } /// @@ -63,6 +71,18 @@ public Scp03KeyParameters( /// This property provides a convenient way to access default SCP03 key parameters, /// using the standard SCP03 key identifier and default static keys. /// - public static Scp03KeyParameters DefaultKey => new Scp03KeyParameters(ScpKid.Scp03, 0xFF, new StaticKeys()); + public static Scp03KeyParameters DefaultKey => new Scp03KeyParameters(ScpKeyIds.Scp03, DefaultKvn, new StaticKeys()); + + /// + /// Creates a new instance of , representing the parameters for + /// a Secure Channel Protocol 03 (SCP03) key, using the standard SCP03 key identifier and + /// the given static keys. + /// + /// The static keys shared with the device. + /// An instance of . + public static Scp03KeyParameters FromStaticKeys(StaticKeys staticKeys) => + new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); + + public void Dispose() => StaticKeys.Dispose(); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs index 0b42bbfc0..2bb775f67 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Security.Cryptography; using Yubico.YubiKey.Scp.Commands; +using Yubico.YubiKey.Scp.Helpers; namespace Yubico.YubiKey.Scp { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs index 3afdc5922..29d14dbdb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpConnection.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using Yubico.Core.Devices.SmartCard; using Yubico.YubiKey.Pipelines; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs index ad070bac4..d47853b00 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs @@ -16,7 +16,10 @@ namespace Yubico.YubiKey.Scp { - public abstract class ScpKeyParameters //TODO handle dispose like static keys? Use ReadOnlyMemorySpan? + /// + /// Abstract base class for parameters for a Secure Channel Protocol (SCP) key. + /// + public abstract class ScpKeyParameters { public KeyReference KeyReference { get; protected set; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs deleted file mode 100644 index 3e7d39bbe..000000000 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKid.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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. - -namespace Yubico.YubiKey.Scp -{ - public static class ScpKid - { - public const byte Scp03 = 0x01; - public const byte Scp11a = 0x11; - public const byte Scp11b = 0x13; - public const byte Scp11c = 0x15; - } -} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs index e09241507..3f2bbbcfe 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs @@ -38,63 +38,12 @@ public class StaticKeys : IDisposable { private const int KeySizeBytes = 16; - private const byte MinimumKvnValue = 1; - private const byte MaximumKvnValue = 3; - private const byte DefaultKvnValue = 0xff; - - private byte _keyVersionNumber; - private readonly byte[] _macKey = new byte[KeySizeBytes]; private readonly byte[] _encKey = new byte[KeySizeBytes]; private readonly byte[] _dekKey = new byte[KeySizeBytes]; private bool _disposed; - /// - /// The number that identifies the key set. Unless specified by the - /// caller, this class will assume the Key Version Number is 1, or else - /// 255 if the default keys are used. - /// - /// - /// When the SDK makes an SCP03 Connection with a YubiKey, it will - /// specify the Key Version Number. In that way, the YubiKey will know - /// which keys to use to complete the handshake. - /// - /// A YubiKey can store up to three sets of SCP03 keys. You can think of - /// it as if the YubiKey contains three slots (1, 2, and 3) for SCP03 - /// keys. Each set (slot) is specified by a number, which the standard - /// calls the Key Version Number. On a YubiKey, the only numbers allowed - /// to be a Key Version Number are 255, 1, 2, and 3. - /// - /// - /// Most YubiKeys are manufactured with a default set of SCP03 keys in - /// slot 1. Slots 2 and 3 are empty. The initial, default set of keys in - /// slot 1 is given the Key Version Number 255. If you replace those - /// keys, the replacement must be specified as number 1. If you want to - /// add key sets to the other two slots, you must use the numbers 2 and - /// 3. Note that you cannot set the two empty slots until the initial, - /// default keys are replaced. - /// - /// - /// If the Key Version Number to use is not the value this class uses by - /// default, then set this value after constructing the object. - /// - /// - public byte KeyVersionNumber - { - get => _keyVersionNumber; - - set - { - if (value != DefaultKvnValue && (value < MinimumKvnValue || value > MaximumKvnValue)) - { - throw new ArgumentException(ExceptionMessages.InvalidScp03Kvn); - } - - _keyVersionNumber = value; - } - } - /// /// AES128 shared secret key used to calculate the Session-MAC key. Also /// called the 'DMK' or 'Key-MAC' in some specifications. @@ -116,9 +65,6 @@ public byte KeyVersionNumber /// /// Constructs an instance given the supplied keys. This class will /// consider these keys to be the key set with the Key Version Number of - /// 1. If the key version number should be something else, set the - /// property after calling the constructor. - /// /// /// This class will copy the input key data, not just a reference. You /// can overwrite the input buffers as soon as the StaticKeys @@ -127,6 +73,7 @@ public byte KeyVersionNumber /// 16-byte AES128 shared secret key /// 16-byte AES128 shared secret key /// 16-byte AES128 shared secret key + /// public StaticKeys(ReadOnlyMemory channelMacKey, ReadOnlyMemory channelEncryptionKey, ReadOnlyMemory dataEncryptionKey) @@ -147,7 +94,6 @@ public StaticKeys(ReadOnlyMemory channelMacKey, } SetKeys(channelMacKey, channelEncryptionKey, dataEncryptionKey); - KeyVersionNumber = 1; _disposed = false; } @@ -166,11 +112,10 @@ public StaticKeys() }); SetKeys(DefaultKey, DefaultKey, DefaultKey); - KeyVersionNumber = 255; _disposed = false; } - + private void SetKeys(ReadOnlyMemory channelMacKey, ReadOnlyMemory channelEncryptionKey, ReadOnlyMemory dataEncryptionKey) @@ -184,7 +129,6 @@ private void SetKeys(ReadOnlyMemory channelMacKey, internal StaticKeys GetCopy() => new StaticKeys(ChannelMacKey, ChannelEncryptionKey, DataEncryptionKey) { - KeyVersionNumber = KeyVersionNumber }; /// @@ -198,7 +142,7 @@ public bool AreKeysSame(StaticKeys? compareKeys) return false; } - return + return ChannelEncryptionKey.Span.SequenceEqual(compareKeys.ChannelEncryptionKey.Span) && ChannelMacKey.Span.SequenceEqual(compareKeys.ChannelMacKey.Span) && DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs index 7a903eabc..56a6be1ee 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/StaticKeys.cs @@ -188,7 +188,7 @@ internal StaticKeys GetCopy() => { KeyVersionNumber = KeyVersionNumber }; - + internal Scp03KeyParameters ConvertToScp03KeyParameters() => new Scp03KeyParameters(ScpKeyIds.Scp03, 0xFF, ConvertFromLegacy()); @@ -207,7 +207,7 @@ public bool AreKeysSame(StaticKeys? compareKeys) ChannelMacKey.Span.SequenceEqual(compareKeys.ChannelMacKey.Span) && DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span); } - + private Scp.StaticKeys ConvertFromLegacy() => new Scp.StaticKeys(ChannelMacKey, ChannelEncryptionKey, DataEncryptionKey) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiHsmAuth/YubiHsmAuthSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiHsmAuth/YubiHsmAuthSession.cs index 3e8d7d266..5126a2ced 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiHsmAuth/YubiHsmAuthSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiHsmAuth/YubiHsmAuthSession.cs @@ -14,7 +14,9 @@ using System; using System.Globalization; -using Yubico.YubiKey.InterIndustry.Commands; +using Yubico.Core.Logging; +using Yubico.YubiKey.Oath; +using Yubico.YubiKey.Scp; using Yubico.YubiKey.YubiHsmAuth.Commands; namespace Yubico.YubiKey.YubiHsmAuth @@ -22,15 +24,8 @@ namespace Yubico.YubiKey.YubiHsmAuth /// /// The main entry-point for all YubiHSM Auth related operations. /// - public sealed partial class YubiHsmAuthSession : IDisposable + public sealed partial class YubiHsmAuthSession : ApplicationSession { - private bool _disposed; - - /// - /// The object that represents the connection to the YubiKey. - /// - public IYubiKeyConnection Connection { get; private set; } - /// /// The delegate this class will call when it needs a management key or /// credential password. @@ -55,12 +50,6 @@ public sealed partial class YubiHsmAuthSession : IDisposable /// public Func? KeyCollector { get; set; } - // The default constructor explicitly defined. We don't want it to be used. - private YubiHsmAuthSession() - { - throw new NotImplementedException(); - } - /// /// Create an instance of YubiHsmAuthSession class, the object /// that represents the YubiHSM Auth application on the YubiKey. @@ -90,22 +79,16 @@ private YubiHsmAuthSession() /// /// The object that represents the actual YubiKey which will perform the operations. /// + /// If supplied, the parameters used open the SCP connection. /// /// The yubiKey argument is null. /// /// /// Failed to connect to the YubiHSM Auth application. /// - public YubiHsmAuthSession(IYubiKeyDevice yubiKey) + public YubiHsmAuthSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null) + : base(Log.GetLogger(), yubiKey, YubiKeyApplication.YubiHsmAuth, keyParameters) { - if (yubiKey is null) - { - throw new ArgumentNullException(nameof(yubiKey)); - } - - Connection = yubiKey.Connect(YubiKeyApplication.YubiHsmAuth); - - _disposed = false; } /// @@ -159,27 +142,5 @@ private Func GetKeyCollector() return KeyCollector; } - - /// - /// When the YubiHsmAuthSession object goes out of scope, this method is called. It will close the session. - /// - // Note that .NET recommends a Dispose method call Dispose(true) and GC.SuppressFinalize(this). - // The actual disposal is in the Dispose(bool) method. - // - // However, that does not apply to sealed classes. So the Dispose method will simply perform the - // "closing" process, no call to Dispose(bool) or GC. - public void Dispose() - { - if (_disposed) - { - return; - } - - // At the moment, there is no "close session" method. So for now, - // just connect to the management application. - _ = Connection.SendCommand(new SelectApplicationCommand(YubiKeyApplication.Management)); - Connection.Dispose(); - _disposed = true; - } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyResponse.cs index 7ac50887b..89857e220 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyResponse.cs @@ -151,12 +151,7 @@ public class YubiKeyResponse : IYubiKeyResponse /// responseApdu public YubiKeyResponse(ResponseApdu responseApdu) { - if (responseApdu is null) - { - throw new ArgumentNullException(nameof(responseApdu)); - } - - ResponseApdu = responseApdu; + ResponseApdu = responseApdu ?? throw new ArgumentNullException(nameof(responseApdu)); } /// 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 dda84ae52..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Otp/ConfigureStaticTests.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.Devices.Hid; -using Yubico.YubiKey.Otp.Operations; -using Yubico.YubiKey.Scp; -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, 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 d609e9feb..4763da4f0 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs @@ -60,7 +60,7 @@ public void GenerateAndSign(PivAlgorithm algorithm) Assert.True(testDevice.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Piv)); Assert.True(testDevice is YubiKeyDevice); - // if (testDevice is YubiKeyDevice device) + // if (testDevice is YubiKeyDevice device) TODO // { // #pragma warning disable CS0618 // Specifically testing this feature // // testDevice = device.WithScp03(new StaticKeys()); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs deleted file mode 100644 index a8e87a02e..000000000 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Commands/DeleteKeyCommandTests.cs +++ /dev/null @@ -1,150 +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.Scp; -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)] - [Obsolete("Obsolete")] - 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 Scp03.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 Scp03.Commands.DeleteKeyCommand(1, false); - Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - - [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5Fips)] - [InlineData(StandardTestDevice.Fw5)] - public void DeleteKey_One_Succeeds2(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 Scp03KeyParameters(ScpKid.Scp03, 3, new StaticKeys(key1, key2, key3)); - var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - var isValid = testDevice.TryConnectScp(YubiKeyApplication.Scp03, currentKeys, out var connection); - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new Scp03.Commands.DeleteKeyCommand(1, false); - var 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, - }; -#pragma warning disable CS0618 // Type or member is obsolete - var currentKeys = new Scp03.StaticKeys(key2, key1, key3) -#pragma warning restore CS0618 // Type or member is obsolete - { - KeyVersionNumber = 3 - }; - - IYubiKeyDevice testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - //TODO -#pragma warning disable CS0618 // Type or member is obsolete - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -#pragma warning restore CS0618 // Type or member is obsolete - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new Scp03.Commands.DeleteKeyCommand(2, false); - Scp03.Commands.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); - //TODO -#pragma warning disable CS0618 // Type or member is obsolete - var isValid = testDevice.TryConnectScp03(YubiKeyApplication.Scp03, currentKeys, out IScp03YubiKeyConnection? connection); -#pragma warning restore CS0618 // Type or member is obsolete - - Assert.True(isValid); - Assert.NotNull(connection); - - var cmd = new Scp03.Commands.DeleteKeyCommand(3, true); - Scp03.Commands.Scp03Response rsp = connection!.SendCommand(cmd); - Assert.Equal(ResponseStatus.Success, rsp.Status); - } - } -} diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 0172f1d75..4a4e04e2f 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Yubico AB +// 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. @@ -128,7 +128,7 @@ public void Scp11b_OtpSession_Operations_Succeeds( configObj = configObj.WithKeyboard(KeyboardLayout.en_US); configObj = configObj.GeneratePassword(generatedPassword); - configObj.Execute(); ; + configObj.Execute(); } [SkippableTheory(typeof(DeviceNotFoundException))] @@ -473,14 +473,14 @@ static ECParameters ConvertToECParameters( Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters bcPrivateKey) { // Convert the BigInteger D to byte array - byte[] dBytes = bcPrivateKey.D.ToByteArrayUnsigned(); + 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 - byte[] xBytes = Q.XCoord.ToBigInteger().ToByteArrayUnsigned(); - byte[] yBytes = Q.YCoord.ToBigInteger().ToByteArrayUnsigned(); + var xBytes = Q.XCoord.ToBigInteger().ToByteArrayUnsigned(); + var yBytes = Q.YCoord.ToBigInteger().ToByteArrayUnsigned(); // Create ECParameters with P-256 curve return new ECParameters @@ -503,20 +503,20 @@ private ScpCertificates GetOceCertificates( var certificates = new List(); // Convert PEM to a string - string pemString = Encoding.UTF8.GetString(pem); + var pemString = Encoding.UTF8.GetString(pem); // Split the PEM string into individual certificates - string[] pemCerts = pemString.Split( + var pemCerts = pemString.Split( new[] { "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----" }, StringSplitOptions.RemoveEmptyEntries ); - foreach (string certString in pemCerts) + foreach (var certString in pemCerts) { if (!string.IsNullOrWhiteSpace(certString)) { // Remove any whitespace and convert to byte array - byte[] certData = Convert.FromBase64String(certString.Trim()); + var certData = Convert.FromBase64String(certString.Trim()); var cert = new X509Certificate2(certData); certificates.Add(cert); } 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/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs index 41e1cee5b..38bac23f1 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Fido2SessionTests.cs @@ -43,7 +43,7 @@ void Constructor_ValidYubiKeyDevice_Succeeds() var mockResponse = new GetInfoResponse(new ResponseApdu(Fido2InfoTests.GetSampleEncoded(), SWConstants.Success)); _ = mockConnection - .Setup(c => c.SendCommand(It.IsAny>(), It.IsAny())) + .Setup(c => c.SendCommand(It.IsAny>())) .Returns(mockResponse); _ = mockYubiKey @@ -63,7 +63,7 @@ void Constructor_GivenValidYubiKeyDevice_ConnectsToFido2Application() var mockResponse = new GetInfoResponse(new ResponseApdu(Fido2InfoTests.GetSampleEncoded(), SWConstants.Success)); _ = mockConnection - .Setup(c => c.SendCommand(It.IsAny>(), It.IsAny())) + .Setup(c => c.SendCommand(It.IsAny>())) .Returns(mockResponse); _ = mockYubiKey @@ -83,7 +83,7 @@ void GetAuthenticatorInfo_SendsGetInfoCommand() var mockResponse = new GetInfoResponse(new ResponseApdu(Fido2InfoTests.GetSampleEncoded(), SWConstants.Success)); _ = mockConnection - .Setup(c => c.SendCommand(It.IsAny>(),It.IsAny())) + .Setup(c => c.SendCommand(It.IsAny>())) .Returns(mockResponse); _ = mockYubiKey @@ -92,9 +92,7 @@ void GetAuthenticatorInfo_SendsGetInfoCommand() var session = new Fido2Session(mockYubiKey.Object); - //session.AuthenticatorInfo; - - mockConnection.Verify(c => c.SendCommand(It.IsAny(),It.IsAny())); + mockConnection.Verify(c => c.SendCommand(It.IsAny())); } } } diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs index 068224ddb..c4ebb4e40 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/CommandChainingTransformTests.cs @@ -81,7 +81,7 @@ public void Invoke_CommandApduWithoutData_InvokesNextTransformWithSameApdu() // Assert mockTransform.Verify( x => - x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny(),It.IsAny())); + x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny())); } [Fact] @@ -91,7 +91,7 @@ public void Invoke_CommandApduWithoutData_ReturnsNextTransformResponseAsIs() var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new CommandChainingTransform(mockTransform.Object); @@ -118,7 +118,7 @@ public void Invoke_CommandApduWithSmallData_InvokesNextTransformWithSameApdu() // Assert mockTransform.Verify( x => - x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny(),It.IsAny())); + x.Invoke(It.Is(a => a == commandApdu), It.IsAny(), It.IsAny())); } [Fact] @@ -128,7 +128,7 @@ public void Invoke_CommandApduWithSmallData_ReturnsNextTransformResponseAsIs() var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new CommandChainingTransform(mockTransform.Object); @@ -152,7 +152,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_OrsHex10ToClaOnAllExceptLast() var commandApdu = new CommandApdu { Data = Enumerable.Repeat(0xFF, 16).ToArray() }; _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((a, b, c) => observedCla.Add(a.Cla)); // Act @@ -179,7 +179,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_AllOtherApduPropertiesRemainUn }; _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((a, b, c) => observedApdus.Add(a)); // Act @@ -208,7 +208,7 @@ public void Invoke_CommandApduWithLargeDataBuffer_SplitsDataAcrossInvokeCalls() }; _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((a, b, c) => observedApdus.Add(a.Data.ToArray())); // Act diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs index 89f884cf2..224e435ab 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/OathResponseChainingTransformTests.cs @@ -71,7 +71,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Arrange var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ResponseApdu(new byte[] { 0x90, 0x00 })); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -80,7 +80,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] @@ -90,7 +90,7 @@ public void Invoke_SuccessfulResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -108,7 +108,7 @@ public void Invoke_FailedResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -127,7 +127,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -137,7 +137,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -150,7 +150,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -160,7 +160,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() // Assert mockTransform.Verify(x => - x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -172,7 +172,7 @@ public void Invoke_BytesAvailble_ConcatsAllBuffers() var response2 = new ResponseApdu(new byte[] { 5, 6, 7, 8, SW1Constants.Success, 0x00 }); byte[] expectedData = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); @@ -192,7 +192,7 @@ public void Invoke_BytesAvailable_ReturnsLastStatusWord() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new OathResponseChainingTransform(mockTransform.Object); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs index 2a4b80636..b91c3f99b 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ResponseChainingTransformTests.cs @@ -71,7 +71,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Arrange var mockTransform = new Mock(); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ResponseApdu(new byte[] { 0x90, 0x00 })); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -80,7 +80,7 @@ public void Invoke_Called_CallsNextTransformInvokeMethod() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] @@ -90,7 +90,7 @@ public void Invoke_SuccessfulResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { 0x90, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -108,7 +108,7 @@ public void Invoke_FailedResponseApdu_ReturnsResponse() var mockTransform = new Mock(); var expectedResponse = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(expectedResponse); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -127,7 +127,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -137,7 +137,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeTwice() // Assert mockTransform.Verify(x => - x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + x.Invoke(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -150,7 +150,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.Success, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -160,7 +160,7 @@ public void Invoke_BytesAvailableThenSuccess_CallsInvokeWithCorrectIns() // Assert mockTransform.Verify(x => - x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + x.Invoke(It.Is(c => c.Ins == expectedIns), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -172,7 +172,7 @@ public void Invoke_BytesAvailble_ConcatsAllBuffers() var response2 = new ResponseApdu(new byte[] { 5, 6, 7, 8, SW1Constants.Success, 0x00 }); byte[] expectedData = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); @@ -192,7 +192,7 @@ public void Invoke_BytesAvailable_ReturnsLastStatusWord() var response1 = new ResponseApdu(new byte[] { SW1Constants.BytesAvailable, 0x00 }); var response2 = new ResponseApdu(new byte[] { SW1Constants.NoPreciseDiagnosis, 0x00 }); _ = mockTransform - .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .SetupSequence(x => x.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response1) .Returns(response2); var transform = new ResponseChainingTransform(mockTransform.Object); diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs index ad939c734..1283caee5 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs @@ -30,8 +30,7 @@ public class PipelineFixture : IApduTransform public ResponseApdu Invoke( CommandApdu command, Type commandType, - Type responseType, - bool encrypt = false) + Type responseType) { if (command is null) { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs index 84d05794a..1ce7fd438 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelEncryptionTests.cs @@ -15,6 +15,7 @@ using System; using Xunit; using Yubico.Core.Buffers; +using Yubico.YubiKey.Scp.Helpers; namespace Yubico.YubiKey.Scp { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs index 8f0f2013a..7efd3e9ac 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/ChannelMacTests.cs @@ -16,6 +16,7 @@ using Xunit; using Yubico.Core.Buffers; using Yubico.Core.Iso7816; +using Yubico.YubiKey.Scp.Helpers; namespace Yubico.YubiKey.Scp { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs index f279ba825..45aff3055 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/DerivationTests.cs @@ -15,6 +15,8 @@ using System; using Xunit; using Yubico.Core.Buffers; +using Yubico.YubiKey.Scp.Helpers; + namespace Yubico.YubiKey.Scp { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs index c52aa217c..647721978 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs @@ -15,6 +15,7 @@ using System; using Xunit; using Yubico.Core.Buffers; +using Yubico.YubiKey.Scp.Helpers; namespace Yubico.YubiKey.Scp { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs index 5c2c5bc46..f71e42120 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/YubikeyApplicationTests.cs @@ -13,63 +13,55 @@ // limitations under the License. using System; -using System.Collections.ObjectModel; 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()); - } + [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 ApplicationIds_ReturnsReadOnlyDictionary() - { - var result = YubiKeyApplicationExtensions.Iso7816ApplicationIds; - Assert.IsType>>(result); + [Fact] + public void GetIso7816ApplicationId_ThrowsForUnsupportedApplication() + { + var unsupportedApp = (YubiKeyApplication)999; + Assert.Throws(() => unsupportedApp.GetIso7816ApplicationId()); } - [Fact] - public void ApplicationIds_ContainsAllApplications() - { + [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 - } + 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) - { + [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 }; + [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/HollowConnection.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs index 41d9718a5..0101756ef 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowConnection.cs @@ -59,7 +59,7 @@ public HollowConnection(YubiKeyApplication yubikeyApplication, FirmwareVersion f _firmwareVersion = firmwareVersion; } - public TResponse SendCommand(IYubiKeyCommand yubiKeyCommand, bool encrypt = false) + public TResponse SendCommand(IYubiKeyCommand yubiKeyCommand) where TResponse : IYubiKeyResponse { if (yubiKeyCommand is SelectApplicationCommand) diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs index 91d44178e..e3c8f741e 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/HollowYubiKeyDevice.cs @@ -277,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 1f11eb966..e26d2ee94 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); /// /// Get YubiKey test device of specified transport and for which the @@ -128,11 +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/TestDeviceSelection.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs index 09662c0ea..2084ee867 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs @@ -27,7 +27,8 @@ 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 @@ -54,14 +55,20 @@ 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 allow-list filtered YubiKey that was found. public static IYubiKeyDevice SelectByStandardTestDevice( this IEnumerable yubiKeys, - StandardTestDevice testDevice) + StandardTestDevice testDeviceType, + FirmwareVersion? minimumFirmwareVersion = null) { var devices = yubiKeys as IYubiKeyDevice[] ?? yubiKeys.ToArray(); if (!devices.Any()) @@ -69,37 +76,67 @@ public static IYubiKeyDevice SelectByStandardTestDevice( throw new InvalidOperationException("Could not find any connected Yubikeys"); } - 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.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})", devices); } return device; } } + private static FirmwareVersion MatchVersion( + StandardTestDevice testDeviceType, + FirmwareVersion? minimumFirmwareVersion) + { + if (minimumFirmwareVersion is { }) + { + return minimumFirmwareVersion; + } + + switch (testDeviceType) + { + case StandardTestDevice.Fw3: + return FirmwareVersion.V3_1_0; + case StandardTestDevice.Fw4Fips: + return FirmwareVersion.V4_0_0; + case StandardTestDevice.Fw5: + case StandardTestDevice.Fw5Fips: + case StandardTestDevice.Fw5Bio: + default: + return FirmwareVersion.V5_0_0; + } + } + public static IYubiKeyDevice SelectByMinimumVersion( this IEnumerable yubiKeys, FirmwareVersion minimumFirmwareVersion) @@ -119,13 +156,16 @@ public static IYubiKeyDevice SelectByMinimumVersion( return device!; } - private static void ThrowDeviceNotFoundException(string errorMessage, IYubiKeyDevice[] devices) + 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 +179,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) + { + } } } From 01b6ab265cd95e2e6a0492528db725ac7d8f2036 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 19:30:09 +0100 Subject: [PATCH 09/53] docs: add comments to TlvObject --- Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs | 24 ++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs index a8aac8319..08f703258 100644 --- a/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs +++ b/Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Yubico AB +// 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. @@ -43,38 +43,58 @@ public class TlvObject private readonly int _offset; /// - /// Creates a new Tlv given a tag and a value. + /// 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(); } From c41fff1da06e0a98a5a34678839f233755b32976 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 19:30:39 +0100 Subject: [PATCH 10/53] docs: fix incorrect values in docs for RSA key sizes and their identifier --- .../application-piv/apdu/auth-sign.md | 23 +++++++++---------- .../application-piv/apdu/generate-pair.md | 18 +++++++-------- .../application-piv/apdu/import-asym.md | 2 +- .../application-piv/apdu/metadata.md | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md index d13e61a58..fc86ba433 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md @@ -17,18 +17,18 @@ limitations under the License. --> ### Command APDU Info | CLA | INS | P1 | P2 | Lc | Data | Le | -|:---:|:---:|:-----------:|:-------------:|:----------:|:--------------------------------:|:--------:| -| 00 | 87 | *algorithm* | *slot number* | *data len* | *encoded digest of data to sign* | (absent) | +| :-: | :-: | :---------: | :-----------: | :--------: | :------------------------------: | :------: | +| 00 | 87 | _algorithm_ | _slot number_ | _data len_ | _encoded digest of data to sign_ | (absent) | -The *algorithm* is either `06` (RSA-1024), `07` (RSA-2048), `05` (RSA 3072), `16` (RSA 4096), `11` (ECC-P256), or `14` +The _algorithm_ is either `06` (RSA-1024), `07` (RSA-2048), `05` (RSA 3072), `16` (RSA 4096), `11` (ECC-P256), or `14` (ECC-P384). -The *slot number* can be the number of any slot that holds a private key, other than `F9`. +The _slot number_ can be the number of any slot that holds a private key, other than `F9`. That is, the slot number can be any PIV slot other than `80`, `81`, `9B`, or `F9`. The attestation key, `F9`, will sign a certificate it creates, so it can sign. It simply cannot sign arbitrary data, only attestation statements. -The *encoded digest* is +The _encoded digest_ is ```C 7C len1 82 00 81 len2 @@ -79,12 +79,12 @@ For ECC, there is one format: #### Response APDU for AUTHENTICATE:SIGN (success) -Total Length: *variable + 2*\ -Data Length: *variable* +Total Length: _variable + 2_\ +Data Length: _variable_ | Data | SW1 | SW2 | -|:---------------------------------:|:---:|:---:| -| 7C *len1* 82 *len2 \* | 90 | 00 | +| :-------------------------------: | :-: | :-: | +| 7C _len1_ 82 _len2 \_ | 90 | 00 | Note that the signature might be returned over multiple commands. Each return command will be able to return up to 256 bytes. To get more bytes of a return, call the GET @@ -122,8 +122,8 @@ Total Length: 2\ Data Length: 0 | Data | SW1 | SW2 | -|:---------:|:---:|:---:| -| (no data) | 69 | 82 | +| :-------: | :-: | :-: | +| (no data) | 69 | 82 | If the key was generated or imported with a PIN policy other than "Never", and the command was sent without first verifying the PIN or the wrong PIN was entered, then the following @@ -190,4 +190,3 @@ Received (SW1=0x90, SW2=0x00): Sending: 00 C0 00 00 Received (SW1=0x6A, SW2=0x80) ``` - diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md index 3de7a42a2..940013584 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md @@ -16,9 +16,9 @@ limitations under the License. --> ### Command APDU Info -| CLA | INS | P1 | P2 | Lc | Data | Le | -|:---:|:---:|:--:|:-------------:|:----------:|:--------------------------------------------------------------------------------------------------------------:|:--------:| -| 00 | 47 | 00 | *Slot number* | *data len* | [AC *\* 80 01 *\*]
\[AA 01 *\*\]
\[AB 01 *\*\] | (absent) | +| CLA | INS | P1 | P2 | Lc | Data | Le | +| :-: | :-: | :-: | :-----------: | :--------: | :------------------------------------------------------------------------------------------------------------: | :------: | +| 00 | 47 | 00 | _Slot number_ | _data len_ | [AC *\* 80 01 *\*]
\[AA 01 _\_\]
\[AB 01 _\_\] | (absent) | The slot number can be one of the following (hex values): @@ -36,7 +36,7 @@ The value for the "remaining bytes" field must be equal to the number of bytes t bytes come after the "remaining bytes" field, the field's value must be 03. There are six choices for "alg" (algorithm and size): RSA-1024 (06), -RSA-2048 (07), RSA 3072 (08), RSA 4096 (09), ECC-P-256 (11), and ECC-P-384 (14). +RSA-2048 (07), RSA 3072 (05), RSA 4096 (16), ECC-P-256 (11), and ECC-P-384 (14). Both the PIN policy and touch policy are optional. If either or both are not given, they will be default. The default for PIN is "once" and touch is "never". @@ -65,17 +65,17 @@ Total Length: 2\ Data Length: 0 | Data | SW1 | SW2 | -|:---------:|:---:|:---:| +| :-------: | :-: | :-: | | (no data) | 69 | 82 | ### Response APDU Info: Success -Total Length: *variable + 2*\ -Data Length: *variable* +Total Length: _variable + 2_\ +Data Length: _variable_ | Data | SW1 | SW2 | -|:------------:|:---:|:---:| -| *public key* | 90 | 00 | +| :----------: | :-: | :-: | +| _public key_ | 90 | 00 | The public key is in the form of a set of TLVs. If the key is ECC, there is one TLV, where the value (the V) is the public point. If the key is RSA, there are two TLVs, where the diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md index 4be530bc8..15524bee2 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/import-asym.md @@ -30,7 +30,7 @@ F9 ``` There are six choices for "alg" (algorithm and size): RSA-1024 (06), -RSA-2048 (07), RSA 3072 (08), RSA 4096 (09), ECC-P-256 (11), and ECC-P-384 (14). +RSA-2048 (07), RSA 3072 (05), RSA 4096 (16), ECC-P-256 (11), and ECC-P-384 (14). The key data to load is a set of TLV constructions. The L (length) is DER encoding format. The V is the integer in canonical form. If the key is an RSA private key, there diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md index 4db309d94..592b69cb4 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/metadata.md @@ -41,7 +41,7 @@ rules. The values (V of TLV) are dependent on the tags, described in the table b Tag | Name | Meaning | Data | Slots :---: | :---: | :---: | :---: -01 | Algorithm| Algorithm/Type of the key | ff (PIN or PUK), 03 (Triple DES), 08 (AES-128),
0A (AES-192), 0C (AES-256),
06 (RSA-1024), 07 (RSA-2048),
08 (RSA 3072), 09 (RSA 4096)
11 (ECC-P256), or 14 (ECC-P384) | all slots +01 | Algorithm| Algorithm/Type of the key | ff (PIN or PUK), 03 (Triple DES), 08 (AES-128),
0A (AES-192), 0C (AES-256),
06 (RSA-1024), 07 (RSA-2048),
05 (RSA 3072), 16 (RSA 4096)
11 (ECC-P256), or 14 (ECC-P384) | all slots 02 | Policy| PIN and touch policy | PIN: 0 (Default), 1 (Never),
2 (Once), 3 (Always)
Touch: 0 (Default), 1 (Never),
2 (Always), 3 (Cached) | 9a, 9b, 9c, 9d, 9e, f9, 82 - 95 03 | Origin| Imported or generated | 1 (generated), 2 (imported) | 9a, 9c, 9d, 9e, f9, 82 - 95 04 | Public| Pub key partner to the pri key | DER encoding of public key | 9a, 9c, 9d, 9e, f9, 82 - 95 From 00cdeaee88aaa5d456fc77cec2d83489f5b74178 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 19:45:59 +0100 Subject: [PATCH 11/53] docs: add docs to ApplicationSession.cs --- .../src/Yubico/YubiKey/ApplicationSession.cs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs index f2961c323..8d91a6468 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ApplicationSession.cs @@ -21,17 +21,17 @@ namespace Yubico.YubiKey { /// - /// Abstract base class for sessions with a YubiKey. This class is used - /// to wrap the IYubiKeyConnection and provide a way of - /// interacting with the connection that is more convenient for most - /// users. + /// Abstract base class for sessions with a YubiKey. This class is used + /// to wrap the IYubiKeyConnection and provide a way of + /// interacting with the connection that is more convenient for most + /// users. /// public abstract class ApplicationSession : IDisposable { /// - /// The object that represents the connection to the YubiKey. Most - /// applications will ignore this, but it can be used to call Commands - /// directly. + /// The object that represents the connection to the YubiKey. Most + /// applications will ignore this, but it can be used to call Commands + /// directly. /// public IYubiKeyConnection Connection { get; protected set; } @@ -40,10 +40,19 @@ public abstract class ApplicationSession : IDisposable ///
public ScpKeyParameters? KeyParameters { get; } + /// + /// The specific YubiKey application to connect to. + /// + public YubiKeyApplication Application { get; } + + /// + /// The logger instance used for logging information. + /// protected ILogger Logger { get; } + /// + /// The YubiKey device to establish a session with. + /// protected IYubiKeyDevice YubiKey { get; } - public YubiKeyApplication Application { get; } - private bool _disposed; @@ -127,8 +136,9 @@ public virtual void Dispose() } Connection.Dispose(); - _disposed = true; GC.SuppressFinalize(this); + + _disposed = true; } } } From f90fbee4b75e714f0c7a06247fe3fca7ededad64 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:09:53 +0100 Subject: [PATCH 12/53] logs: change log level in ConnectionFactory --- .../src/Yubico/YubiKey/ConnectionFactory.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs index b4f78e3da..9ba7e72b2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionFactory.cs @@ -68,7 +68,7 @@ internal IScp03YubiKeyConnection CreateScpConnection(YubiKeyApplication applicat throw new InvalidOperationException("No smart card interface present. Unable to establish SCP connection to YubiKey."); } - _log.LogInformation("Connecting via the SmartCard interface using SCP03."); + _log.LogDebug("Connecting via the SmartCard interface using SCP03."); WaitForReclaimTimeout(Transport.SmartCard); return new Scp03Connection(_smartCardDevice, application, scp03Keys); @@ -94,7 +94,7 @@ public IScpYubiKeyConnection CreateScpConnection(YubiKeyApplication application, throw new InvalidOperationException("No smart card interface present. Unable to establish SCP connection to YubiKey."); } - _log.LogInformation("Connecting via the SmartCard interface using SCP03."); + _log.LogDebug("Connecting via the SmartCard interface using SCP."); WaitForReclaimTimeout(Transport.SmartCard); return new ScpConnection(_smartCardDevice, application, keyParameters); @@ -114,7 +114,7 @@ public IYubiKeyConnection CreateConnection(YubiKeyApplication application) { if (_smartCardDevice != null) { - _log.LogInformation("Connecting via the SmartCard interface."); + _log.LogDebug("Connecting via the SmartCard interface."); WaitForReclaimTimeout(Transport.SmartCard); return new SmartCardConnection(_smartCardDevice, application); @@ -122,7 +122,7 @@ public IYubiKeyConnection CreateConnection(YubiKeyApplication application) if (_hidKeyboardDevice != null && application == YubiKeyApplication.Otp) { - _log.LogInformation("Connecting via the Keyboard interface."); + _log.LogDebug("Connecting via the Keyboard interface."); WaitForReclaimTimeout(Transport.HidKeyboard); return new KeyboardConnection(_hidKeyboardDevice); @@ -130,7 +130,7 @@ public IYubiKeyConnection CreateConnection(YubiKeyApplication application) if (_hidFidoDevice != null && (application == YubiKeyApplication.Fido2 || application == YubiKeyApplication.FidoU2f)) { - _log.LogInformation("Connecting via the FIDO interface."); + _log.LogDebug("Connecting via the FIDO interface."); WaitForReclaimTimeout(Transport.HidFido); return new FidoConnection(_hidFidoDevice); @@ -156,14 +156,14 @@ private void WaitForReclaimTimeout(Transport newTransport) // We're only affected by the reclaim timeout if we're switching USB transports. if (_device.LastActiveTransport == newTransport) { - _log.LogInformation( + _log.LogDebug( "{Transport} transport is already active. No need to wait for reclaim.", _device.LastActiveTransport); return; } - _log.LogInformation( + _log.LogDebug( "Switching USB transports from {OldTransport} to {NewTransport}.", _device.LastActiveTransport, newTransport); @@ -176,7 +176,7 @@ private void WaitForReclaimTimeout(Transport newTransport) { var waitNeeded = reclaimTimeout - timeSinceLastActivation; - _log.LogInformation( + _log.LogDebug( "Reclaim timeout still active. Need to wait {TimeMS} milliseconds.", waitNeeded.TotalMilliseconds); @@ -185,7 +185,7 @@ private void WaitForReclaimTimeout(Transport newTransport) _device.LastActiveTransport = newTransport; - _log.LogInformation("Reclaim timeout has lapsed. It is safe to switch USB transports."); + _log.LogDebug("Reclaim timeout has lapsed. It is safe to switch USB transports."); } private bool CanFastReclaim() @@ -221,7 +221,7 @@ private void LogConnectionAttempt( _ => "Unknown" }; - _log.LogInformation("YubiKey connecting to {Application} application over {ScpInfo}", applicationName, scpInfo); + _log.LogDebug("YubiKey connecting to {Application} application over {ScpInfo}", applicationName, scpInfo); } private static string GetApplicationName(YubiKeyApplication application) => From f5fbbbf543348a1a7cdd1aeb00b0ad5430f3413e Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:13:38 +0100 Subject: [PATCH 13/53] misc: return ReadOnlyMemory in ECPublicKeyParameters.cs --- .../src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs index 6eecf4dc8..f6eab2fcc 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/ECPublicKeyParameters.cs @@ -60,7 +60,7 @@ public ECPublicKeyParameters(ECDsa ecdsa) : base(ecdsa.ExportParameters(false)) /// Gets the bytes representing the public key. ///
/// A containing the public key bytes with the format 0x04 || X || Y. - public Memory GetBytes() + public ReadOnlyMemory GetBytes() { byte[] publicKeyRawData = new byte[] { 0x4 } // Format identifier (uncompressed point): 0x04 From a786befae566dcf695f1287afb70e2cbc314a3f5 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:16:52 +0100 Subject: [PATCH 14/53] misc: OtpSession cleanup --- Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs index 5659c19b2..025a9bab5 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs @@ -287,8 +287,6 @@ public NdefDataReader ReadNdefTag() internal FirmwareVersion FirmwareVersion => _otpStatus.FirmwareVersion; - // internal IYubiKeyDevice YubiKey { get; private set; } - FirmwareVersion IOtpSession.FirmwareVersion => FirmwareVersion; IYubiKeyDevice IOtpSession.YubiKey => YubiKey; @@ -298,7 +296,6 @@ public NdefDataReader ReadNdefTag() // public void Dispose() => Connection.Dispose(); #region Private Fields - // private IYubiKeyConnection Connection; private readonly OtpStatus _otpStatus; #endregion } From 7bd0b36fe128debccff9648c332b6f29544e5a44 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:18:31 +0100 Subject: [PATCH 15/53] misc: ScpApduTransform.cs edits --- .../YubiKey/Pipelines/ScpApduTransform.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs index f078f8d6a..8337a4ed9 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -103,14 +103,14 @@ private static bool ShouldNotEncode(Type commandType) // This method introduced high coupling between the SCP pipeline and the applications. // The applications should not have to know about the SCP pipeline, or they should be able to // send the commands without the pipeline. - var exceptionList = new[] + var exemptionList = new[] { typeof(InterIndustry.Commands.SelectApplicationCommand), typeof(Oath.Commands.SelectOathCommand), typeof(Scp.Commands.ResetCommand), }; - return exceptionList.Contains(commandType); + return exemptionList.Contains(commandType); } private EncryptDataFunc InitializeScp11(Scp11KeyParameters keyParameters) @@ -141,19 +141,20 @@ public void Dispose() GC.SuppressFinalize(this); } - // The Dispose needs to make sure the local disposable fields are - // disposed. protected virtual void Dispose(bool disposing) { - if (!_disposed) + if (_disposed) { - if (disposing) - { - _scpState?.Dispose(); + return; + } - _disposed = true; - } + if (!disposing) + { + return; } + + _scpState?.Dispose(); + _disposed = true; } } } From 23af160124461b47ce076fad1f94606ee71d47f9 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:22:29 +0100 Subject: [PATCH 16/53] misc: PivSession.cs edits --- .../src/Yubico/YubiKey/Piv/PivSession.cs | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs index 996347266..da78f1f0f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs @@ -153,43 +153,6 @@ public sealed partial class PivSession : ApplicationSession { private bool _disposed; - /// - /// Create an instance of PivSession, the object that represents - /// the PIV application on the YubiKey. The communication between the SDK - /// and the YubiKey will be protected by SCP03. - /// - /// - /// See the User's Manual entry on - /// SCP03 for more information on - /// this communication protocol. - /// - /// Because this class implements IDisposable, use the using - /// keyword. For example, - /// - /// IYubiKeyDevice yubiKeyToUse = SelectYubiKey(); - /// // Assume you have some method that obtains the appropriate SCP03 - /// // key set. - /// using StaticKeys scp03Keys = CollectScp03Keys(); - /// using (var piv = new PivSession(yubiKeyToUse, scp03Keys)) - /// { - /// /* Perform PIV operations. */ - /// } - /// - /// - /// - /// - /// The object that represents the actual YubiKey which will perform the - /// operations. - /// - /// - /// The SCP03 key set to use in establishing the connection. - /// - /// - /// The yubiKey argument is null. - /// - /// - /// This exception is thrown when unable to determine the management key type. - /// [Obsolete("Use new Scp")] public PivSession(IYubiKeyDevice yubiKey, Yubico.YubiKey.Scp03.StaticKeys scp03Keys) : this(yubiKey, scp03Keys.ConvertToScp03KeyParameters()) From cd13afd7009d220c2ca02d8d46da7ead014a52ce Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:27:17 +0100 Subject: [PATCH 17/53] misc: ExternalAuthenticateResponse.cs edits --- .../Scp/Commands/ExternalAuthenticateResponse.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs index d8d9038e4..87c9e1678 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ExternalAuthenticateResponse.cs @@ -33,14 +33,18 @@ public ExternalAuthenticateResponse(ResponseApdu responseApdu) : throw new ArgumentNullException(nameof(responseApdu)); } - if (responseApdu.SW != SWConstants.Success) + if (responseApdu.SW == SWConstants.Success) { - string message = string.Format( - CultureInfo.CurrentCulture, - $"{ExceptionMessages.IncorrectExternalAuthenticateData}" + " " + - $"SW: 0x{responseApdu.SW.ToString("X4", CultureInfo.InvariantCulture)}"); - throw new ArgumentException(message, nameof(responseApdu)); + return; } + + string message = string.Format( + CultureInfo.CurrentCulture, + $"{ExceptionMessages.IncorrectExternalAuthenticateData}" + " " + + $"SW: 0x{responseApdu.SW.ToString("X4", CultureInfo.InvariantCulture)}"); + + // Example output: IncorrectExternalAuthenticateData SW: 0x6300 + throw new ArgumentException(message, nameof(responseApdu)); } } } From d9cc85fa701528cc7f4e3c3343e5a59a03cddc24 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:27:28 +0100 Subject: [PATCH 18/53] misc: InitializeUpdateCommand.cs edits --- .../src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs index a6773d570..9b81315d2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/InitializeUpdateCommand.cs @@ -54,6 +54,7 @@ public InitializeUpdateCommand(int keyVersionNumber, ReadOnlyMemory hostCh P1 = (byte)_keyVersionNumber, Data = _hostChallenge }; + public InitializeUpdateResponse CreateResponseForApdu(ResponseApdu responseApdu) => new InitializeUpdateResponse(responseApdu); } } From 660095d8c4965fd098dafa8dd2b38b66542d2b10 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 20:30:58 +0100 Subject: [PATCH 19/53] docs: added docs to PutKeyCommand --- .../src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs index 1205ea5fb..4e00ec148 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs @@ -113,6 +113,12 @@ internal class PutKeyCommand : IYubiKeyCommand public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; + /// + /// This is used internally by the . Clients should not have to build this manually. + /// + /// The P1 parameter for the PutKey Apdu command + /// The P2 parameter for the PutKey Apdu command + /// The data to use for the PutKey Apdu command public PutKeyCommand(byte p1, byte p2, ReadOnlyMemory data) { _p1 = p1; From 6f8b74e2225e6a54cdb416108ee9baed7b8b8371 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 23:30:46 +0100 Subject: [PATCH 20/53] docs: adding comments and docs --- .../Yubico/YubiKey/Scp/Helpers/ChannelMac.cs | 64 ++++++++++-- .../Yubico/YubiKey/Scp/Helpers/Derivation.cs | 99 ++++++++++++++++--- .../YubiKey/Scp/IScpYubiKeyConnection.cs | 3 + .../src/Yubico/YubiKey/Scp/Scp11State.cs | 74 ++++++++++++-- .../Yubico/YubiKey/Scp/ScpKeyParameters.cs | 9 +- .../src/Yubico/YubiKey/Scp/ScpState.cs | 42 ++++++-- .../YubiKey/Scp/SecurityDomainSession.cs | 22 ++--- .../src/Yubico/YubiKey/Scp/SessionKeys.cs | 33 +++++-- 8 files changed, 283 insertions(+), 63 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs index 267b35b38..137a80557 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs @@ -1,4 +1,4 @@ -// Copyright 2021 Yubico AB +// 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. @@ -20,50 +20,95 @@ namespace Yubico.YubiKey.Scp.Helpers { + /// + /// Provides functionality for MAC (Message Authentication Code) operations in secure channel communications. + /// This class handles the MAC generation and verification for APDUs in a secure channel. + /// internal static class ChannelMac { - public static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( + /// + /// Generates a MAC for a command APDU using the provided MAC key and chaining value. + /// + /// The command APDU to be MACed. + /// The key used for MAC generation. + /// The MAC chaining value from previous operations (must be 16 bytes). + /// + /// A tuple containing: + /// - The command APDU with the generated MAC appended + /// - The new MAC chaining value for use in subsequent operations + /// + /// Thrown when the MAC chaining value is not 16 bytes long. + public static (CommandApdu macdApdu, ReadOnlyMemory newMacChainingValue) MacApdu( CommandApdu apdu, ReadOnlySpan macKey, ReadOnlySpan macChainingValue) { + // MAC chaining value must be 16 bytes to match AES block size if (macChainingValue.Length != 16) { throw new ArgumentException(ExceptionMessages.UnknownScpError, nameof(macChainingValue)); } + // Add 8 bytes of space for the MAC that will be calculated var apduWithLongerLen = AddDataToApdu(apdu, new byte[8]); byte[] apduBytesWithZeroMac = apduWithLongerLen.AsByteArray(); + // Prepare input for MAC calculation: + // - First 16 bytes: MAC chaining value from previous operation + // - Remaining bytes: APDU bytes (excluding the 8 zero bytes we added) byte[] macInp = new byte[16 + apduBytesWithZeroMac.Length - 8]; macChainingValue.CopyTo(macInp); apduBytesWithZeroMac.AsSpan(0, apduBytesWithZeroMac.Length - 8).CopyTo(macInp.AsSpan(16)); - byte[] newMacChainingValue = new byte[16]; + // Calculate MAC using AES-CMAC (16 bytes) + Span newMacChainingValue = stackalloc byte[16]; using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); cmacObj.CmacInit(macKey); cmacObj.CmacUpdate(macInp); cmacObj.CmacFinal(newMacChainingValue); - var macdApdu = AddDataToApdu(apdu, newMacChainingValue.AsSpan(0, 8)); - return (macdApdu, newMacChainingValue); + // Create final APDU with the first 8 bytes of the MAC appended + var macdApdu = AddDataToApdu(apdu, newMacChainingValue[..8]); + return (macdApdu, newMacChainingValue.ToArray()); } - public static void VerifyRmac(ReadOnlySpan response, ReadOnlySpan rmacKey, ReadOnlySpan macChainingValue) + /// + /// Verifies the Response MAC (RMAC) of a response message using the provided RMAC key and chaining value. + /// + /// The response data containing the RMAC to verify. + /// The key used for RMAC verification. + /// The MAC chaining value from previous operations. + /// + /// Thrown when: + /// - The response length is insufficient (less than 8 bytes) + /// - The response length is incorrect for decryption + /// - The RMAC verification fails + /// + public static void VerifyRmac( + ReadOnlySpan response, + ReadOnlySpan rmacKey, + ReadOnlySpan macChainingValue) { + // Response must be at least 8 bytes (minimum length for MAC) if (response.Length < 8) { throw new SecureChannelException(ExceptionMessages.InsufficientResponseLengthToVerifyRmac); } + // Response length (excluding MAC) must be multiple of 16 for AES block alignment if ((response.Length - 8) % 16 != 0) { throw new SecureChannelException(ExceptionMessages.IncorrectResponseLengthToDecrypt); } - int respDataLen = response.Length - 8; + // Extract the received MAC (last 8 bytes of response) var recvdRmac = response[^8..]; + // Prepare input for MAC verification: + // - First 16 bytes: MAC chaining value + // - Next bytes: Response data (excluding MAC) + // - Last 2 bytes: Success status words (90 00) + int respDataLen = response.Length - 8; Span macInp = stackalloc byte[16 + respDataLen + 2]; macChainingValue.CopyTo(macInp); response[..respDataLen].CopyTo(macInp[16..]); @@ -71,13 +116,15 @@ public static void VerifyRmac(ReadOnlySpan response, ReadOnlySpan rm macInp[16 + respDataLen] = SW1Constants.Success; macInp[16 + respDataLen + 1] = SWConstants.Success & 0xFF; + // Calculate expected MAC using AES-CMAC using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); Span cmac = stackalloc byte[16]; cmacObj.CmacInit(rmacKey); cmacObj.CmacUpdate(macInp); cmacObj.CmacFinal(cmac); - if (!CryptographicOperations.FixedTimeEquals(recvdRmac, cmac.Slice(0, 8))) + // Use constant-time comparison to prevent timing attacks + if (!CryptographicOperations.FixedTimeEquals(recvdRmac, cmac[..8])) { throw new SecureChannelException(ExceptionMessages.IncorrectRmac); } @@ -103,6 +150,7 @@ private static CommandApdu AddDataToApdu(CommandApdu apdu, ReadOnlySpan da data.CopyTo(newData.AsSpan(currentDataLength)); newApdu.Data = newData; + return newApdu; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs index f3712487b..de91d62e9 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs @@ -19,19 +19,52 @@ namespace Yubico.YubiKey.Scp.Helpers { + /// + /// Provides key derivation functionality for Secure Channel Protocol (SCP) operations. + /// This class implements the key derivation functions used in SCP03 protocol for + /// generating session keys and cryptograms. + /// internal static class Derivation { + /// + /// Data Derivation Constant for Session Encryption Key + /// internal const byte DDC_SENC = 0x04; + + /// + /// Data Derivation Constant for Session MAC Key + /// internal const byte DDC_SMAC = 0x06; + + /// + /// Data Derivation Constant for Session RMAC Key + /// internal const byte DDC_SRMAC = 0x07; + + /// + /// Data Derivation Constant for Card Cryptogram + /// internal const byte DDC_CARD_CRYPTOGRAM = 0x00; + + /// + /// Data Derivation Constant for Host Cryptogram + /// internal const byte DDC_HOST_CRYPTOGRAM = 0x01; - // Derive a key from the challenges. - // This method only supports deriving a 64- or 128-bit result based on - // challenges each of which must be 8 bytes. - // The result (output) will be 8 bytes (outputLenBits = 64 bits) - // or 16 bytes (outputLen = 128 bits). + /// + /// Derives a key or cryptogram from the host and card challenges using the specified derivation parameters. + /// + /// The derivation constant indicating the type of key being derived (e.g., SENC, SMAC). + /// The desired output length in bits (must be either 64 or 128). + /// The key derivation function key. + /// The 8-byte challenge from the host. + /// The 8-byte challenge from the card. + /// A derived key or cryptogram as a Memory{byte}. + /// + /// Thrown when: + /// - The output length is not 64 or 128 bits + /// - Either challenge is not exactly 8 bytes + /// public static Memory Derive( byte dataDerivationConstant, byte outputLenBits, @@ -39,50 +72,84 @@ public static Memory Derive( ReadOnlySpan hostChallenge, ReadOnlySpan cardChallenge) { + // Validate output length if (outputLenBits != 64 && outputLenBits != 128) { throw new SecureChannelException(ExceptionMessages.IncorrectDerivationLength); } + // Validate challenge lengths if (hostChallenge.Length != 8 || cardChallenge.Length != 8) { throw new SecureChannelException(ExceptionMessages.InvalidChallengeLength); } - Span macInp = stackalloc byte[32]; - macInp[11] = dataDerivationConstant; + // Initialize MAC input buffer with zeros + // Format according to GP spec 2.3.1: + // [padding(11) || derivation constant || padding(2) || output length || counter=1 || host challenge || card challenge] + Span macInputBuffer = stackalloc byte[32]; + macInputBuffer[11] = dataDerivationConstant; + + // Set output length and counter + macInputBuffer[14] = outputLenBits; + macInputBuffer[15] = 1; // Counter is always 1 for our use case - // This is the output length. - macInp[14] = outputLenBits; - macInp[15] = 1; - hostChallenge.CopyTo(macInp.Slice(16, 8)); - cardChallenge.CopyTo(macInp.Slice(24, 8)); + // Copy host and cardchallenges to the end of the input buffer + hostChallenge.CopyTo(macInputBuffer.Slice(16, 8)); + cardChallenge.CopyTo(macInputBuffer.Slice(24, 8)); + // Calculate CMAC using AES-128 Span cmac = stackalloc byte[16]; using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); cmacObj.CmacInit(kdfKey); - cmacObj.CmacUpdate(macInp); + cmacObj.CmacUpdate(macInputBuffer); cmacObj.CmacFinal(cmac); - if (outputLenBits == 128) // Output is a 128-bit key + // If output length is 128 bits, return the full CMAC + if (outputLenBits == 128) { return cmac.ToArray(); } - // Else, output is a cryptogram + // For 64-bit output (cryptograms), use only the first 8 bytes Span smallerResult = stackalloc byte[8]; cmac[..8].CopyTo(smallerResult); + // Securely clear the full CMAC from memory CryptographicOperations.ZeroMemory(cmac); return smallerResult.ToArray(); } + /// + /// Derives a cryptogram using the specified derivation constant and challenges. + /// This is a convenience method that calls Derive with a 64-bit output length. + /// + /// The derivation constant ( or . + /// The key used for cryptogram derivation. + /// The 8-byte host challenge. + /// The 8-byte card challenge. + /// A 64-bit (8-byte) cryptogram. public static Memory DeriveCryptogram( byte dataDerivationConstant, ReadOnlySpan key, ReadOnlySpan hostChallenge, - ReadOnlySpan cardChallenge) => Derive(dataDerivationConstant, 64, key, hostChallenge, cardChallenge); + ReadOnlySpan cardChallenge) + => Derive(dataDerivationConstant, 64, key, hostChallenge, cardChallenge); + /// + /// Derives session keys from the static keys using host and card challenges. + /// This method generates three session keys (SMAC, SENC, SRMAC) and includes the static DEK. + /// + /// The static key set containing channel MAC, encryption, and data encryption keys. + /// The 8-byte host challenge. + /// The 8-byte card challenge. + /// A SessionKeys object containing all derived session keys. + /// + /// This method implements secure memory handling: + /// - Uses stackalloc for temporary buffers + /// - Securely clears sensitive data after use + /// - Properly handles exceptions to ensure no sensitive data leaks + /// public static SessionKeys DeriveSessionKeysFromStaticKeys( StaticKeys staticKeys, ReadOnlySpan hostChallenge, diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs index 44d8f4937..dccc28d6f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/IScpYubiKeyConnection.cs @@ -25,6 +25,9 @@ public interface IScpYubiKeyConnection : IYubiKeyConnection ///
public ScpKeyParameters KeyParameters { get; } + /// + /// Get the encryptor function to encrypt any data for a SCP command using the current session keys. + /// internal EncryptDataFunc EncryptDataFunc { get; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index d99c344a0..bbe752b8c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -32,11 +32,24 @@ internal class Scp11State : ScpState private const int EckaTag = 0x5F49; private const int KeyAgreementTag = 0xA6; + /// + /// Initializes a new instance of the class with the specified session keys and receipt. + /// + /// The session keys for secure channel communication. + /// The receipt data used for verification. public Scp11State(SessionKeys sessionKeys, Memory receipt) : base(sessionKeys, receipt) { } + /// + /// Creates a new SCP11 secure channel state by performing key agreement and authentication with the YubiKey. + /// + /// The APDU pipeline for communication with the YubiKey. + /// The key parameters required for SCP11 authentication. + /// A new instance of configured for secure channel communication. + /// Thrown when key parameters are invalid or incompatible. + /// Thrown when secure channel establishment fails. internal static Scp11State CreateScpState( IApduTransform pipeline, Scp11KeyParameters keyParameters) @@ -65,6 +78,7 @@ internal static Scp11State CreateScpState( byte[] keyLen = { 16 }; // 128-bit byte[] keyIdentifier = { 0x11, GetScpIdentifierByte(keyParameters.KeyReference) }; + // Construct the host authentication data (payload) byte[] hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( new TlvObject( KeyAgreementTag, @@ -77,6 +91,7 @@ internal static Scp11State CreateScpState( new TlvObject(EckaTag, ephemeralPublicKeyEncodedPointOceEcka) ); + // Construct the host authentication command var authenticateCommand = keyParameters.KeyReference.Id == ScpKeyIds.Scp11B ? new InternalAuthenticateCommand( keyParameters.KeyReference.VersionNumber, keyParameters.KeyReference.Id, @@ -84,26 +99,31 @@ internal static Scp11State CreateScpState( : new ExternalAuthenticateCommand( keyParameters.KeyReference.VersionNumber, keyParameters.KeyReference.Id, hostAuthenticateTlvEncodedData) as IYubiKeyCommand; - + + // Issue the host authentication command var authenticateResponseApdu = pipeline.Invoke( - authenticateCommand.CreateCommandApdu(), authenticateCommand.GetType(), typeof(ScpResponse)); //works + authenticateCommand.CreateCommandApdu(), authenticateCommand.GetType(), typeof(ScpResponse)); var authenticateResponse = authenticateCommand.CreateResponseForApdu(authenticateResponseApdu); authenticateResponse.ThrowIfFailed( $"Error when performing {authenticateCommand.GetType().Name}: {authenticateResponse.StatusMessage}"); + // Decode the response as a TLV list var authenticateResponseTlvs = TlvObjects.DecodeList(authenticateResponseApdu.Data.Span); + // Extract the ephemeral public key from the response var epkSdEckaTlv = authenticateResponseTlvs[0]; var epkSdEckaTlvEncodedData = epkSdEckaTlv.GetBytes(); var sdReceipt = TlvObjects.UnpackValue( ReceiptTag, authenticateResponseTlvs[1].GetBytes().Span); // Yubikey X963KDF Receipt to match with our own X963KDF + // Decide which key to use for key agreement var skOceEcka = keyParameters.SkOceEcka?.Parameters ?? // If set, we will use this for SCP11A and SCP11C. ekpOceEcka; // Otherwise, just use the newly created ephemeral key for SCP11b. + // Perform key agreement var (encryptionKey, macKey, rMacKey, dekKey) = GetX963KDFKeyAgreementKeys( skOceEcka.Curve, @@ -116,7 +136,8 @@ internal static Scp11State CreateScpState( keyUsage, keyType, keyLen); - + + // Create the session keys var sessionKeys = new SessionKeys( macKey, encryptionKey, @@ -127,6 +148,22 @@ internal static Scp11State CreateScpState( return new Scp11State(sessionKeys, sdReceipt.ToArray()); } + /// + /// Performs X9.63 Key Derivation Function (KDF) to generate session keys for the secure channel. + /// + /// The elliptic curve used for key agreement. + /// The YubiKey's public key parameters. + /// The host's ephemeral key pair parameters. + /// The host's static key pair parameters. + /// The receipt computed by the YubiKey. + /// The YubiKey's ephemeral public key in TLV format. + /// The host authentication data in TLV format. + /// The intended usage of the derived keys. + /// The type of keys to be derived. + /// The length of keys to be derived. + /// A tuple containing the encryption, MAC, R-MAC, and DEK keys. + /// Thrown when the curves of the provided keys do not match. + /// Thrown when key agreement receipt verification fails. private static (Memory encryptionKey, Memory macKey, Memory rMacKey, Memory dekKey) GetX963KDFKeyAgreementKeys( ECCurve curve, // The curve being used for the key agreement @@ -188,7 +225,7 @@ private static (Memory encryptionKey, Memory macKey, Memory rM } // Get keys - byte[] receiptVerificationKey = keys[0]; // receipt verificationKey + byte[] receiptVerificationKey = keys[0]; byte[] encryptionKey = keys[1]; byte[] macKey = keys[2]; byte[] rmacKey = keys[3]; @@ -196,10 +233,11 @@ private static (Memory encryptionKey, Memory macKey, Memory rM // Do AES CMAC using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); - Span oceReceipt = stackalloc byte[16]; // Our generated receipt + + Span oceReceipt = stackalloc byte[16]; cmacObj.CmacInit(receiptVerificationKey); cmacObj.CmacUpdate(keyAgreementData); - cmacObj.CmacFinal(oceReceipt); + cmacObj.CmacFinal(oceReceipt); // Our generated receipt if (!CryptographicOperations.FixedTimeEquals( oceReceipt, sdReceipt.Span)) // Needs to match with the receipt generated by the Yubikey @@ -210,6 +248,12 @@ private static (Memory encryptionKey, Memory macKey, Memory rM return (encryptionKey, macKey, rmacKey, dekKey); } + /// + /// Extracts EC public key parameters from TLV-encoded data. + /// + /// The TLV-encoded public key data. + /// The elliptic curve parameters. + /// The extracted EC parameters containing the public key coordinates. private static ECParameters ExtractPublicKeyEcParameters(ReadOnlyMemory epkSdEckaTlv, ECCurve curve) { var epkSdEckaEncodedPoint = TlvObjects.UnpackValue(EckaTag, epkSdEckaTlv.Span); @@ -228,8 +272,11 @@ private static ECParameters ExtractPublicKeyEcParameters(ReadOnlyMemory ep /// /// Gets the standardized SCP identifier for the given key reference. - /// Global Platform Secure Channel Protocol 11 Card Specification v2.3 – Amendment F § 7.1.1 + /// As defined in Global Platform Secure Channel Protocol 11 Card Specification v2.3 – Amendment F § 7.1.1 /// + /// The key reference to get the identifier for. + /// The SCP identifier byte. + /// Thrown when the key reference ID is not a valid SCP11 KID. private static byte GetScpIdentifierByte(KeyReference keyReference) => keyReference.Id switch { @@ -239,6 +286,14 @@ private static byte GetScpIdentifierByte(KeyReference keyReference) => _ => throw new ArgumentException("Invalid SCP11 KID") }; + /// + /// Performs the Security Operation command sequence required for SCP11a and SCP11c authentication. + /// + /// The APDU pipeline for communication with the YubiKey. + /// The key parameters containing certificates and references. + /// Thrown when required key parameters are missing. + /// Thrown when required certificates are missing. + /// Thrown when the security operation fails. private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyParameters keyParams) { // GPC v2.3 Amendment F (SCP11) v1.4 §7.5 @@ -283,6 +338,11 @@ private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyPa } } + /// + /// Combines multiple byte arrays into a single array. + /// + /// The arrays to merge. + /// A new array containing all input arrays concatenated in sequence. private static byte[] MergeArrays(params ReadOnlyMemory[] values) { using var memoryStream = new MemoryStream(); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs index d47853b00..c1372fb59 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs @@ -21,10 +21,15 @@ namespace Yubico.YubiKey.Scp ///
public abstract class ScpKeyParameters { + /// + /// The key reference associated with the key parameters. + /// public KeyReference KeyReference { get; protected set; } - public ReadOnlySpan GetBytes => new ReadOnlySpan(new[] { KeyReference.Id, KeyReference.VersionNumber }); - + /// + /// Creates a new instance of . + /// + /// protected ScpKeyParameters(KeyReference keyReference) { KeyReference = keyReference; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs index 8d1daca11..95e699bab 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs @@ -7,8 +7,15 @@ namespace Yubico.YubiKey.Scp { internal abstract class ScpState : IDisposable { + /// + /// The session keys for secure channel communication. + /// protected readonly SessionKeys SessionKeys; - protected Memory MacChainingValue; + + /// + /// The chaining value for the MAC. + /// + protected ReadOnlyMemory MacChainingValue; private int _encryptionCounter = 1; private bool _disposed; @@ -39,6 +46,7 @@ public CommandApdu EncodeCommand(CommandApdu command) throw new ArgumentNullException(nameof(command)); } + // Create an encrypted APDU var encodedCommand = new CommandApdu { Cla = (byte)(command.Cla | 0x04), //0x04 is for secure-messaging @@ -47,14 +55,16 @@ public CommandApdu EncodeCommand(CommandApdu command) P2 = command.P2 }; + // Encrypt the data var encryptedData = ChannelEncryption.EncryptData( command.Data.Span, SessionKeys.EncKey.Span, _encryptionCounter); + // Update the encryption counter _encryptionCounter++; encodedCommand.Data = encryptedData; // Create a MAC:ed APDU - (var macdApdu, byte[] newMacChainingValue) = MacApdu( + var (macdApdu, newMacChainingValue) = MacApdu( encodedCommand, SessionKeys.MacKey.Span, MacChainingValue.Span); @@ -96,9 +106,11 @@ public ResponseApdu DecodeResponse(ResponseApdu response) var responseData = response.Data; VerifyRmac(responseData.Span, SessionKeys.RmacKey.Span, MacChainingValue.Span); + // Initialize decryptedData as an empty array ReadOnlyMemory decryptedData = Array.Empty(); if (responseData.Length > 8) { + // Decrypt the response data if it's longer than 8 bytes int previousEncryptionCounter = _encryptionCounter - 1; decryptedData = ChannelEncryption.DecryptData( responseData[..^8].Span, @@ -107,10 +119,15 @@ public ResponseApdu DecodeResponse(ResponseApdu response) ); } + // Create a new array to hold the decrypted data and status words byte[] fullDecryptedResponse = new byte[decryptedData.Length + 2]; decryptedData.CopyTo(fullDecryptedResponse); + + // Append the status words to the decrypted data fullDecryptedResponse[decryptedData.Length] = response.SW1; fullDecryptedResponse[decryptedData.Length + 1] = response.SW2; + + // Return a new ResponseApdu with the decrypted data and status words return new ResponseApdu(fullDecryptedResponse); } @@ -137,24 +154,31 @@ public EncryptDataFunc GetDataEncryptor() plainText.Span); } - protected static (CommandApdu macdApdu, byte[] newMacChainingValue) MacApdu( + /// + /// Performs the MAC on the APDU + /// + /// + /// + /// + /// + protected static (CommandApdu macdApdu, ReadOnlyMemory newMacChainingValue) MacApdu( CommandApdu commandApdu, ReadOnlySpan macKey, ReadOnlySpan macChainingValue) => ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - private static void VerifyRmac( ReadOnlySpan responseData, ReadOnlySpan rmacKey, ReadOnlySpan macChainingValue) => ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) { if (!_disposed) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 3835769da..33790c481 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -263,10 +263,10 @@ public void PutKey(KeyReference keyReference, StaticKeys staticKeys, int replace } /// - /// Puts an ECC private key onto the YubiKey using the Security Domain. + /// Puts an EC private key onto the YubiKey using the Security Domain. /// /// The key reference identifying where to store the key. - /// The ECC private key parameters to store. + /// The EC private key parameters to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the private key is not of type NIST P-256. /// Thrown when no secure session is established. @@ -306,7 +306,7 @@ public void PutKey(KeyReference keyReference, ECPrivateKeyParameters privateKeyP CryptographicOperations.ZeroMemory(privateKeyBytes.Span); } - // Write the ECC parameters + // Write the EC parameters var paramsTlv = new TlvObject(EcKeyType, new byte[] { 0x00 }).GetBytes(); commandDataWriter.Write(paramsTlv.ToArray()); commandDataWriter.Write((byte)0); @@ -331,10 +331,10 @@ public void PutKey(KeyReference keyReference, ECPrivateKeyParameters privateKeyP } /// - /// Puts an ECC public key onto the YubiKey using the Security Domain. + /// Puts an EC public key onto the YubiKey using the Security Domain. /// /// The key reference identifying where to store the key. - /// The ECC public key parameters to store. + /// The EC public key parameters to store. /// The key version number to replace, or 0 for a new key. /// Thrown when the public key is not of type SECP256R1. /// Thrown when no secure session is established. @@ -358,13 +358,13 @@ public void PutKey(KeyReference keyReference, ECPublicKeyParameters publicKeyPar // Write the key version number commandDataWriter.Write(keyReference.VersionNumber); - // Write the ECC public key + // Write the EC public key var publicKeyTlvData = new TlvObject(EcPublicKeyKeyType, publicKeyParameters.GetBytes().Span).GetBytes(); commandDataWriter.Write(publicKeyTlvData.ToArray()); - // Write the ECC parameters + // Write the EC parameters var paramsTlv = new TlvObject(EcKeyType, new byte[1]).GetBytes(); commandDataWriter.Write(paramsTlv.ToArray()); commandDataWriter.Write((byte)0); @@ -489,10 +489,10 @@ public ECPublicKeyParameters GenerateEcKey(KeyReference keyReference, byte repla var encodedPoint = tlvReader.ReadValue(EcPublicKeyKeyType).Span; // Create the ECParameters with the public point - var eccPublicKey = encodedPoint.CreateECPublicKeyFromBytes(); + var ecPublicKey = encodedPoint.CreateECPublicKeyFromBytes(); Logger.LogInformation("Key generated (KeyReference: {KeyReference})", keyReference); - return eccPublicKey; + return ecPublicKey; } /// @@ -840,11 +840,11 @@ public void Reset() { case ScpKeyIds.Scp03: // SCP03 uses KID=0, we use KVN=0 to allow deleting the default keys - // which have an invalid KVN (0xff). + // which have an invalid KVN (0xFF). overridenKeyRef = new KeyReference(0, 0); ins = InitializeUpdateCommand.GpInitializeUpdateIns; break; - case 0x02: + case 0x02: case 0x03: continue; // Skip these as they are deleted by 0x01 case ScpKeyIds.Scp11A: diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs index 3952ee38a..af450a87c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs @@ -23,10 +23,12 @@ internal class SessionKeys : IDisposable /// Gets the session MAC key. /// public ReadOnlyMemory MacKey => _macKey; + /// /// Gets the session encryption key. /// public ReadOnlyMemory EncKey => _encryptionKey; + /// /// Gets the session RMAC key. /// @@ -37,10 +39,10 @@ internal class SessionKeys : IDisposable ///
public ReadOnlyMemory? DataEncryptionKey => _dataEncryptionKey; - private readonly Memory _macKey; - private readonly Memory _encryptionKey; - private readonly Memory _rmacKey; - private readonly Memory _dataEncryptionKey; + private readonly Memory _macKey = new byte[16]; + private readonly Memory _encryptionKey = new byte[16]; + private readonly Memory _rmacKey = new byte[16]; + private readonly Memory _dataEncryptionKey = new byte[16]; private bool _disposed; /// @@ -56,12 +58,23 @@ public SessionKeys( Memory rmacKey, Memory dataEncryptionKey) { - _macKey = macKey; - _encryptionKey = encryptionKey; - _rmacKey = rmacKey; - _dataEncryptionKey = dataEncryptionKey; - - _disposed = false; + ValidateKeyLength(macKey, nameof(macKey)); + ValidateKeyLength(encryptionKey, nameof(encryptionKey)); + ValidateKeyLength(rmacKey, nameof(rmacKey)); + ValidateKeyLength(dataEncryptionKey, nameof(dataEncryptionKey)); + + macKey.CopyTo(_macKey); + encryptionKey.CopyTo(_encryptionKey); + rmacKey.CopyTo(_rmacKey); + dataEncryptionKey.CopyTo(_dataEncryptionKey); + } + + private static void ValidateKeyLength(Memory key, string paramName) + { + if (key.Length != 16) + { + throw new ArgumentException("Incorrect session key length. Must be 16.", paramName); + } } public void Dispose() From de43147ca11b12332b89efb9e5869ba1157b2466 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 23:31:42 +0100 Subject: [PATCH 21/53] misc: misc edits --- .../YubiKey/Cryptography/AesUtilities.cs | 2 +- .../src/Yubico/YubiKey/Otp/OtpSession.cs | 2 +- .../YubiKey/Scp/Commands/PutKeyCommand.cs | 1 - .../YubiKey/Scp/Commands/StoreDataCommand.cs | 2 +- .../YubiKey/Scp/Helpers/ChannelEncryption.cs | 8 +++--- .../src/Yubico/YubiKey/Scp/Scp03State.cs | 12 +++----- .../src/Yubico/YubiKey/Scp/StaticKeys.cs | 28 +++++++++---------- .../Yubico/YubiKey/Scp03/ChannelEncryption.cs | 8 +++--- .../YubiKey/Scp03/Commands/PutKeyCommand.cs | 11 ++++---- .../YubiKey/Cryptography/AesUtilitiesTests.cs | 2 +- 10 files changed, 36 insertions(+), 40 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs index 132173565..69aba1e4a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/AesUtilities.cs @@ -32,7 +32,7 @@ internal static class AesUtilities /// 16-byte AES128 key /// 16-byte input block /// The 16-byte AES128 ciphertext - public static byte[] BlockCipher(ReadOnlySpan encryptionKey, ReadOnlySpan plaintext) + public static Memory BlockCipher(ReadOnlySpan encryptionKey, ReadOnlySpan plaintext) { if (encryptionKey == null) { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs index 025a9bab5..0ae469a78 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Otp/OtpSession.cs @@ -80,7 +80,7 @@ public OtpSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null : base(Log.GetLogger(), yubiKey, YubiKeyApplication.Otp, keyParameters) { // Getting the OTP status allows the user to read the OTP status on the OtpSession object. - _otpStatus = Connection.SendCommand(new ReadStatusCommand()).GetData(); // + _otpStatus = Connection.SendCommand(new ReadStatusCommand()).GetData(); } #region OTP Operation Object Factory diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs index 4e00ec148..bd7930bfd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PutKeyCommand.cs @@ -106,7 +106,6 @@ internal class PutKeyCommand : IYubiKeyCommand { private const byte GpPutKeyCla = 0x80; private const byte GpPutKeyIns = 0xD8; - private readonly byte[] _data; private readonly byte _p1; private readonly byte _p2; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs index ea58e4813..2b53c626d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs @@ -31,7 +31,7 @@ internal class StoreDataCommand : IYubiKeyCommand private const byte GpStoreDataIns = 0xE2; private readonly ReadOnlyMemory _data; - public YubiKeyApplication Application => YubiKeyApplication.InterIndustry; + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; /// /// Initializes a new instance of the class, with the given data to be stored. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs index ccbf41762..23fd84863 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs @@ -42,10 +42,10 @@ public static ReadOnlyMemory EncryptData( byte[] ivInput = new byte[16]; countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block - byte[] iv = AesUtilities.BlockCipher(encryptionKey, ivInput); + var iv = AesUtilities.BlockCipher(encryptionKey, ivInput); var paddedPayload = Padding.PadToBlockSize(dataToEncrypt); - var encryptedData = AesUtilities.AesCbcEncrypt(encryptionKey, iv, paddedPayload.Span); + var encryptedData = AesUtilities.AesCbcEncrypt(encryptionKey, iv.Span, paddedPayload.Span); return encryptedData; } @@ -73,8 +73,8 @@ public static ReadOnlyMemory DecryptData( countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block ivInput[0] = 0x80; // to mark as RMAC calculation - byte[] iv = AesUtilities.BlockCipher(key, ivInput); - var decryptedData = AesUtilities.AesCbcDecrypt(key, iv, dataToDecrypt); + var iv = AesUtilities.BlockCipher(key, ivInput); + var decryptedData = AesUtilities.AesCbcDecrypt(key, iv.Span, dataToDecrypt); return Padding.RemovePadding(decryptedData.Span); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs index 2bb775f67..800ff8abe 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs @@ -78,8 +78,7 @@ private static Scp03State CreateScpState( return new Scp03State(sessionKeys, hostCryptogram); } - private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCryptogram) - PerformInitializeUpdate( + private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCryptogram) PerformInitializeUpdate( IApduTransform pipeline, Scp03KeyParameters keyParameters, ReadOnlyMemory hostChallenge) @@ -101,13 +100,11 @@ private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCry return (cardChallenge, cardCryptogram); } - private void - PerformExternalAuthenticate( - IApduTransform pipeline) + private void PerformExternalAuthenticate(IApduTransform pipeline) { // Create a MAC:ed APDU var eaCommandPlain = new ExternalAuthenticateCommand(_hostCryptogram); - (var macdApdu, byte[] newMacChainingValue) = MacApdu( + var (macdApdu, newMacChainingValue) = MacApdu( eaCommandPlain.CreateCommandApdu(), SessionKeys.MacKey.ToArray(), MacChainingValue.ToArray() @@ -123,8 +120,7 @@ private void typeof(ExternalAuthenticateCommand), typeof(ExternalAuthenticateResponse)); - var externalAuthenticateResponse = eaCommandMaced.CreateResponseForApdu(eaResponseApdu); - externalAuthenticateResponse.ThrowIfFailed(); + eaCommandMaced.CreateResponseForApdu(eaResponseApdu).ThrowIfFailed(); } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs index 3f2bbbcfe..5efc83f91 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs @@ -94,8 +94,6 @@ public StaticKeys(ReadOnlyMemory channelMacKey, } SetKeys(channelMacKey, channelEncryptionKey, dataEncryptionKey); - - _disposed = false; } /// @@ -105,15 +103,13 @@ public StaticKeys(ReadOnlyMemory channelMacKey, /// public StaticKeys() { - var DefaultKey = new ReadOnlyMemory( + var defaultKey = new ReadOnlyMemory( new byte[] { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f }); - SetKeys(DefaultKey, DefaultKey, DefaultKey); - - _disposed = false; + SetKeys(defaultKey, defaultKey, defaultKey); } private void SetKeys(ReadOnlyMemory channelMacKey, @@ -162,17 +158,21 @@ public void Dispose() /// protected virtual void Dispose(bool disposing) { - if (!_disposed) + if (_disposed) { - if (disposing) - { - CryptographicOperations.ZeroMemory(_macKey.AsSpan()); - CryptographicOperations.ZeroMemory(_encKey.AsSpan()); - CryptographicOperations.ZeroMemory(_dekKey.AsSpan()); + return; + } - _disposed = true; - } + if (!disposing) + { + return; } + + CryptographicOperations.ZeroMemory(_macKey.AsSpan()); + CryptographicOperations.ZeroMemory(_encKey.AsSpan()); + CryptographicOperations.ZeroMemory(_dekKey.AsSpan()); + + _disposed = true; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs index 79ee9cfcd..80f3a89cf 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/ChannelEncryption.cs @@ -30,10 +30,10 @@ public static byte[] EncryptData(byte[] payload, byte[] key, int encryptionCount byte[] ivInput = new byte[16]; countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block - byte[] iv = AesUtilities.BlockCipher(key, ivInput); + var iv = AesUtilities.BlockCipher(key, ivInput); byte[] paddedPayload = Padding.PadToBlockSize(payload); - var encryptedData = AesUtilities.AesCbcEncrypt(key.AsSpan(), iv.AsSpan(), paddedPayload.AsSpan()); + var encryptedData = AesUtilities.AesCbcEncrypt(key.AsSpan(), iv.Span, paddedPayload.AsSpan()); return encryptedData.ToArray(); } @@ -46,9 +46,9 @@ public static byte[] DecryptData(byte[] payload, byte[] key, int encryptionCount byte[] ivInput = new byte[16]; countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block ivInput[0] = 0x80; // to mark as RMAC calculation - byte[] iv = AesUtilities.BlockCipher(key, ivInput); + var iv = AesUtilities.BlockCipher(key, ivInput); - var decryptedData = AesUtilities.AesCbcDecrypt(key.AsSpan(), iv.AsSpan(), payload); + var decryptedData = AesUtilities.AesCbcDecrypt(key.AsSpan(), iv.Span, payload); return Padding.RemovePadding(decryptedData.ToArray()).ToArray(); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs index 22efb2a34..5b3abab7e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/PutKeyCommand.cs @@ -229,16 +229,17 @@ private void BuildKeyDataField( byte[] dataToEncrypt = new byte[AesBlockSize] { 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 }; - byte[] checkBlock = AesUtilities.BlockCipher(keyData, dataToEncrypt.AsSpan()); - byte[] encryptedKey = AesUtilities.BlockCipher(encryptionKey, keyToBlock.Span); + + var checkBlock = AesUtilities.BlockCipher(keyData, dataToEncrypt.AsSpan()); + var encryptedKey = AesUtilities.BlockCipher(encryptionKey, keyToBlock.Span); binaryWriter.Write(KeyType); binaryWriter.Write(BlockSize); binaryWriter.Write(AesBlockSize); - binaryWriter.Write(encryptedKey); + binaryWriter.Write(encryptedKey.ToArray()); binaryWriter.Write(KeyCheckSize); - binaryWriter.Write(checkBlock, 0, KeyCheckSize); - Array.Copy(checkBlock, 0, _checksum, checksumOffset, KeyCheckSize); + binaryWriter.Write(checkBlock.ToArray(), 0, KeyCheckSize); + Array.Copy(checkBlock.ToArray(), 0, _checksum, checksumOffset, KeyCheckSize); } finally { diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/AesUtilitiesTests.cs index 2cc5508ec..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")); From ff36d90532d25d2e3d4a4845f97b90ec744cab30 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 23:32:22 +0100 Subject: [PATCH 22/53] tests: test edits added YubiHsm SCP 11b tests --- .../Yubico/YubiKey/Oath/OathSessionTests.cs | 1 - .../Yubico/YubiKey/Piv/GenerateTests.cs | 6 --- .../Yubico/YubiKey/Piv/GetPutDataTests.cs | 10 +++-- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 13 +++--- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 44 +++++++++++++------ ...03ResponseTests.cs => ScpResponseTests.cs} | 2 +- 6 files changed, 43 insertions(+), 33 deletions(-) rename Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/{Scp03ResponseTests.cs => ScpResponseTests.cs} (98%) diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs index 1bc2a988f..01a4149f9 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Oath/OathSessionTests.cs @@ -14,7 +14,6 @@ using System.Collections.Generic; using Xunit; -using Yubico.YubiKey.Scp; using Yubico.YubiKey.TestUtilities; namespace Yubico.YubiKey.Oath diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs index 4763da4f0..4d1bb46cf 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Piv/GenerateTests.cs @@ -60,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) TODO - // { - // #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 a4c7f474e..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; @@ -115,13 +116,14 @@ public void Chuid_Auth_Req(StandardTestDevice testDeviceType) }; var testDevice = IntegrationTestDeviceEnumeration.GetTestDevice(testDeviceType); - // TODO -#pragma warning disable CS0618 // Type or member is obsolete - using (var pivSession = new PivSession(testDevice, new StaticKeys())) -#pragma warning restore CS0618 // Type or member is obsolete + + 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); var getDataResponse = pivSession.Connection.SendCommand(getDataCommand); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index cccbf9295..7751192c8 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -29,18 +29,18 @@ namespace Yubico.YubiKey.Scp [Trait(TraitTypes.Category, TestCategories.Simple)] public class Scp03Tests { - private IYubiKeyDevice GetDevice( - StandardTestDevice desiredDeviceType, - Transport transport = Transport.All, - FirmwareVersion? minimumFirmwareVersion = null) - => IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, minimumFirmwareVersion); - private readonly ReadOnlyMemory _defaultPin = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; public Scp03Tests() { ResetAllowedDevices(); } + + private IYubiKeyDevice GetDevice( + StandardTestDevice desiredDeviceType, + Transport transport = Transport.All, + FirmwareVersion? minimumFirmwareVersion = null) + => IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, minimumFirmwareVersion); private static void ResetAllowedDevices() { @@ -52,7 +52,6 @@ private static void ResetAllowedDevices() } } - [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] [InlineData(StandardTestDevice.Fw5Fips)] diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 4a4e04e2f..d38f831a4 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -29,6 +29,7 @@ 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; @@ -40,18 +41,18 @@ public class Scp11Tests { private const byte OceKid = 0x010; + public Scp11Tests() + { + ResetAllowedDevices(); + } + private IYubiKeyDevice GetDevice( StandardTestDevice desiredDeviceType, Transport transport = Transport.SmartCard, FirmwareVersion? minimumFirmwareVersion = null) => IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, minimumFirmwareVersion ?? FirmwareVersion.V5_7_2); - - public Scp11Tests() - { - ResetAllowedDevices(); - } - + private static void ResetAllowedDevices() { // Reset all attached allowed devices @@ -75,8 +76,7 @@ public void Scp11b_PivSession_Operations_Succeeds( using var session = new PivSession(testDevice, keyParams); session.ResetApplication(); - var collectorObj = new Simple39KeyCollector(); - session.KeyCollector = collectorObj.Simple39KeyCollectorDelegate; + session.KeyCollector = new Simple39KeyCollector().Simple39KeyCollectorDelegate; var isVerified = session.TryVerifyPin(); Assert.True(isVerified); @@ -100,8 +100,7 @@ public void Scp11b_OathSession_Operations_Succeeds( } using var session = new OathSession(testDevice, keyParams); - var collectorObj = new SimpleOathKeyCollector(); - session.KeyCollector = collectorObj.SimpleKeyCollectorDelegate; + session.KeyCollector = new SimpleOathKeyCollector().SimpleKeyCollectorDelegate; session.SetPassword(); Assert.True(session.IsPasswordProtected); @@ -117,19 +116,36 @@ public void Scp11b_OtpSession_Operations_Succeeds( var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); var keyParams = Get_Scp11b_EncryptedChannel_Parameters(testDevice, keyReference); - using var otpSession = new OtpSession(testDevice, keyParams); - if (otpSession.IsLongPressConfigured) + using var session = new OtpSession(testDevice, keyParams); + if (session.IsLongPressConfigured) { - otpSession.DeleteSlot(Slot.LongPress); + session.DeleteSlot(Slot.LongPress); } - var configObj = otpSession.ConfigureStaticPassword(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_YubiHsmSession_Operations_Succeeds( + StandardTestDevice desiredDeviceType) + { + var testDevice = YhaTestUtilities.GetCleanDevice(desiredDeviceType); + var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); + var keyParams = Get_Scp11b_EncryptedChannel_Parameters(testDevice, keyReference); + + using var session = new YubiHsmAuthSession(testDevice, keyParams); + session.AddCredential(YhaTestUtilities.DefaultMgmtKey, YhaTestUtilities.DefaultAes128Cred); + + var result = session.ListCredentials(); + Assert.Single(result); + } [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/Scp03ResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/ScpResponseTests.cs similarity index 98% rename from Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/Scp03ResponseTests.cs rename to Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/ScpResponseTests.cs index d658c2b69..4c011c35d 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/Scp03ResponseTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Commands/ScpResponseTests.cs @@ -17,7 +17,7 @@ using Yubico.Core.Iso7816; using Yubico.YubiKey.Scp.Commands; -namespace Yubico.YubiKey.Scp0.Commands +namespace Yubico.YubiKey.Scp.Commands { public class ScpResponseTests { From d77bc6a2f702101e1e2394a49a219f3e803dd473 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 27 Nov 2024 23:55:43 +0100 Subject: [PATCH 23/53] docs, misc: edits in ScpApduTransform throws exception on invalid KeyParameters add docs --- .../YubiKey/Pipelines/ScpApduTransform.cs | 75 ++++++++++--------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs index 8337a4ed9..4f9d5da14 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -31,22 +31,22 @@ namespace Yubico.YubiKey.Pipelines /// internal class ScpApduTransform : IApduTransform, IDisposable { + /// + /// The for the SCP connection + /// public ScpKeyParameters KeyParameters { get; } - public EncryptDataFunc EncryptDataFunc => - _dataEncryptor ?? ThrowIfUninitialized(); - + /// + /// The which encrypts any data using the session key + /// + /// Thrown when the has not been initialized. + public EncryptDataFunc EncryptDataFunc => _dataEncryptor ?? ThrowIfUninitialized(); + private ScpState ScpState => _scpState ?? ThrowIfUninitialized(); private EncryptDataFunc? _dataEncryptor; - - private ScpState ScpState => - _scpState ?? ThrowIfUninitialized(); - private readonly IApduTransform _pipeline; private ScpState? _scpState; private bool _disposed; - [DoesNotReturn] - private T ThrowIfUninitialized() => throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); /// /// Constructs a new pipeline from the given one. /// @@ -61,17 +61,21 @@ public ScpApduTransform(IApduTransform pipeline, ScpKeyParameters keyParameters) /// /// Performs SCP handshake. Must be called after SELECT. /// + /// Thrown if the instance is invalid. public void Setup() { _pipeline.Setup(); - if (KeyParameters.GetType() == typeof(Scp03KeyParameters)) - { - _dataEncryptor = InitializeScp03((Scp03KeyParameters)KeyParameters); - } - else if (KeyParameters.GetType() == typeof(Scp11KeyParameters)) + switch (KeyParameters) { - _dataEncryptor = InitializeScp11((Scp11KeyParameters)KeyParameters); + case Scp03KeyParameters scp03KeyParameters: + InitializeScp03(scp03KeyParameters); + break; + case Scp11KeyParameters scp11KeyParameters: + InitializeScp11(scp11KeyParameters); + break; + default: + throw new ArgumentException($"Type of {nameof(KeyParameters)} is not recognized", nameof(KeyParameters)); } } @@ -94,32 +98,16 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT var encodedCommand = ScpState.EncodeCommand(command); var response = _pipeline.Invoke(encodedCommand, commandType, responseType); - return ScpState.DecodeResponse(response); } - private static bool ShouldNotEncode(Type commandType) - { - // This method introduced high coupling between the SCP pipeline and the applications. - // The applications should not have to know about the SCP pipeline, or they should be able to - // send the commands without the pipeline. - var exemptionList = new[] - { - typeof(InterIndustry.Commands.SelectApplicationCommand), - typeof(Oath.Commands.SelectOathCommand), - typeof(Scp.Commands.ResetCommand), - }; - - return exemptionList.Contains(commandType); - } - - private EncryptDataFunc InitializeScp11(Scp11KeyParameters keyParameters) + private void InitializeScp11(Scp11KeyParameters keyParameters) { _scpState = Scp11State.CreateScpState(_pipeline, keyParameters); - return _scpState.GetDataEncryptor(); + _dataEncryptor = _scpState.GetDataEncryptor(); } - private EncryptDataFunc InitializeScp03(Scp03KeyParameters keyParams) + private void InitializeScp03(Scp03KeyParameters keyParams) { // Generate host challenge using var rng = CryptographyProviders.RngCreator(); @@ -127,8 +115,25 @@ private EncryptDataFunc InitializeScp03(Scp03KeyParameters keyParams) rng.GetBytes(hostChallenge); _scpState = Scp03State.CreateScpState(_pipeline, keyParams, hostChallenge); + _dataEncryptor = _scpState.GetDataEncryptor(); + } - return _scpState.GetDataEncryptor(); + [DoesNotReturn] + private T ThrowIfUninitialized() => throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); + + private static bool ShouldNotEncode(Type commandType) + { + // This method introduces some coupling between the SCP pipeline and the applications. + // The applications should not have to know about the SCP pipeline, or they should be able to + // send the commands without the pipeline. + var exemptionList = new[] + { + typeof(InterIndustry.Commands.SelectApplicationCommand), + typeof(Oath.Commands.SelectOathCommand), + typeof(Scp.Commands.ResetCommand), + }; + + return exemptionList.Contains(commandType); } // There is a call to cleanup and a call to Dispose. The cleanup only From 11b87d9dd4a7ae8f8c326ef4def49c440693f3de Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 28 Nov 2024 00:30:16 +0100 Subject: [PATCH 24/53] misc: edits to StoreDataCommand and ChannelEncryption ChannelEncyrption: use Span instead of byte[] ChannelMac: use Span instead of byte[] StoreDataCommand: change order of fields --- .../YubiKey/Scp/Commands/StoreDataCommand.cs | 3 +- .../YubiKey/Scp/Helpers/ChannelEncryption.cs | 39 +++++++++++-------- .../Yubico/YubiKey/Scp/Helpers/ChannelMac.cs | 29 +++++++------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs index 2b53c626d..acc236d12 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/StoreDataCommand.cs @@ -28,11 +28,10 @@ namespace Yubico.YubiKey.Scp.Commands /// internal class StoreDataCommand : IYubiKeyCommand { + public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; private const byte GpStoreDataIns = 0xE2; private readonly ReadOnlyMemory _data; - public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; - /// /// Initializes a new instance of the class, with the given data to be stored. /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs index 23fd84863..5fbaa976f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelEncryption.cs @@ -23,7 +23,7 @@ internal static class ChannelEncryption /// /// Encrypts the provided data using AES CBC mode with the given key and encryption counter. /// - /// The data to be encrypted. + /// The data to be encrypted. /// The AES key to use for encryption. /// /// A counter used to generate the initialization vector (IV) for encryption. @@ -32,20 +32,23 @@ internal static class ChannelEncryption /// A containing the encrypted data. /// public static ReadOnlyMemory EncryptData( - ReadOnlySpan dataToEncrypt, + ReadOnlySpan plainText, ReadOnlySpan encryptionKey, int encryptionCounter) { - // NB: Could skip this if the payload is empty (rather than sending a 16-byte encrypted '0x800000...' payload - byte[] countBytes = new byte[sizeof(int)]; + Span countBytes = stackalloc byte[sizeof(int)]; BinaryPrimitives.WriteInt32BigEndian(countBytes, encryptionCounter); - byte[] ivInput = new byte[16]; - countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block - var iv = AesUtilities.BlockCipher(encryptionKey, ivInput); + Span ivInput = stackalloc byte[16]; + int offset = 16 - countBytes.Length; + countBytes.CopyTo(ivInput[offset..]); - var paddedPayload = Padding.PadToBlockSize(dataToEncrypt); - var encryptedData = AesUtilities.AesCbcEncrypt(encryptionKey, iv.Span, paddedPayload.Span); + var iv = AesUtilities.BlockCipher(encryptionKey, ivInput); + var paddedPlaintext = Padding.PadToBlockSize(plainText); + var encryptedData = AesUtilities.AesCbcEncrypt( + encryptionKey, + iv.Span, + paddedPlaintext.Span); return encryptedData; } @@ -53,7 +56,7 @@ public static ReadOnlyMemory EncryptData( /// /// Decrypts the provided data using AES CBC mode with the given key and encryption counter. /// - /// The encrypted data to be decrypted. + /// The encrypted data to be decrypted. /// The AES key to use for decryption. /// /// A counter used to generate the initialization vector (IV) for decryption. @@ -62,21 +65,23 @@ public static ReadOnlyMemory EncryptData( /// A containing the decrypted data with padding removed. /// public static ReadOnlyMemory DecryptData( - ReadOnlySpan dataToDecrypt, + ReadOnlySpan encryptedData, ReadOnlySpan key, int encryptionCounter) { - byte[] countBytes = new byte[sizeof(int)]; + Span countBytes = stackalloc byte[sizeof(int)]; BinaryPrimitives.WriteInt32BigEndian(countBytes, encryptionCounter); - byte[] ivInput = new byte[16]; - countBytes.CopyTo(ivInput, 16 - countBytes.Length); // copy to rightmost part of block + Span ivInput = stackalloc byte[16]; + int offset = 16 - countBytes.Length; ivInput[0] = 0x80; // to mark as RMAC calculation + countBytes.CopyTo(ivInput[offset..]); var iv = AesUtilities.BlockCipher(key, ivInput); - var decryptedData = AesUtilities.AesCbcDecrypt(key, iv.Span, dataToDecrypt); - - return Padding.RemovePadding(decryptedData.Span); + var decryptedData = AesUtilities.AesCbcDecrypt(key, iv.Span, encryptedData); + var plainText = Padding.RemovePadding(decryptedData.Span); + + return plainText; } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs index 137a80557..62af3b8f3 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/ChannelMac.cs @@ -50,25 +50,27 @@ public static (CommandApdu macdApdu, ReadOnlyMemory newMacChainingValue) M } // Add 8 bytes of space for the MAC that will be calculated - var apduWithLongerLen = AddDataToApdu(apdu, new byte[8]); - byte[] apduBytesWithZeroMac = apduWithLongerLen.AsByteArray(); + var apduWithLongerLen = AddDataToApdu(apdu, stackalloc byte[8]); + Span apduBytesWithZeroMac = apduWithLongerLen.AsByteArray(); // Prepare input for MAC calculation: // - First 16 bytes: MAC chaining value from previous operation // - Remaining bytes: APDU bytes (excluding the 8 zero bytes we added) - byte[] macInp = new byte[16 + apduBytesWithZeroMac.Length - 8]; - macChainingValue.CopyTo(macInp); - apduBytesWithZeroMac.AsSpan(0, apduBytesWithZeroMac.Length - 8).CopyTo(macInp.AsSpan(16)); + Span macInputBuffer = stackalloc byte[16 + apduBytesWithZeroMac.Length - 8]; + macChainingValue.CopyTo(macInputBuffer); + apduBytesWithZeroMac[..^8].CopyTo(macInputBuffer[16..]); // Calculate MAC using AES-CMAC (16 bytes) - Span newMacChainingValue = stackalloc byte[16]; using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + + Span newMacChainingValue = stackalloc byte[16]; cmacObj.CmacInit(macKey); - cmacObj.CmacUpdate(macInp); + cmacObj.CmacUpdate(macInputBuffer); cmacObj.CmacFinal(newMacChainingValue); // Create final APDU with the first 8 bytes of the MAC appended var macdApdu = AddDataToApdu(apdu, newMacChainingValue[..8]); + return (macdApdu, newMacChainingValue.ToArray()); } @@ -109,18 +111,19 @@ public static void VerifyRmac( // - Next bytes: Response data (excluding MAC) // - Last 2 bytes: Success status words (90 00) int respDataLen = response.Length - 8; - Span macInp = stackalloc byte[16 + respDataLen + 2]; - macChainingValue.CopyTo(macInp); - response[..respDataLen].CopyTo(macInp[16..]); + Span macInput = stackalloc byte[16 + respDataLen + 2]; + macChainingValue.CopyTo(macInput); + response[..respDataLen].CopyTo(macInput[16..]); - macInp[16 + respDataLen] = SW1Constants.Success; - macInp[16 + respDataLen + 1] = SWConstants.Success & 0xFF; + macInput[16 + respDataLen] = SW1Constants.Success; + macInput[16 + respDataLen + 1] = SWConstants.Success & 0xFF; // Calculate expected MAC using AES-CMAC using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); + Span cmac = stackalloc byte[16]; cmacObj.CmacInit(rmacKey); - cmacObj.CmacUpdate(macInp); + cmacObj.CmacUpdate(macInput); cmacObj.CmacFinal(cmac); // Use constant-time comparison to prevent timing attacks From 3e0419620d29260424716f21de10cb894aaf5f03 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 28 Nov 2024 01:48:33 +0100 Subject: [PATCH 25/53] tests: minor work on Scp-related tests remove bad tests (PaddingTests.cs) --- .../integration/Yubico/YubiKey/Scp/Scp03Tests.cs | 5 +++-- .../unit/Yubico/YubiKey/Scp/PaddingTests.cs | 16 ---------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index 7751192c8..4bf8a908d 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -55,7 +55,7 @@ private static void ResetAllowedDevices() [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_TestImportKey(StandardTestDevice desiredDeviceType) + public void Scp03_PutKey_with_StaticKey_Imports_Key(StandardTestDevice desiredDeviceType) { byte[] sk = { @@ -88,7 +88,7 @@ public void Scp03_TestImportKey(StandardTestDevice desiredDeviceType) [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_PutKey_WithPublicKey_Succeeds(StandardTestDevice desiredDeviceType) + public void Scp03_PutKey_with_PublicKey_Imports_Key(StandardTestDevice desiredDeviceType) { var keyReference = new KeyReference(ScpKeyIds.ScpCaPublicKey, 0x3); var testDevice = GetDevice(desiredDeviceType, Transport.All, FirmwareVersion.V5_7_2); @@ -99,6 +99,7 @@ public void Scp03_PutKey_WithPublicKey_Succeeds(StandardTestDevice desiredDevice 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)); } diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs index 647721978..77b659f19 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/PaddingTests.cs @@ -28,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() { From 48e8d8efa7c4530411832f05dad08ef9d9cb11af Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 28 Nov 2024 01:48:44 +0100 Subject: [PATCH 26/53] misc: minor work on scp related classes use ReadOnlyMemory in ScpState and SessionKeys.cs added validation of hostchallenge length added documentation for the class refactor around external authenticate for clarity no need to check for null (Padding.cs) --- .../src/Yubico/YubiKey/Scp/Helpers/Padding.cs | 5 -- .../src/Yubico/YubiKey/Scp/Scp03State.cs | 67 +++++++++++++++---- .../src/Yubico/YubiKey/Scp/ScpState.cs | 10 +-- .../src/Yubico/YubiKey/Scp/SessionKeys.cs | 26 +++---- 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs index 1c61ab063..cdea0b945 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Padding.cs @@ -43,11 +43,6 @@ public static Memory PadToBlockSize(ReadOnlySpan payload) /// The padding is invalid. public static Memory RemovePadding(ReadOnlySpan paddedPayload) { - if (paddedPayload == null) - { - throw new ArgumentNullException(nameof(paddedPayload)); - } - for (int i = paddedPayload.Length - 1; i >= 0; i--) { if (paddedPayload[i] == 0x80) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs index 800ff8abe..74ed9815d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs @@ -15,28 +15,55 @@ using System; using System.Linq; using System.Security.Cryptography; +using Yubico.Core.Iso7816; using Yubico.YubiKey.Scp.Commands; using Yubico.YubiKey.Scp.Helpers; namespace Yubico.YubiKey.Scp { + /// + /// Manages the state for Secure Channel Protocol 03 (SCP03) communication with a YubiKey. + /// This class handles key agreement, authentication, and secure messaging between the host + /// and the YubiKey using AES encryption and MAC operations. It supports the establishment + /// of secure channels with PIV and other smart card applications by deriving session keys + /// and performing mutual authentication. + /// internal class Scp03State : ScpState { private readonly ReadOnlyMemory _hostCryptogram; + /// + /// Creates a new SCP03 secure channel state using the provided session keys and host cryptogram. + /// + /// The session keys generated by the SCP03 key agreement. + /// The host cryptogram used to authenticate the host and establish the secure channel. public Scp03State( SessionKeys sessionKeys, - Memory hostCryptogram) - : base(sessionKeys, new Memory(new byte[16])) + ReadOnlyMemory hostCryptogram) + : base(sessionKeys, new byte[16]) { _hostCryptogram = hostCryptogram; } + /// + /// Creates a new SCP03 secure channel state by performing key agreement and authentication with the YubiKey. + /// + /// The APDU pipeline for communication with the YubiKey. + /// The key parameters required for SCP03 authentication. + /// The host challenge to use for the key agreement. + /// A new instance of configured for secure channel communication. + /// Thrown when host challenge or key parameters are invalid or incompatible. + /// Thrown when secure channel establishment fails. internal static Scp03State CreateScpState( IApduTransform pipeline, Scp03KeyParameters keyParameters, ReadOnlyMemory hostChallenge) { + if (hostChallenge.Length != 8) + { + throw new ArgumentException("Invalid size, must be 8 bytes", nameof(hostChallenge)); + } + var (cardChallenge, cardCryptogram) = PerformInitializeUpdate(pipeline, keyParameters, hostChallenge); var state = CreateScpState(keyParameters, hostChallenge, cardChallenge, cardCryptogram); state.PerformExternalAuthenticate(pipeline); @@ -94,8 +121,8 @@ private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCry var initializeUpdateResponse = initializeUpdateCommand.CreateResponseForApdu(initializeUpdateResponseApdu); initializeUpdateResponse.ThrowIfFailed($"Error when performing {initializeUpdateCommand.GetType().Name}: {initializeUpdateResponse.StatusMessage}"); - var cardChallenge = initializeUpdateResponse.CardChallenge.ToArray().AsMemory(); - var cardCryptogram = initializeUpdateResponse.CardCryptogram.ToArray().AsMemory(); + byte[] cardChallenge = initializeUpdateResponse.CardChallenge.ToArray(); + byte[] cardCryptogram = initializeUpdateResponse.CardCryptogram.ToArray(); return (cardChallenge, cardCryptogram); } @@ -103,24 +130,36 @@ private static (ReadOnlyMemory cardChallenge, ReadOnlyMemory cardCry private void PerformExternalAuthenticate(IApduTransform pipeline) { // Create a MAC:ed APDU - var eaCommandPlain = new ExternalAuthenticateCommand(_hostCryptogram); - var (macdApdu, newMacChainingValue) = MacApdu( - eaCommandPlain.CreateCommandApdu(), - SessionKeys.MacKey.ToArray(), - MacChainingValue.ToArray() - ); + var (commandExtAuth, newMacChainingValue) = GetMacedExternalAuthenticateCommand( + new ExternalAuthenticateCommand(_hostCryptogram)); // Update the states MacChainingValue MacChainingValue = newMacChainingValue; // Send command - var eaCommandMaced = new ExternalAuthenticateCommand(macdApdu.Data.ToArray()); - var eaResponseApdu = pipeline.Invoke( - eaCommandMaced.CreateCommandApdu(), + var responseExtAuth = pipeline.Invoke( + commandExtAuth.CreateCommandApdu(), typeof(ExternalAuthenticateCommand), typeof(ExternalAuthenticateResponse)); - eaCommandMaced.CreateResponseForApdu(eaResponseApdu).ThrowIfFailed(); + commandExtAuth.CreateResponseForApdu(responseExtAuth).ThrowIfFailed(); + } + + private (ExternalAuthenticateCommand, ReadOnlyMemory macChainingValue) GetMacedExternalAuthenticateCommand(ExternalAuthenticateCommand externalAuthenticateCommand) + { + (var commandDataMaced, var newMacChainingValue) = GetMacdCommandData(externalAuthenticateCommand.CreateCommandApdu()); + return (new ExternalAuthenticateCommand(commandDataMaced), newMacChainingValue); + } + + private (ReadOnlyMemory commandDataMaced, ReadOnlyMemory newMacChainingValue) GetMacdCommandData(CommandApdu commandApdu) + { + var (macedApdu, newMacChainingValue) = MacApdu( + commandApdu, + SessionKeys.MacKey.Span, + MacChainingValue.Span + ); + + return (macedApdu.Data, newMacChainingValue); } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs index 95e699bab..b4d9420a1 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs @@ -23,7 +23,7 @@ internal abstract class ScpState : IDisposable /// /// Initializes the host-side state for an SCP session. /// - protected ScpState(SessionKeys sessionKeys, Memory macChain) + protected ScpState(SessionKeys sessionKeys, ReadOnlyMemory macChain) { MacChainingValue = macChain; SessionKeys = sessionKeys; @@ -164,14 +164,14 @@ public EncryptDataFunc GetDataEncryptor() protected static (CommandApdu macdApdu, ReadOnlyMemory newMacChainingValue) MacApdu( CommandApdu commandApdu, ReadOnlySpan macKey, - ReadOnlySpan macChainingValue) => - ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); + ReadOnlySpan macChainingValue) + => ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); private static void VerifyRmac( ReadOnlySpan responseData, ReadOnlySpan rmacKey, - ReadOnlySpan macChainingValue) => - ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); + ReadOnlySpan macChainingValue) + => ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); public void Dispose() { diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs index af450a87c..772783d46 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs @@ -39,10 +39,10 @@ internal class SessionKeys : IDisposable /// public ReadOnlyMemory? DataEncryptionKey => _dataEncryptionKey; - private readonly Memory _macKey = new byte[16]; - private readonly Memory _encryptionKey = new byte[16]; - private readonly Memory _rmacKey = new byte[16]; - private readonly Memory _dataEncryptionKey = new byte[16]; + private readonly Memory _macKey; + private readonly Memory _encryptionKey; + private readonly Memory _rmacKey; + private readonly Memory _dataEncryptionKey; private bool _disposed; /// @@ -53,23 +53,23 @@ internal class SessionKeys : IDisposable /// The session RMAC key. /// The session data encryption key. Optional. public SessionKeys( - Memory macKey, - Memory encryptionKey, - Memory rmacKey, - Memory dataEncryptionKey) + ReadOnlyMemory macKey, + ReadOnlyMemory encryptionKey, + ReadOnlyMemory rmacKey, + ReadOnlyMemory dataEncryptionKey) { ValidateKeyLength(macKey, nameof(macKey)); ValidateKeyLength(encryptionKey, nameof(encryptionKey)); ValidateKeyLength(rmacKey, nameof(rmacKey)); ValidateKeyLength(dataEncryptionKey, nameof(dataEncryptionKey)); - macKey.CopyTo(_macKey); - encryptionKey.CopyTo(_encryptionKey); - rmacKey.CopyTo(_rmacKey); - dataEncryptionKey.CopyTo(_dataEncryptionKey); + _macKey = macKey.ToArray(); + _encryptionKey = encryptionKey.ToArray(); + _rmacKey = rmacKey.ToArray(); + _dataEncryptionKey = dataEncryptionKey.ToArray(); } - private static void ValidateKeyLength(Memory key, string paramName) + private static void ValidateKeyLength(ReadOnlyMemory key, string paramName) { if (key.Length != 16) { From e8fbab3ca143a88a7940729ef24b50883857d05e Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 28 Nov 2024 11:13:33 +0100 Subject: [PATCH 27/53] docs: edited docstrings and comments added docstring for Scp11State removed unintended comments in PivSession --- Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs | 9 +-------- Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs | 8 +++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs index 93b3e46b1..4b870e81c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs @@ -188,7 +188,7 @@ public PivSession(IYubiKeyDevice yubiKey, Yubico.YubiKey.Scp03.StaticKeys scp03K /// operations. /// /// - /// The SCP03 key parameters, if any, to use in establishing the SCP connection. + /// The SCP key parameters, if any, to use in establishing the SCP connection. /// /// /// The yubiKey argument is null. @@ -203,13 +203,6 @@ public PivSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null RefreshManagementKeyAlgorithm(); } - // /// - // /// The object that represents the connection to the YubiKey. Most - // /// applications will ignore this, but it can be used to call Commands - // /// directly. - // /// - // public IYubiKeyConnection Connection { get; } - /// /// The Delegate this class will call when it needs a PIN, PUK, or /// management key. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index bbe752b8c..d8785099a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -16,7 +16,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Security.Cryptography; using Yubico.Core.Cryptography; using Yubico.Core.Iso7816; @@ -26,6 +25,13 @@ namespace Yubico.YubiKey.Scp { + /// + /// Manages the state for Secure Channel Protocol 11 (SCP11) communication with a YubiKey. + /// This class handles key agreement, authentication, and secure messaging between the host + /// and the YubiKey using AES-128 encryption and MAC operations. It supports different SCP11 + /// variants (11a, 11b, 11c) for establishing secure channels with PIV and other smart card + /// applications. + /// internal class Scp11State : ScpState { private const int ReceiptTag = 0x86; From d50a2c9d436591728ed555d4ff1ed2e4dc9a20c1 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 28 Nov 2024 12:20:20 +0100 Subject: [PATCH 28/53] misc: typos and formatting docs: revert auto format of .md files misc: fixed typo in SecurityDomainSession --- .../application-piv/apdu/auth-sign.md | 21 ++++++++++--------- .../application-piv/apdu/generate-pair.md | 16 +++++++------- .../YubiKey/Scp/SecurityDomainSession.cs | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md index def2cecdc..d13e61a58 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/auth-sign.md @@ -17,18 +17,18 @@ limitations under the License. --> ### Command APDU Info | CLA | INS | P1 | P2 | Lc | Data | Le | -| :-: | :-: | :---------: | :-----------: | :--------: | :------------------------------: | :------: | -| 00 | 87 | _algorithm_ | _slot number_ | _data len_ | _encoded digest of data to sign_ | (absent) | +|:---:|:---:|:-----------:|:-------------:|:----------:|:--------------------------------:|:--------:| +| 00 | 87 | *algorithm* | *slot number* | *data len* | *encoded digest of data to sign* | (absent) | The *algorithm* is either `06` (RSA-1024), `07` (RSA-2048), `05` (RSA 3072), `16` (RSA 4096), `11` (ECC-P256), or `14` (ECC-P384). -The _slot number_ can be the number of any slot that holds a private key, other than `F9`. +The *slot number* can be the number of any slot that holds a private key, other than `F9`. That is, the slot number can be any PIV slot other than `80`, `81`, `9B`, or `F9`. The attestation key, `F9`, will sign a certificate it creates, so it can sign. It simply cannot sign arbitrary data, only attestation statements. -The _encoded digest_ is +The *encoded digest* is ```C 7C len1 82 00 81 len2 @@ -79,12 +79,12 @@ For ECC, there is one format: #### Response APDU for AUTHENTICATE:SIGN (success) -Total Length: _variable + 2_\ -Data Length: _variable_ +Total Length: *variable + 2*\ +Data Length: *variable* | Data | SW1 | SW2 | -| :-------------------------------: | :-: | :-: | -| 7C _len1_ 82 _len2 \_ | 90 | 00 | +|:---------------------------------:|:---:|:---:| +| 7C *len1* 82 *len2 \* | 90 | 00 | Note that the signature might be returned over multiple commands. Each return command will be able to return up to 256 bytes. To get more bytes of a return, call the GET @@ -122,8 +122,8 @@ Total Length: 2\ Data Length: 0 | Data | SW1 | SW2 | -| :-------: | :-: | :-: | -| (no data) | 69 | 82 | +|:---------:|:---:|:---:| +| (no data) | 69 | 82 | If the key was generated or imported with a PIN policy other than "Never", and the command was sent without first verifying the PIN or the wrong PIN was entered, then the following @@ -190,3 +190,4 @@ Received (SW1=0x90, SW2=0x00): Sending: 00 C0 00 00 Received (SW1=0x6A, SW2=0x80) ``` + diff --git a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md index 940013584..72d411ed1 100644 --- a/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md +++ b/Yubico.YubiKey/docs/users-manual/application-piv/apdu/generate-pair.md @@ -16,9 +16,9 @@ limitations under the License. --> ### Command APDU Info -| CLA | INS | P1 | P2 | Lc | Data | Le | -| :-: | :-: | :-: | :-----------: | :--------: | :------------------------------------------------------------------------------------------------------------: | :------: | -| 00 | 47 | 00 | _Slot number_ | _data len_ | [AC *\* 80 01 *\*]
\[AA 01 _\_\]
\[AB 01 _\_\] | (absent) | +| CLA | INS | P1 | P2 | Lc | Data | Le | +|:---:|:---:|:--:|:-------------:|:----------:|:--------------------------------------------------------------------------------------------------------------:|:--------:| +| 00 | 47 | 00 | *Slot number* | *data len* | [AC *\* 80 01 *\*]
\[AA 01 *\*\]
\[AB 01 *\*\] | (absent) | The slot number can be one of the following (hex values): @@ -65,17 +65,17 @@ Total Length: 2\ Data Length: 0 | Data | SW1 | SW2 | -| :-------: | :-: | :-: | +|:---------:|:---:|:---:| | (no data) | 69 | 82 | ### Response APDU Info: Success -Total Length: _variable + 2_\ -Data Length: _variable_ +Total Length: *variable + 2*\ +Data Length: *variable* | Data | SW1 | SW2 | -| :----------: | :-: | :-: | -| _public key_ | 90 | 00 | +|:------------:|:---:|:---:| +| *public key* | 90 | 00 | The public key is in the form of a set of TLVs. If the key is ECC, there is one TLV, where the value (the V) is the public point. If the key is RSA, there are two TLVs, where the diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 33790c481..453c56a53 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -347,7 +347,7 @@ public void PutKey(KeyReference keyReference, ECPublicKeyParameters publicKeyPar var pkParams = publicKeyParameters.Parameters; if (pkParams.Curve.Oid.Value != ECCurve.NamedCurves.nistP256.Oid.Value) { - throw new ArgumentException("Private key must be of type NIST P-256"); + throw new ArgumentException("Public key must be of type NIST P-256"); } try From ab94c6e8b6d7f751860acc48142a2f50ff032184 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Thu, 28 Nov 2024 12:42:01 +0100 Subject: [PATCH 29/53] Update build-pull-requests.yml --- .github/workflows/build-pull-requests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-pull-requests.yml b/.github/workflows/build-pull-requests.yml index 22486ae04..2eac0cf48 100644 --- a/.github/workflows/build-pull-requests.yml +++ b/.github/workflows/build-pull-requests.yml @@ -56,7 +56,7 @@ jobs: run: dotnet nuget add source --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/Yubico/index.json" - name: Build Yubico.NET.SDK.sln - run: dotnet build --configuration ReleaseWithDocs --nologo --verbosity normal Yubico.NET.SDK.sln + run: dotnet build --configuration ReleaseWithDocs --nologo --verbosity minimal Yubico.NET.SDK.sln - name: Save build artifacts uses: actions/upload-artifact@v4 @@ -74,4 +74,4 @@ jobs: path: | Yubico.DotNetPolyfills/src/bin/ReleaseWithDocs/**/*.dll Yubico.Core/src/bin/ReleaseWithDocs/**/*.dll - Yubico.YubiKey/src/bin/ReleaseWithDocs/**/*.dll \ No newline at end of file + Yubico.YubiKey/src/bin/ReleaseWithDocs/**/*.dll From 0d9bd6d51e84322c16bc26bac26a5de4a06b60b2 Mon Sep 17 00:00:00 2001 From: Dennis Dyallo Date: Thu, 28 Nov 2024 12:42:28 +0100 Subject: [PATCH 30/53] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58d432659..990b467dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,7 +82,7 @@ jobs: # Build the project - name: Build Yubico.NET.SDK.sln - run: dotnet build --configuration ReleaseWithDocs --nologo --verbosity normal Yubico.NET.SDK.sln + run: dotnet build --configuration ReleaseWithDocs --nologo --verbosity minimal Yubico.NET.SDK.sln # Upload artifacts - name: Save documentation artifacts From a9a1429fb8baf05d946d223e42b038ea58477f88 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Thu, 28 Nov 2024 14:12:33 +0100 Subject: [PATCH 31/53] misc: minor edits (SCP03, SCP11) Scp03 enum is obsolete removed comments added SecurityDomain enum to ConnectionManager.cs YubiKeyDeviceExtensions.cs was not meant to be deleted yet --- .../src/Yubico/YubiKey/ConnectionManager.cs | 1 + .../Yubico/YubiKey/Scp/Scp11KeyParameters.cs | 2 - .../src/Yubico/YubiKey/Scp/StaticKeys.cs | 6 -- .../Commands/InitializeUpdateResponse.cs | 2 +- .../src/Yubico/YubiKey/SmartCardConnection.cs | 3 +- .../src/Yubico/YubiKey/YubiKeyApplication.cs | 67 ++++++++++--------- .../Yubico/YubiKey/YubiKeyDeviceExtensions.cs | 59 ++++++++++++++++ 7 files changed, 99 insertions(+), 41 deletions(-) create mode 100644 Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs index 07d5e9259..e2e8a5abf 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs @@ -77,6 +77,7 @@ public static bool DeviceSupportsApplication(IDevice device, YubiKeyApplication (ISmartCardDevice _, YubiKeyApplication.Piv) => true, (ISmartCardDevice _, YubiKeyApplication.OpenPgp) => true, (ISmartCardDevice _, YubiKeyApplication.InterIndustry) => true, + (ISmartCardDevice _, YubiKeyApplication.SecurityDomain) => true, (ISmartCardDevice _, YubiKeyApplication.YubiHsmAuth) => true, // NB: Certain past models of YK NEO and YK 4 supported these applications over CCID (ISmartCardDevice _, YubiKeyApplication.FidoU2f) => true, diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs index ab164d1bc..986b8787d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs @@ -125,8 +125,6 @@ private void ValidateParameters() } } - - // TODO Is this needed? public void Dispose() { if (_disposed) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs index 5efc83f91..195b5ed20 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/StaticKeys.cs @@ -144,18 +144,12 @@ public bool AreKeysSame(StaticKeys? compareKeys) DataEncryptionKey.Span.SequenceEqual(compareKeys.DataEncryptionKey.Span); } - /// - /// Releases any unmanaged resources and overwrites any sensitive data. - /// public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } - /// - /// Releases any unmanaged resources and overwrites any sensitive data. - /// protected virtual void Dispose(bool disposing) { if (_disposed) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs index 7410fef90..19ee5e979 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp03/Commands/InitializeUpdateResponse.cs @@ -19,7 +19,7 @@ namespace Yubico.YubiKey.Scp03.Commands { - [Obsolete("Use new InitializeUpdateResponse instead")] // TODO Verify still works + [Obsolete("Use new InitializeUpdateResponse instead")] internal class InitializeUpdateResponse : Scp03Response { public IReadOnlyCollection DiversificationData { get; protected set; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs index 9b90fd367..53dffcfcd 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs @@ -52,8 +52,7 @@ public SmartCardConnection( if (yubiKeyApplication == YubiKeyApplication.Fido2) { _apduPipeline = new FidoErrorTransform(_apduPipeline); - } // TODO Why does this constructor following logic differ from overloaded one below? - // It appears to be missing Otp Error Transform + } // CCID has the concept of multiple applications. Since we cannot guarantee the // state of the smart card when connecting, we should always send down a connection diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs index 466d5ad75..3376d18a8 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyApplication.cs @@ -31,42 +31,46 @@ public enum YubiKeyApplication InterIndustry = 8, OtpNdef = 9, YubiHsmAuth = 10, - Scp03 = 11, // TODO This should be removed in future + [Obsolete("Use SecurityDomain instead")] + Scp03 = 11, SecurityDomain = 12 } internal static class YubiKeyApplicationExtensions { - private static readonly byte[] ManagementAppId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 }; - private static readonly byte[] OtpAppId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01 }; - private static readonly byte[] FidoU2fAppId = { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }; - private static readonly byte[] Fido2AppId = { 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01 }; - private static readonly byte[] OathAppId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01 }; - private static readonly byte[] OpenPgpAppId = { 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01 }; - private static readonly byte[] PivAppId = { 0xa0, 0x00, 0x00, 0x03, 0x08 }; - private static readonly byte[] OtpNdef = { 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; - private static readonly byte[] YubiHsmAuthId = { 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01 }; - private static readonly byte[] SecurityDomainAppId = { 0xa0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; + private static readonly byte[] ManagementAppId = { 0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 }; + private static readonly byte[] OtpAppId = { 0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01 }; + private static readonly byte[] FidoU2fAppId = { 0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01 }; + private static readonly byte[] Fido2AppId = { 0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01 }; + private static readonly byte[] OathAppId = { 0xA0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01 }; + private static readonly byte[] OpenPgpAppId = { 0xD2, 0x76, 0x00, 0x01, 0x24, 0x01 }; + private static readonly byte[] PivAppId = { 0xA0, 0x00, 0x00, 0x03, 0x08 }; + private static readonly byte[] OtpNdef = { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; + private static readonly byte[] YubiHsmAuthId = { 0xA0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01 }; + private static readonly byte[] SecurityDomainAppId = { 0xA0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 }; - public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application) => Iso7816ApplicationIds.ContainsKey(application) - ? Iso7816ApplicationIds[application].ToArray() - : throw new NotSupportedException(ExceptionMessages.ApplicationIdNotFound); + public static byte[] GetIso7816ApplicationId(this YubiKeyApplication application) => + Iso7816ApplicationIds.ContainsKey(application) + ? Iso7816ApplicationIds[application].ToArray() + : throw new NotSupportedException(ExceptionMessages.ApplicationIdNotFound); public static IReadOnlyDictionary> Iso7816ApplicationIds => - new Dictionary> - { - { YubiKeyApplication.Management, ManagementAppId }, - { YubiKeyApplication.Otp, OtpAppId }, - { YubiKeyApplication.FidoU2f, FidoU2fAppId }, - { YubiKeyApplication.Fido2, Fido2AppId }, - { YubiKeyApplication.Oath, OathAppId }, - { YubiKeyApplication.OpenPgp, OpenPgpAppId }, - { YubiKeyApplication.Piv, PivAppId }, - { YubiKeyApplication.OtpNdef, OtpNdef }, - { YubiKeyApplication.YubiHsmAuth, YubiHsmAuthId }, - { YubiKeyApplication.Scp03, SecurityDomainAppId }, // Todo check if it can be removed non breaking - { YubiKeyApplication.SecurityDomain, SecurityDomainAppId } - }; + new Dictionary> + { + { YubiKeyApplication.Management, ManagementAppId }, + { YubiKeyApplication.Otp, OtpAppId }, + { YubiKeyApplication.FidoU2f, FidoU2fAppId }, + { YubiKeyApplication.Fido2, Fido2AppId }, + { YubiKeyApplication.Oath, OathAppId }, + { YubiKeyApplication.OpenPgp, OpenPgpAppId }, + { YubiKeyApplication.Piv, PivAppId }, + { YubiKeyApplication.OtpNdef, OtpNdef }, + { YubiKeyApplication.YubiHsmAuth, YubiHsmAuthId }, + #pragma warning disable CS0618 // Type or member is obsolete // Remove in next major release + { YubiKeyApplication.Scp03, SecurityDomainAppId }, + #pragma warning restore CS0618 // Type or member is obsolete // Remove in next major release + { YubiKeyApplication.SecurityDomain, SecurityDomainAppId } + }; /// /// Gets the associated with the given applicationId. @@ -84,10 +88,13 @@ public static YubiKeyApplication GetYubiKeyApplication(this ReadOnlySpan a } } - throw new ArgumentException($"No YubiKey application found with application id: {Base16.EncodeBytes(applicationId)}", nameof(applicationId)); + throw new ArgumentException( + $"No YubiKey application found with application id: {Base16.EncodeBytes(applicationId)}", + nameof(applicationId)); } /// - public static YubiKeyApplication GetYubiKeyApplication(this byte[] applicationId) => GetYubiKeyApplication(applicationId.AsSpan()); + public static YubiKeyApplication GetYubiKeyApplication(this byte[] applicationId) => + GetYubiKeyApplication(applicationId.AsSpan()); } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs new file mode 100644 index 000000000..636e197f7 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceExtensions.cs @@ -0,0 +1,59 @@ +// 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 Yubico.YubiKey.Scp03; + +namespace Yubico.YubiKey +{ + [Obsolete("This class is deprecated")] + public static class YubiKeyDeviceExtensions + { + public static IYubiKeyDevice WithScp03(this YubiKeyDevice device, StaticKeys scp03Keys) => + GetScp03Device(device, scp03Keys); + internal static Scp03YubiKeyDevice GetScp03Device(this IYubiKeyDevice device, StaticKeys scp03Keys) + { + if (device is null) + { + throw new ArgumentNullException(nameof(device)); + } + if (scp03Keys is null) + { + throw new ArgumentNullException(nameof(scp03Keys)); + } + + if (device is Scp03YubiKeyDevice scp03Device) + { + if (scp03Device.StaticKeys.AreKeysSame(scp03Keys)) + { + return scp03Device; + } + + throw new ArgumentException(ExceptionMessages.Scp03KeyMismatch); + } + + if (device is YubiKeyDevice yubiKeyDevice) + { + if (!yubiKeyDevice.HasSmartCard) + { + throw new NotSupportedException(ExceptionMessages.CcidNotSupported); + } + + return new Scp03YubiKeyDevice(yubiKeyDevice, scp03Keys); + } + + throw new NotSupportedException(ExceptionMessages.NotSupportedByYubiKeyVersion); + } + } +} From 25a010b50b4c180d7679ff79b2a27de78c210f5c Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Fri, 29 Nov 2024 15:18:02 +0100 Subject: [PATCH 32/53] tests: possibility to select by transport Scp03Tests: All tests select transport. Will fail test if Nfc and Fips are selected --- .../Yubico/YubiKey/Scp/Scp03Tests.cs | 203 ++++++++++++------ .../IntegrationTestDeviceEnumeration.cs | 2 +- .../TestUtilities/TestDeviceSelection.cs | 31 ++- 3 files changed, 150 insertions(+), 86 deletions(-) diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs index 4bf8a908d..e67c657e2 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp03Tests.cs @@ -21,6 +21,7 @@ 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; @@ -35,12 +36,25 @@ public Scp03Tests() { ResetAllowedDevices(); } - + private IYubiKeyDevice GetDevice( StandardTestDevice desiredDeviceType, - Transport transport = Transport.All, - FirmwareVersion? minimumFirmwareVersion = null) - => IntegrationTestDeviceEnumeration.GetTestDevice(desiredDeviceType, transport, minimumFirmwareVersion); + 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 ResetAllowedDevices() { @@ -53,9 +67,10 @@ private static void ResetAllowedDevices() } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_PutKey_with_StaticKey_Imports_Key(StandardTestDevice desiredDeviceType) + [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 = { @@ -63,7 +78,7 @@ public void Scp03_PutKey_with_StaticKey_Imports_Key(StandardTestDevice desiredDe 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, }; - var testDevice = GetDevice(desiredDeviceType); + var testDevice = GetDevice(desiredDeviceType, transport); var newKeyParams = Scp03KeyParameters.FromStaticKeys(new StaticKeys(sk, sk, sk)); // Authenticate with default key, then replace default key @@ -86,12 +101,49 @@ public void Scp03_PutKey_with_StaticKey_Imports_Key(StandardTestDevice desiredDe } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_PutKey_with_PublicKey_Imports_Key(StandardTestDevice desiredDeviceType) + [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.All, FirmwareVersion.V5_7_2); + 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); @@ -105,11 +157,12 @@ public void Scp03_PutKey_with_PublicKey_Imports_Key(StandardTestDevice desiredDe } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_AuthenticateWithWrongKey_Should_ThrowException(StandardTestDevice desiredDeviceType) + [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); + var testDevice = GetDevice(desiredDeviceType, transport); var incorrectKeys = RandomStaticKeys(); var keyRef = Scp03KeyParameters.FromStaticKeys(incorrectKeys); @@ -126,11 +179,12 @@ public void Scp03_AuthenticateWithWrongKey_Should_ThrowException(StandardTestDev } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_GetInformation_WithDefaultKey_Returns_DefaultKey(StandardTestDevice desiredDeviceType) + [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); + var testDevice = GetDevice(desiredDeviceType, transport); using var session = new SecurityDomainSession(testDevice); var result = session.GetKeyInformation(); @@ -146,18 +200,19 @@ public void Scp03_GetInformation_WithDefaultKey_Returns_DefaultKey(StandardTestD } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_Connect_GetKeyInformation_WithDefaultKey_Returns_DefaultKey(StandardTestDevice desiredDeviceType) + [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); + 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); @@ -187,11 +242,12 @@ public void Scp03_Connect_GetKeyInformation_WithDefaultKey_Returns_DefaultKey(St } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_GetCertificates_ReturnsCerts(StandardTestDevice desiredDeviceType) + [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.All, FirmwareVersion.V5_7_2); + var testDevice = GetDevice(desiredDeviceType, transport, FirmwareVersion.V5_7_2); using var session = new SecurityDomainSession(testDevice); @@ -202,11 +258,12 @@ public void Scp03_GetCertificates_ReturnsCerts(StandardTestDevice desiredDeviceT } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_Reset_Restores_SecurityDomainKeys_To_FactoryKeys(StandardTestDevice desiredDeviceType) + [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); + var testDevice = GetDevice(desiredDeviceType, transport); byte[] sk = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, @@ -218,9 +275,6 @@ public void Scp03_Reset_Restores_SecurityDomainKeys_To_FactoryKeys(StandardTestD 0x01, new StaticKeys(sk, sk, sk)); - // assumeFalse("SCP03 not supported over NFC on FIPS capable devices", TODO - // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - using (var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey)) { session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys, 0); @@ -253,11 +307,12 @@ public void Scp03_Reset_Restores_SecurityDomainKeys_To_FactoryKeys(StandardTestD } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_GetSupportedCaIdentifiers_Succeeds(StandardTestDevice desiredDeviceType) + [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.All, FirmwareVersion.V5_7_2); + var testDevice = GetDevice(desiredDeviceType, transport, FirmwareVersion.V5_7_2); using var session = new SecurityDomainSession(testDevice, Scp03KeyParameters.DefaultKey); var result = session.GetSupportedCaIdentifiers(true, true); @@ -265,38 +320,44 @@ public void Scp03_GetSupportedCaIdentifiers_Succeeds(StandardTestDevice desiredD } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_GetCardRecognitionData_Succeeds(StandardTestDevice desiredDeviceType) + [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); + 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)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_GetData_Succeeds(StandardTestDevice desiredDeviceType) + [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); + 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)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds(StandardTestDevice desiredDeviceType) + [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); + 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); - var result = pivSession.TryVerifyPin(_defaultPin, out _); Assert.True(result); @@ -306,11 +367,12 @@ public void Scp03_PivSession_TryVerifyPinAndGetMetaData_Succeeds(StandardTestDev } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_Device_Connect_With_Application_Succeeds(StandardTestDevice desiredDeviceType) + [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) { - var testDevice = GetDevice(desiredDeviceType); + var testDevice = GetDevice(desiredDeviceType, transport); Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); using var connection = testDevice.Connect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey); @@ -322,11 +384,12 @@ public void Scp03_Device_Connect_With_Application_Succeeds(StandardTestDevice de } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_Device_Connect_ApplicationId_Succeeds(StandardTestDevice desiredDeviceType) + [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) { - var testDevice = GetDevice(desiredDeviceType); + var testDevice = GetDevice(desiredDeviceType, transport); Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); using IYubiKeyConnection connection = testDevice.Connect( @@ -340,11 +403,12 @@ public void Scp03_Device_Connect_ApplicationId_Succeeds(StandardTestDevice desir } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_Device_TryConnect_With_Application_Succeeds(StandardTestDevice desiredDeviceType) + [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) { - var testDevice = GetDevice(desiredDeviceType); + var testDevice = GetDevice(desiredDeviceType, transport); Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); var isValid = testDevice.TryConnect( @@ -363,11 +427,12 @@ public void Scp03_Device_TryConnect_With_Application_Succeeds(StandardTestDevice } [SkippableTheory(typeof(DeviceNotFoundException))] - [InlineData(StandardTestDevice.Fw5)] - [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp03_Device_TryConnect_With_ApplicationId_Succeeds(StandardTestDevice desiredDeviceType) + [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) { - var testDevice = GetDevice(desiredDeviceType); + var testDevice = GetDevice(desiredDeviceType, transport); Assert.True(testDevice.FirmwareVersion >= FirmwareVersion.V5_3_0); var isValid = testDevice.TryConnect(YubiKeyApplication.Piv, Scp03KeyParameters.DefaultKey, diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs index e26d2ee94..29a070a0d 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs +++ b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/IntegrationTestDeviceEnumeration.cs @@ -125,7 +125,7 @@ public static IYubiKeyDevice GetTestDevice( Transport transport = Transport.All, FirmwareVersion? minimumFirmwareVersion = null) => GetTestDevices(transport) - .SelectByStandardTestDevice(testDeviceType, minimumFirmwareVersion); + .SelectByStandardTestDevice(testDeviceType, minimumFirmwareVersion, transport); /// /// Get YubiKey test device of specified transport and for which the diff --git a/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs b/Yubico.YubiKey/tests/utilities/Yubico/YubiKey/TestUtilities/TestDeviceSelection.cs index 2084ee867..5d5954423 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; @@ -63,17 +64,20 @@ public static IYubiKeyDevice RenewDeviceEnumeration( /// 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 testDeviceType, - FirmwareVersion? minimumFirmwareVersion = null) + 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); } var devicesVersionFiltered = @@ -107,7 +111,7 @@ bool MatchingDeviceSelector( } catch (InvalidOperationException) { - ThrowDeviceNotFoundException($"Target test device not found ({testDeviceType})", devices); + ThrowDeviceNotFoundException($"Target test device not found ({testDeviceType}, Transport: {transport})", devices); } return device; @@ -123,18 +127,12 @@ private static FirmwareVersion MatchVersion( return minimumFirmwareVersion; } - switch (testDeviceType) + return testDeviceType switch { - case StandardTestDevice.Fw3: - return FirmwareVersion.V3_1_0; - case StandardTestDevice.Fw4Fips: - return FirmwareVersion.V4_0_0; - case StandardTestDevice.Fw5: - case StandardTestDevice.Fw5Fips: - case StandardTestDevice.Fw5Bio: - default: - return FirmwareVersion.V5_0_0; - } + StandardTestDevice.Fw3 => FirmwareVersion.V3_1_0, + StandardTestDevice.Fw4Fips => FirmwareVersion.V4_0_0, + _ => FirmwareVersion.V5_0_0, + }; } public static IYubiKeyDevice SelectByMinimumVersion( @@ -153,9 +151,10 @@ public static IYubiKeyDevice SelectByMinimumVersion( ThrowDeviceNotFoundException("No matching YubiKey found", devices); } - return device!; + return device; } + [DoesNotReturn] private static void ThrowDeviceNotFoundException( string errorMessage, IYubiKeyDevice[] devices) From 57caf29c85cea7dc8a986080f5a971afae0b318c Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Tue, 3 Dec 2024 11:37:35 +0100 Subject: [PATCH 33/53] tests: worked on SCP tests added unit tests for ScpApduTransform.cs added unit tests for Scp03StateTests.cs added unit tests for Scp03KeyParametersTests.cs added unit tests for Scp11KeyParametersTests.cs (StaticKeys) added comment (DEK) (ScpApduTransform) added comments in ScpApduTransform.cs removed old unit test class ScpApduTransformTests.cs --- .../YubiKey/Pipelines/ScpApduTransform.cs | 11 +- .../src/Yubico/YubiKey/Scp/SessionKeys.cs | 2 +- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 4 - .../Pipelines/Scp03ApduTransformTests.cs | 135 ------------ .../Pipelines/ScpApduTransformTests.cs | 144 +++++++++++++ .../YubiKey/Scp/Scp03KeyParametersTests.cs | 106 +++++++++ .../Yubico/YubiKey/Scp/Scp03StateTests.cs | 146 +++++++++++++ .../YubiKey/Scp/Scp11KeyParametersTests.cs | 204 ++++++++++++++++++ 8 files changed, 610 insertions(+), 142 deletions(-) delete mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs create mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/ScpApduTransformTests.cs create mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs create mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03StateTests.cs create mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp11KeyParametersTests.cs diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs index 4f9d5da14..e3a583e2e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -92,12 +92,15 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT { if (ShouldNotEncode(commandType)) { + // Invoke the pipeline without encoding the command return _pipeline.Invoke(command, commandType, responseType); } - + + // Encode command var encodedCommand = ScpState.EncodeCommand(command); var response = _pipeline.Invoke(encodedCommand, commandType, responseType); + // Decode response return ScpState.DecodeResponse(response); } @@ -111,10 +114,14 @@ private void InitializeScp03(Scp03KeyParameters keyParams) { // Generate host challenge using var rng = CryptographyProviders.RngCreator(); + byte[] hostChallenge = new byte[8]; rng.GetBytes(hostChallenge); - + + // Create the state object that manages keys, mac chaining, etc. _scpState = Scp03State.CreateScpState(_pipeline, keyParams, hostChallenge); + + // Set the data encryptor for later use _dataEncryptor = _scpState.GetDataEncryptor(); } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs index 772783d46..208df6405 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs @@ -35,7 +35,7 @@ internal class SessionKeys : IDisposable public ReadOnlyMemory RmacKey => _rmacKey; /// - /// Gets the session data encryption key. + /// Gets the session data encryption key (DEK). /// public ReadOnlyMemory? DataEncryptionKey => _dataEncryptionKey; diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index d38f831a4..d4a48db8e 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -568,10 +568,6 @@ private Memory GetSki( private static Scp03KeyParameters ImportScp03Key( SecurityDomainSession session) { - // assumeFalse("SCP03 management not supported over NFC on FIPS capable devices", - // state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); // todo - - var scp03Ref = new KeyReference(0x01, 0x01); var staticKeys = new StaticKeys( GetRandomBytes(16), 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 1283caee5..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Pipelines/Scp03ApduTransformTests.cs +++ /dev/null @@ -1,135 +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 -{ - [Obsolete("TODO Migrate?")] - 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]; - } - } - } - - [Obsolete("Class is replaced by ScpApduTransform.")] - 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/Scp/Scp03KeyParametersTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs new file mode 100644 index 000000000..f3e962312 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs @@ -0,0 +1,106 @@ +// 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.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..d63c5fcd1 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp11KeyParametersTests.cs @@ -0,0 +1,204 @@ +// 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 _sdParams; + private readonly ECParameters _oceParams; + private readonly X509Certificate2[] _certificates; + + public Scp11KeyParametersTests() + { + using var sdKey = ECDsa.Create(_curve); + _sdParams = sdKey.ExportParameters(false); + + using var oceKey = ECDsa.Create(_curve); + _oceParams = oceKey.ExportParameters(true); + + // Create a self-signed cert for testing + var req = new CertificateRequest( + "CN=Test", + oceKey, + HashAlgorithmName.SHA256); + + _certificates = 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(_sdParams); + + // Act + var keyParams = new Scp11KeyParameters(keyRef, pkSdEcka); + + // Assert + Assert.NotNull(keyParams); + Assert.Equal(ScpKeyIds.Scp11B, keyParams.KeyReference.Id); + 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(_sdParams); + var skOceEcka = new ECPrivateKeyParameters(_oceParams); + + // Act + var keyParams = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _certificates); + + // 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); + } + + [Fact] + public void Constructor_Scp11b_WithOptionalParams_ThrowsArgumentException() + { + // Arrange + var keyRef = new KeyReference(ScpKeyIds.Scp11B, 0x01); + var oceKeyRef = new KeyReference(0x01, 0x01); + var pkSdEcka = new ECPublicKeyParameters(_sdParams); + var skOceEcka = new ECPrivateKeyParameters(_oceParams); + + // Act & Assert + Assert.Throws(() => new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _certificates)); + } + + [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(_sdParams); + + // 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(_sdParams); + + // 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(_sdParams); + var skOceEcka = new ECPrivateKeyParameters(_oceParams); + + Scp11KeyParameters keyParams = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _certificates); + + // 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(_sdParams); + var skOceEcka = new ECPrivateKeyParameters(_oceParams); + + var keyParams = new Scp11KeyParameters( + keyRef, + pkSdEcka, + oceKeyRef, + skOceEcka, + _certificates); + + // Act & Assert - Should not throw + keyParams.Dispose(); + keyParams.Dispose(); + } + + public void Dispose() + { + foreach (var cert in _certificates) + { + cert.Dispose(); + } + } + } +} From 20898f96b40d2ee02547a6e78041df3a4235616c Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Tue, 3 Dec 2024 18:00:04 +0100 Subject: [PATCH 34/53] misc, docs: Added some docstrings and made small adjustments (SCP) (SessionKeys) using predetermined key sizes and copying key info (ScpState) reduce nesting and simplify dispose method (Scp03State) add dispose method (Scp11KeyParameters) Add docstring for constructor, using private setters (Scp03KeyParameters) Added ArgumentException docstring explanation --- .../YubiKey/Pipelines/ScpApduTransform.cs | 17 ++++--- .../Yubico/YubiKey/Scp/Helpers/Derivation.cs | 25 ++++------- .../Yubico/YubiKey/Scp/Scp03KeyParameters.cs | 18 +++++--- .../src/Yubico/YubiKey/Scp/Scp03State.cs | 32 +++++++++++--- .../Yubico/YubiKey/Scp/Scp11KeyParameters.cs | 44 ++++++++++--------- .../src/Yubico/YubiKey/Scp/Scp11State.cs | 14 +++--- .../Yubico/YubiKey/Scp/ScpKeyParameters.cs | 4 +- .../src/Yubico/YubiKey/Scp/ScpState.cs | 35 +++++++-------- .../src/Yubico/YubiKey/Scp/SessionKeys.cs | 19 ++++---- 9 files changed, 113 insertions(+), 95 deletions(-) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs index e3a583e2e..d2e734c1a 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/ScpApduTransform.cs @@ -85,7 +85,7 @@ public void Setup() /// /// Encodes the command using the SCP state, sends it to the underlying pipeline, and then decodes the response. /// - /// Note: Some commands should not be encoded. For those, the pipeline is invoked directly. + /// Note: Some commands should not be encoded. For those, the pipeline is invoked directly without encoding. /// /// public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseType) @@ -104,12 +104,6 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT return ScpState.DecodeResponse(response); } - private void InitializeScp11(Scp11KeyParameters keyParameters) - { - _scpState = Scp11State.CreateScpState(_pipeline, keyParameters); - _dataEncryptor = _scpState.GetDataEncryptor(); - } - private void InitializeScp03(Scp03KeyParameters keyParams) { // Generate host challenge @@ -125,6 +119,15 @@ private void InitializeScp03(Scp03KeyParameters keyParams) _dataEncryptor = _scpState.GetDataEncryptor(); } + private void InitializeScp11(Scp11KeyParameters keyParameters) + { + // Create the state object that manages keys, mac chaining, etc. + _scpState = Scp11State.CreateScpState(_pipeline, keyParameters); + + // Set the data encryptor for later use + _dataEncryptor = _scpState.GetDataEncryptor(); + } + [DoesNotReturn] private T ThrowIfUninitialized() => throw new InvalidOperationException($"{nameof(Scp.ScpState)} has not been initialized. The Setup method must be called."); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs index de91d62e9..eea8504b7 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Helpers/Derivation.cs @@ -77,6 +77,7 @@ public static Memory Derive( { throw new SecureChannelException(ExceptionMessages.IncorrectDerivationLength); } + // Validate challenge lengths if (hostChallenge.Length != 8 || cardChallenge.Length != 8) { @@ -91,7 +92,7 @@ public static Memory Derive( // Set output length and counter macInputBuffer[14] = outputLenBits; - macInputBuffer[15] = 1; // Counter is always 1 for our use case + macInputBuffer[15] = 1; // Counter is always 1 for our use case // Copy host and cardchallenges to the end of the input buffer hostChallenge.CopyTo(macInputBuffer.Slice(16, 8)); @@ -99,25 +100,15 @@ public static Memory Derive( // Calculate CMAC using AES-128 Span cmac = stackalloc byte[16]; + using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); cmacObj.CmacInit(kdfKey); cmacObj.CmacUpdate(macInputBuffer); cmacObj.CmacFinal(cmac); - // If output length is 128 bits, return the full CMAC - if (outputLenBits == 128) - { - return cmac.ToArray(); - } - - // For 64-bit output (cryptograms), use only the first 8 bytes - Span smallerResult = stackalloc byte[8]; - cmac[..8].CopyTo(smallerResult); - - // Securely clear the full CMAC from memory - CryptographicOperations.ZeroMemory(cmac); - - return smallerResult.ToArray(); + return outputLenBits == 128 + ? cmac.ToArray() // If output length is 128 bits, return the full CMAC + : cmac[..8].ToArray(); // For 64-bit output (cryptograms), use only the first 8 bytes } /// @@ -133,8 +124,8 @@ public static Memory DeriveCryptogram( byte dataDerivationConstant, ReadOnlySpan key, ReadOnlySpan hostChallenge, - ReadOnlySpan cardChallenge) - => Derive(dataDerivationConstant, 64, key, hostChallenge, cardChallenge); + ReadOnlySpan cardChallenge) => + Derive(dataDerivationConstant, 64, key, hostChallenge, cardChallenge); /// /// Derives session keys from the static keys using host and card challenges. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs index e2d368ded..f9e011971 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Yubico AB +// 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. @@ -35,8 +35,8 @@ public sealed class Scp03KeyParameters : ScpKeyParameters, IDisposable /// A reference to the key. /// The static keys shared with the device when initiating the connection. /// - /// Thrown if is greater than 3, which is an invalid Key ID - /// for SCP03. + /// Thrown if is not between 1 and 3, which are the only valid Key IDs + /// for SCP03 /// public Scp03KeyParameters( KeyReference keyReference, @@ -54,22 +54,26 @@ public Scp03KeyParameters( /// Creates a new instance of , representing the parameters for /// a Secure Channel Protocol 03 (SCP03) key. /// - /// The ID of the key. + /// The ID of the key. Must be between 1 and 3 for SCP03. /// The version number of the key. /// The static keys shared with the device. + /// + /// Thrown if is not between 1 and 3, which are the only valid Key IDs + /// for SCP03 + /// public Scp03KeyParameters( byte keyId, byte keyVersionNumber, StaticKeys staticKeys) : this(new KeyReference(keyId, keyVersionNumber), staticKeys) { } - + /// /// Gets the default SCP03 key parameters using the default key identifier and static keys. /// /// /// This property provides a convenient way to access default SCP03 key parameters, - /// using the standard SCP03 key identifier and default static keys. + /// using the standard SCP03 key identifier (0x03) and default static keys with version number 0xFF. /// public static Scp03KeyParameters DefaultKey => new Scp03KeyParameters(ScpKeyIds.Scp03, DefaultKvn, new StaticKeys()); @@ -79,7 +83,7 @@ public Scp03KeyParameters( /// the given static keys. /// /// The static keys shared with the device. - /// An instance of . + /// An instance of with key ID 0x03 and version number 0x01. public static Scp03KeyParameters FromStaticKeys(StaticKeys staticKeys) => new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs index 74ed9815d..b6ac164fb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03State.cs @@ -30,8 +30,8 @@ namespace Yubico.YubiKey.Scp /// internal class Scp03State : ScpState { - private readonly ReadOnlyMemory _hostCryptogram; - + private readonly Memory _hostCryptogram; + private bool _disposed; /// /// Creates a new SCP03 secure channel state using the provided session keys and host cryptogram. /// @@ -42,7 +42,7 @@ public Scp03State( ReadOnlyMemory hostCryptogram) : base(sessionKeys, new byte[16]) { - _hostCryptogram = hostCryptogram; + _hostCryptogram = hostCryptogram.ToArray(); } /// @@ -64,10 +64,20 @@ internal static Scp03State CreateScpState( throw new ArgumentException("Invalid size, must be 8 bytes", nameof(hostChallenge)); } + if (keyParameters is null) + { + throw new ArgumentNullException(nameof(keyParameters)); + } + + if (pipeline is null) + { + throw new ArgumentNullException(nameof(pipeline)); + } + var (cardChallenge, cardCryptogram) = PerformInitializeUpdate(pipeline, keyParameters, hostChallenge); + var state = CreateScpState(keyParameters, hostChallenge, cardChallenge, cardCryptogram); state.PerformExternalAuthenticate(pipeline); - return state; } @@ -147,7 +157,7 @@ private void PerformExternalAuthenticate(IApduTransform pipeline) private (ExternalAuthenticateCommand, ReadOnlyMemory macChainingValue) GetMacedExternalAuthenticateCommand(ExternalAuthenticateCommand externalAuthenticateCommand) { - (var commandDataMaced, var newMacChainingValue) = GetMacdCommandData(externalAuthenticateCommand.CreateCommandApdu()); + var (commandDataMaced, newMacChainingValue) = GetMacdCommandData(externalAuthenticateCommand.CreateCommandApdu()); return (new ExternalAuthenticateCommand(commandDataMaced), newMacChainingValue); } @@ -161,5 +171,17 @@ private void PerformExternalAuthenticate(IApduTransform pipeline) return (macedApdu.Data, newMacChainingValue); } + + public override void Dispose() + { + if (_disposed) + { + return; + } + + CryptographicOperations.ZeroMemory(_hostCryptogram.Span); + base.Dispose(); + _disposed = true; + } } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs index 986b8787d..774e4ec3e 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11KeyParameters.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -27,12 +26,8 @@ namespace Yubico.YubiKey.Scp /// For SCP11b only keyRef and pkSdEcka are required. Note that this does not authenticate the off-card entity. /// For SCP11a and SCP11c the off-card entity CA key reference must be provided, as well as the off-card entity secret key and certificate chain. /// - [SuppressMessage("Style", "IDE0032:Use auto property")] public sealed class Scp11KeyParameters : ScpKeyParameters, IDisposable { - private KeyReference? _oceKeyReference; - private ECPublicKeyParameters _pkSdEcka; - private ECPrivateKeyParameters? _skOceEcka; private X509Certificate2[]? _oceCertificates; private bool _disposed; @@ -40,19 +35,19 @@ public sealed class Scp11KeyParameters : ScpKeyParameters, IDisposable /// The public key of the Security Domain Elliptic Curve Key Agreement (ECKA) key (SCP11a/b/c). Required. /// 'pkSdEcka' is short for PublicKey SecurityDomain Elliptic Curve KeyAgreement (Key) /// - public ECPublicKeyParameters PkSdEcka => _pkSdEcka; + public ECPublicKeyParameters PkSdEcka { get; private set; } /// /// The key reference of the off-card entity (SCP11a/c). Optional. /// 'oceKeyReference' is short for Off-Card Entity Key Reference /// - public KeyReference? OceKeyReference => _oceKeyReference; + public KeyReference? OceKeyReference { get; private set; } /// /// The private key of the off-card entity Elliptic Curve Key Agreement (ECKA) key (SCP11a/c). Optional. /// 'skOceEcka' is short for Secret Key Off-Card Entity Elliptic Curve KeyAgreement (Key) /// - public ECPrivateKeyParameters? SkOceEcka => _skOceEcka; + public ECPrivateKeyParameters? SkOceEcka { get; private set; } /// /// The certificate chain, containing the public key for the off-card entity (SCP11a/c). @@ -76,20 +71,27 @@ public Scp11KeyParameters( IEnumerable? certificates = null) : base(keyReference) { - _pkSdEcka = pkSdEcka; - _oceKeyReference = oceKeyReference; - _skOceEcka = skOceEcka; + PkSdEcka = pkSdEcka; + OceKeyReference = oceKeyReference; + SkOceEcka = skOceEcka; _oceCertificates = certificates?.ToArray(); ValidateParameters(); } + /// + /// Creates a new instance for SCP11b. + /// + /// The key reference. + /// The security domain elliptic curve key agreement key public key. public Scp11KeyParameters( KeyReference keyReference, ECPublicKeyParameters pkSdEcka) - : this(keyReference, pkSdEcka, null, null, null) + : base(keyReference) { + PkSdEcka = pkSdEcka; + ValidateParameters(); } @@ -104,7 +106,7 @@ private void ValidateParameters() OceCertificates?.Count > 0 ) { - throw new ArgumentException("Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); + throw new ArgumentException($"Cannot provide {nameof(OceKeyReference)}, {nameof(SkOceEcka)} or {nameof(OceCertificates)} for SCP11b"); } break; @@ -116,7 +118,7 @@ private void ValidateParameters() OceCertificates?.Count == 0 ) { - throw new ArgumentException("Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); + throw new ArgumentException($"Must provide {nameof(OceKeyReference)}, {nameof(SkOceEcka)} or {nameof(OceCertificates)} for SCP11a/c"); } break; @@ -125,6 +127,9 @@ private void ValidateParameters() } } + /// + /// This will clear all references and empty + /// public void Dispose() { if (_disposed) @@ -132,12 +137,11 @@ public void Dispose() return; } - CryptographicOperations.ZeroMemory(_skOceEcka?.Parameters.D); - _pkSdEcka = null!; - _oceKeyReference = null; - _skOceEcka = null; - _oceCertificates = Array.Empty(); - + CryptographicOperations.ZeroMemory(SkOceEcka?.Parameters.D); + PkSdEcka = null!; + OceKeyReference = null; + SkOceEcka = null; + _oceCertificates = null; _disposed = true; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index d8785099a..d66956328 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -73,10 +73,10 @@ internal static Scp11State CreateScpState( .GenerateKeyPair(sdEckaPkParams.Curve); // Create an encoded point of the ephemeral public key to send to the Yubikey - byte[] ephemeralPublicKeyEncodedPointOceEcka = new byte[65]; - ephemeralPublicKeyEncodedPointOceEcka[0] = 0x04; // Coding Identifier Byte - ekpOceEcka.Q.X.CopyTo(ephemeralPublicKeyEncodedPointOceEcka, 1); - ekpOceEcka.Q.Y.CopyTo(ephemeralPublicKeyEncodedPointOceEcka, 33); + byte[] epkOceEckaEncodedPoint = new byte[65]; + epkOceEckaEncodedPoint[0] = 0x04; // Format identifier byte + ekpOceEcka.Q.X.CopyTo(epkOceEckaEncodedPoint, 1); + ekpOceEcka.Q.Y.CopyTo(epkOceEckaEncodedPoint, 33); // GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 byte[] keyUsage = { 0x3C }; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION @@ -94,7 +94,7 @@ internal static Scp11State CreateScpState( new TlvObject(0x80, keyType), new TlvObject(0x81, keyLen) )), - new TlvObject(EckaTag, ephemeralPublicKeyEncodedPointOceEcka) + new TlvObject(EckaTag, epkOceEckaEncodedPoint) ); // Construct the host authentication command @@ -131,7 +131,7 @@ internal static Scp11State CreateScpState( // Perform key agreement var (encryptionKey, macKey, rMacKey, dekKey) - = GetX963KDFKeyAgreementKeys( + = GetX963KdfKeyAgreementKeys( skOceEcka.Curve, sdEckaPkParams, ekpOceEcka, @@ -171,7 +171,7 @@ internal static Scp11State CreateScpState( /// Thrown when the curves of the provided keys do not match. /// Thrown when key agreement receipt verification fails. private static (Memory encryptionKey, Memory macKey, Memory rMacKey, Memory dekKey) - GetX963KDFKeyAgreementKeys( + GetX963KdfKeyAgreementKeys( ECCurve curve, // The curve being used for the key agreement ECParameters pkSdEcka, // Yubikey Public Key ECParameters eskOceEcka, // Host Ephemeral Private Key diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs index c1372fb59..ac9406651 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpKeyParameters.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; - namespace Yubico.YubiKey.Scp { /// @@ -24,7 +22,7 @@ public abstract class ScpKeyParameters /// /// The key reference associated with the key parameters. /// - public KeyReference KeyReference { get; protected set; } + public KeyReference KeyReference { get; } /// /// Creates a new instance of . diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs index b4d9420a1..0f26700ae 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/ScpState.cs @@ -122,11 +122,11 @@ public ResponseApdu DecodeResponse(ResponseApdu response) // Create a new array to hold the decrypted data and status words byte[] fullDecryptedResponse = new byte[decryptedData.Length + 2]; decryptedData.CopyTo(fullDecryptedResponse); - + // Append the status words to the decrypted data fullDecryptedResponse[decryptedData.Length] = response.SW1; fullDecryptedResponse[decryptedData.Length + 1] = response.SW2; - + // Return a new ResponseApdu with the decrypted data and status words return new ResponseApdu(fullDecryptedResponse); } @@ -164,32 +164,27 @@ public EncryptDataFunc GetDataEncryptor() protected static (CommandApdu macdApdu, ReadOnlyMemory newMacChainingValue) MacApdu( CommandApdu commandApdu, ReadOnlySpan macKey, - ReadOnlySpan macChainingValue) - => ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); + ReadOnlySpan macChainingValue) => + ChannelMac.MacApdu(commandApdu, macKey, macChainingValue); private static void VerifyRmac( ReadOnlySpan responseData, ReadOnlySpan rmacKey, - ReadOnlySpan macChainingValue) - => ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + ReadOnlySpan macChainingValue) => + ChannelMac.VerifyRmac(responseData, rmacKey, macChainingValue); - protected virtual void Dispose(bool disposing) + public virtual void Dispose() { - if (!_disposed) + if (_disposed) { - if (disposing) - { - SessionKeys?.Dispose(); - - _disposed = true; - } + return; } + + SessionKeys.Dispose(); + MacChainingValue = Array.Empty(); + + GC.SuppressFinalize(this); + _disposed = true; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs index 208df6405..e19c3c503 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SessionKeys.cs @@ -39,10 +39,11 @@ internal class SessionKeys : IDisposable /// public ReadOnlyMemory? DataEncryptionKey => _dataEncryptionKey; - private readonly Memory _macKey; - private readonly Memory _encryptionKey; - private readonly Memory _rmacKey; - private readonly Memory _dataEncryptionKey; + private const byte KeySizeBytes = 16; + private readonly Memory _macKey = new byte[KeySizeBytes]; + private readonly Memory _encryptionKey = new byte[KeySizeBytes]; + private readonly Memory _rmacKey = new byte[KeySizeBytes]; + private readonly Memory _dataEncryptionKey = new byte[KeySizeBytes]; private bool _disposed; /// @@ -63,15 +64,15 @@ public SessionKeys( ValidateKeyLength(rmacKey, nameof(rmacKey)); ValidateKeyLength(dataEncryptionKey, nameof(dataEncryptionKey)); - _macKey = macKey.ToArray(); - _encryptionKey = encryptionKey.ToArray(); - _rmacKey = rmacKey.ToArray(); - _dataEncryptionKey = dataEncryptionKey.ToArray(); + macKey.CopyTo(_macKey); + encryptionKey.CopyTo(_encryptionKey); + rmacKey.CopyTo(_rmacKey); + dataEncryptionKey.CopyTo(_dataEncryptionKey); } private static void ValidateKeyLength(ReadOnlyMemory key, string paramName) { - if (key.Length != 16) + if (key.Length != KeySizeBytes) { throw new ArgumentException("Incorrect session key length. Must be 16.", paramName); } From de0c9bc7fed50084720329dfe92cabcb6ef5fae0 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Wed, 4 Dec 2024 11:35:46 +0100 Subject: [PATCH 35/53] docs: Improved documentation on SCP11 (SecurityDomainSession.cs) added docs distinguishing between SCP03 and SCP11 (PutKeyCommand.cs) added docs distinguishing between SCP03 and SCP11 (multiple files) Renamed documentation uid and documentation file --- Yubico.NET.SDK.sln | 2 +- .../users-manual/getting-started/whats-new.md | 2 +- ...otocol-3.md => secure-channel-protocol.md} | 2 +- Yubico.YubiKey/docs/users-manual/toc.yml | 4 +- .../src/Yubico/YubiKey/Piv/PivSession.cs | 2 +- .../YubiKey/Scp/Commands/DeleteKeyCommand.cs | 2 +- .../YubiKey/Scp/Commands/PutKeyCommand.cs | 113 ++++++------------ .../YubiKey/Scp/SecurityDomainSession.cs | 111 +++++++++-------- .../src/Yubico/YubiKey/Scp/StaticKeys.cs | 2 +- .../Scp03/Commands/DeleteKeyCommand.cs | 2 +- .../YubiKey/Scp03/Commands/PutKeyCommand.cs | 2 +- .../src/Yubico/YubiKey/Scp03/Scp03Session.cs | 6 +- .../src/Yubico/YubiKey/Scp03/StaticKeys.cs | 2 +- 13 files changed, 113 insertions(+), 139 deletions(-) rename Yubico.YubiKey/docs/users-manual/sdk-programming-guide/{secure-channel-protocol-3.md => secure-channel-protocol.md} (99%) 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/users-manual/getting-started/whats-new.md b/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md index 3f34f8304..db0263d2d 100644 --- a/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md +++ b/Yubico.YubiKey/docs/users-manual/getting-started/whats-new.md @@ -366,7 +366,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.md similarity index 99% rename from Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol-3.md rename to Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol.md index 91ab0880f..e9634457b 100644 --- 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.md @@ -1,5 +1,5 @@ --- -uid: UsersManualScp03 +uid: UsersManualScp --- -# 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 responses 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. +# Secure Channel Protocol (SCP) -## Static Keys +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). -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. +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: -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. +- **SCP03** - A symmetric key protocol using AES-128 for encryption and authentication +- **SCP11** - An asymmetric protocol using elliptic curve cryptography (ECC) and X509-certificates -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. +## Protocol Overview -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. +### 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. -### Three key sets +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+ -A YubiKey can contain up to three SCP03 key sets. Think of the YubiKey as having three -slots for SCP03 keys. +### SCP11 (Asymmetric) +SCP11[^1] 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: -```txt - slot 1: ENC MAC DEK - slot 2: ENC MAC DEK - slot 3: ENC MAC DEK -``` +- **SCP11a** - Mutual authentication between YubiKey and host +- **SCP11b** - YubiKey authenticates to host only +- **SCP11c** - Enhanced mutual authentication with additional features -Each key is 16 bytes. YubiKeys do not support any other key size. +Key characteristics: +- Uses NIST P-256 elliptic curve +- Certificate-based authentication +- Supported on YubiKey 5 Series with firmware 5.7.2+ -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`. +## When to Use Secure Channels -```txt - slot 1: ENC(default) MAC(default) DEK(default) - slot 2: --empty-- - slot 3: --empty-- -``` +Secure channels are particularly valuable when: -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. +- Communicating with YubiKeys over networks or untrusted channels (e.g. NFC) +- Managing YubiKeys remotely through card management systems +- Protecting sensitive operations in multi-tenant environments +- Ensuring end-to-end security beyond transport encryption -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. +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. -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. +## Security Considerations -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. +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. -This is the initial state of the standard YubiKey. +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. -```txt - slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) - slot 2: --empty-- - slot 3: --empty-- -``` + 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). -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: +The following sections detail how to implement both protocols, manage keys and certificates, and integrate secure channels with various YubiKey applications. -```txt - slot 1: KVN=1 ENC(new) MAC(new) DEK(new) - slot 2: --empty-- - slot 3: --empty-- -``` +## Using Secure Channels with YubiKey Applications -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. +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. -```txt - slot 1: --empty-- - slot 2: KVN=2 ENC(new) MAC(new) DEK(new) - slot 3: --empty-- +### 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 = new KeyReference(keyId, keyVersionNumber); +var certificates = sdSession.GetCertificates(keyReference); // Get from YubiKey +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 +} ``` -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. +### Application Examples -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: +#### PIV with Secure Channel -```txt - slot 1: --empty-- - slot 2: KVN=2 ENC MAC DEK - slot 3: KVN=3 ENC(new) MAC(new) DEK(new) +```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 SCP11 +} + +// Using SCP11b +var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +using (var pivSession = new PivSession(yubiKeyDevice, scp11Params)) +{ + // All PivSession-commands are now automatically protected by SCP11 +} ``` -It is also possible to replace a key set, that is described [below](#replacing-a-key-set). +#### OATH with Secure Channel +```csharp -#### Slot number and KVN +// 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 SCP11 +} + +// Using SCP11b +var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +using (var oathSession = new OathSession(yubiKeyDevice, scp11Params)) +{ + // All OathSession-commands are now automatically protected by SCP11 +} +``` -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. +#### OTP with Secure Channel +```csharp -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. +// 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 SCP11 +} + +// Using SCP11b +var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +using (var otpSession = new OtpSession(yubiKeyDevice, scp11Params)) +{ + // All OtpSession-commands are now automatically protected by SCP11 +} +``` -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. +#### 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 = new KeyReference(ScpKeyIds.Scp11B, kvn); +using (var yubiHsmSession = new YubiHsmSession(yubiKeyDevice, scp11Params)) +{ + // All yubiHsmSession-commands are now automatically protected by SCP11 +} -### 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. +### Direct Connection + +If you need lower-level control, you can establish secure connections directly using [`Connect`](xref:Yubico.YubiKey.IYubiKeyDevice.Connect): ```csharp - using var scp03Keys = new StaticKeys(keyDataMac, keyDataEnc, keyDataDek) +// 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) { - KeyVersionNumber = 2 + // Use connection } +} ``` -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. +### Security Domain Management -When you supply a `StaticKeys` object to the SDK (version 1.9 and later), the object is -cloned (a deep copy is made). +The [`SecurityDomainSession`](xref:Yubico.YubiKey.Scp.SecurityDomainSession) class provides methods to manage SCP configurations: -### Managing the keys +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); -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. +// Get information about installed keys +var keyInfo = session.GetKeyInformation(); -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. +// Store certificates for SCP11 +session.StoreCertificates(keyReference, certificates); -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. +// Manage allowed certificate serials +session.StoreAllowlist(keyReference, allowedSerials); -## Using SCP03 +// Import private key +session.PutKey(keyReference, privateKeyParameters, 0) -There are two categories of SCP03 operations: +// Import public key +session.PutKey(keyReference, publicKeyParameters, 0) -* Using SCP03 to secure Smart Card communications -* Performing SCP03 operations, such as changing or adding a key set +// Reset to factory defaults +session.Reset(); +``` -### Securing Smart Card communications +> [!NOTE] +> Using `DefaultKey` in production code provides no security. Always use proper key management in production environments. -Suppose you are performing PIV operations. You would normally get a YubiKey and then -instantiate the `PivSession` class. +The next sections will detail specific key management and protocol details for both SCP03 and SCP11. -```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } - using (var pivSession = new PivSession(yubiKeyDevice)) - { - . . . - } -``` +## SCP03 (Symmetric Key Protocol) + +### Static Keys Structure -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. +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 - 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)) - { - . . . - } +var staticKeys = new StaticKeys(keyDataMac, keyDataEnc, keyDataDek); +var scp03Params = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); ``` -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. +### Key Sets on the YubiKey -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". +A YubiKey can contain up to three SCP03 key sets. Each set is identified by a Key Version Number (KVN): -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): +```txt + slot 1: ENC MAC DEK (KVN=1) + slot 2: ENC MAC DEK (KVN=2) + slot 3: ENC MAC DEK (KVN=3) +``` -```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } +Standard YubiKeys are manufactured with a default key set (KVN=0xFF): - using IYubiKeyConnection connection = yubiKeyDevice.ConnectScp03(YubiKeyApplication.Piv, scp03Keys); +```txt + slot 1: ENC(default) MAC(default) DEK(default) + slot 2: --empty-- + slot 3: --empty-- ``` -### Performing SCP03 operations +The default keys are publicly known (0x40 41 42 ... 4F) and provide no security. You should replace them in production environments. + +### Managing Key Sets -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. +Use `SecurityDomainSession` to manage SCP03 key sets: ```csharp - if (!YubiKeyDevice.TryGetYubiKey(serialNumber, out IYubiKeyDevice yubiKeyDevice)) - { - // error, can't find YubiKey - } - using StaticKeys scp03Keys = RetrieveScp03KeySet(); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - . . . - } +// 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, 0); + +// Add another key set +using var session = new SecurityDomainSession(yubiKeyDevice, existingKeyParams); +var keyRef = new KeyReference(ScpKeyIds.Scp03, 0x02); // KVN=2 +session.PutKey(keyRef, additionalKeys, 0); + +// Delete a key set +session.DeleteKey(keyRef, false); + +// Reset to factory defaults (restore default keys) +session.Reset(); ``` -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. +### Key Set Rules -The SDK has methods that perform these SCP03 operations: +1. **Key Version Numbers (KVN):** + - Default key set: KVN=0xFF + - Custom key sets: KVN must be 1, 2, or 3 + - YubiKey enforces these specific values -* [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) +2. **Default Key Replacement:** + - When adding first custom key set, default keys are always removed + - Happens regardless of which KVN (1,2,3) is used + - Cannot retain default keys alongside custom keys -#### Replacing the default key set +3. **Multiple Key Sets:** + - After default keys are replaced, can have 1-3 custom key sets + - Each must have unique KVN + - Can add/remove without affecting other sets -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). +4. **Key Replacement:** + - Must authenticate with the key set being replaced + - Cannot replace KVN=1 while authenticated with KVN=2 + +### Example: Complete Key Management Flow ```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); - } +// Start with default keys +var defaultScp03Params = Scp03KeyParameters.DefaultKey; +using (var session = new SecurityDomainSession(yubiKeyDevice, defaultScp03Params)) +{ + // Add first custom key set (removes default) + var keyRef1 = new KeyReference(ScpKeyIds.Scp03, 0x01); + session.PutKey(keyRef1, newKeys, 0); +} + +// 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 = new KeyReference(ScpKeyIds.Scp03, 0x02); + session.PutKey(keyRef2, customKeys2, 0); + + // Check current key information + var keyInfo = session.GetKeyInformation(); +} ``` -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. +### Key Management Responsibilities -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. +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 -#### Adding a new key set +The YubiKey provides no metadata about installed keys beyond what's available through `GetKeyInformation()`. -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. +> [!NOTE] +> Always use proper key management in production. Never store sensitive keys in source code or configuration files. -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: --empty-- - slot 3: --empty-- -``` +## SCP11 (Asymmetric Key Protocol) -To add a new key set with KVN=2, do the following: +SCP11[^1] 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 -```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(); - } -``` +[^1]: 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/) -```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 +It comes in three variants, each offering different security properties: -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. +### SCP11 Variants -```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); - } -``` +- **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 -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. +- **SCP11b**: YubiKey authenticates to host only + - Simplest variant, no mutual authentication + - Uses only ephemeral key pairs + - No certificate chain required + - Suitable when host authentication isn't required -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: KVN=2 ENC MAC DEK - slot 3: --empty-- -``` +- **SCP11c**: Enhanced mutual authentication with additional features + - Most secure variant with additional capabilities + - 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 + - Includes transaction mechanism with rollback capability + - Supports authorization rules in OCE certificates -```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); - } -``` +### Key Benefits of SCP11 over SCP03 -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. +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 all AES variants (128/192/256) +- Better suited for complex deployment scenarios +- Can operate through intermediary applications securely -#### Removing a key set +### Key Parameters -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. +Unlike SCP03's static keys, SCP11 uses `Scp11KeyParameters` which can contain: +- Public/private key pairs +- Certificates +- Key references +- Off-card entity (OCE) information ```csharp - using StaticKeys scp03Keys = GetStaticKeys(1); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - scp03Session.DeleteKeySet(2, false); - } +// SCP11b basic parameters +var keyReference = new KeyReference(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 ``` -Notice that this sample code created the `Scp03Session` using KVN=1, but deleted the key -set with KVN=2. +### Key Management -#### Removing all key sets +Use `SecurityDomainSession` to manage SCP11 keys and certificates: -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). +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); -For example, suppose you have three SCP03 key sets on the YubiKey. +// Generate new EC key pair +var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x3); +var publicKey = session.GenerateEcKey(keyReference, 0); -```txt - slot 1: KVN=1 ENC MAC DEK - slot 2: KVN=2 ENC MAC DEK - slot 3: KVN=3 ENC MAC DEK +// Import existing key pair +var privateKey = new ECPrivateKeyParameters(ecdsa); +session.PutKey(keyReference, privateKey, 0); + +// Store certificates +session.StoreCertificates(keyReference, certificates); + +// Manage certificate serial number allowlist +var serials = new List { + "7F4971B0AD51F84C9DA9928B2D5FEF5E16B2920A", // Examples + "6B90028800909F9FFCD641346933242748FBE9AD" +}; +session.StoreAllowlist(oceKeyReference, serials); ``` -Use the KVN=3 key set to delete key sets 1 and 2. +### SCP11b Example + +Simplest variant, where YubiKey authenticates to host: ```csharp - using StaticKeys scp03Keys = GetStaticKeys(3); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - scp03Session.DeleteKeySet(1, false); - scp03Session.DeleteKeySet(2, false); - } +// Get certificates stored on YubiKey +var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); +IReadOnlyCollection certificateList; +using (var session = new SecurityDomainSession(yubiKeyDevice)) +{ + certificateList = session.GetCertificates(keyReference); +} + +// Create parameters using leaf certificate +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); ``` -```txt - slot 1: --empty-- - slot 2: --empty-- - slot 3: KVN=3 ENC MAC DEK -``` +### SCP11a Example -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. +Full mutual authentication requires more setup: ```csharp - using StaticKeys scp03Keys = GetStaticKeys(3); - using (var scp03Session = new Scp03Session(yubiKeyDevice, scp03Keys)) - { - scp03Session.DeleteKeySet(3, true); - } +// Start with default SCP03 connection +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); + +const byte kvn = 0x03; +var keyRef = new KeyReference(ScpKeyIds.Scp11A, kvn); + +// Generate new key pair on YubiKey +var newPublicKey = session.GenerateEcKey(keyRef, 0); + +// Setup off-card entity (OCE) +var oceRef = new KeyReference(OceKid, kvn); +var ocePublicKey = new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()!); +session.PutKey(oceRef, ocePublicKey, 0); + +// 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); ``` -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: +### Security Considerations -```txt - slot 1: KVN=0xff KeyId=1:ENC(default) KeyId=2:MAC(default) KeyId=3:DEK(default) - slot 2: --empty-- - slot 3: --empty-- +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: Simpler but less secure + - SCP11a: Better security through mutual auth + - SCP11c: Enhanced features but more complex + +4. **Certificate Allowlists:** + - Restrict which certificates can authenticate + - Update lists as certificates change + - Clear allowlists when no longer needed + +### 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 From 129e3837b978197789757bc1722ae8a50181b1f3 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Mon, 16 Dec 2024 12:38:52 +0100 Subject: [PATCH 46/53] misc: renamed some vars and classes to fit GPC specification --- ....cs => PerformSecurityOperationCommand.cs} | 6 ++-- .../YubiKey/Scp/Commands/ResetCommand.cs | 2 +- .../src/Yubico/YubiKey/Scp/Scp11State.cs | 35 +++++++++++-------- .../YubiKey/Scp/SecurityDomainSession.cs | 2 +- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 6 ++-- 5 files changed, 28 insertions(+), 23 deletions(-) rename Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/{SecurityOperationCommand.cs => PerformSecurityOperationCommand.cs} (90%) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PerformSecurityOperationCommand.cs similarity index 90% rename from Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs rename to Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PerformSecurityOperationCommand.cs index 7f70d88fb..005514fcb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/SecurityOperationCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/PerformSecurityOperationCommand.cs @@ -28,7 +28,7 @@ namespace Yubico.YubiKey.Scp.Commands /// It is typically used in conjunction with other SCP commands like Initialize Update /// and External/Internal Authenticate to establish a secure channel. /// - internal class SecurityOperationCommand : IYubiKeyCommand + internal class PerformSecurityOperationCommand : IYubiKeyCommand { public YubiKeyApplication Application => YubiKeyApplication.SecurityDomain; internal const byte GpPerformSecurityOperationIns = 0x2A; @@ -37,7 +37,7 @@ internal class SecurityOperationCommand : IYubiKeyCommand - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Off-Card Entity key version number. /// The Off-Card Entity key ID. @@ -45,7 +45,7 @@ internal class SecurityOperationCommand : IYubiKeyCommand /// This command is used as part of the SCP11 protocol suite for presenting the off-card entity's certificate chain to the YubiKey. /// - public SecurityOperationCommand(byte oceKeyVersionNumer, byte oceKeyId, ReadOnlyMemory oceCertificates) + public PerformSecurityOperationCommand(byte oceKeyVersionNumer, byte oceKeyId, ReadOnlyMemory oceCertificates) { _oceRefVersionNumber = oceKeyVersionNumer; _oceKeyId = oceKeyId; diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs index 273ab4878..3e3e6c805 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Commands/ResetCommand.cs @@ -40,7 +40,7 @@ internal class ResetCommand : IYubiKeyCommand /// , /// , /// , - /// + /// /// /// The version number of the key /// The Key id diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs index 47660f6a9..330f514d9 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp11State.cs @@ -79,20 +79,20 @@ internal static Scp11State CreateScpState( ekpOceEcka.Q.Y.CopyTo(epkOceEckaEncodedPoint, 33); // GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 - byte[] keyUsage = { 0x3C }; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION + byte[] scpIdentifier = { 0x11, GetScpParameterByte(keyParameters.KeyReference) }; + byte[] keyUsageQualifier = { 0x3C }; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION byte[] keyType = { 0x88 }; // AES - byte[] keyLen = { 16 }; // 128-bit - byte[] keyIdentifier = { 0x11, GetScpIdentifierByte(keyParameters.KeyReference) }; + byte[] keyLength = { 16 }; // 128-bit // Construct the host authentication data (payload) byte[] hostAuthenticateTlvEncodedData = TlvObjects.EncodeMany( new TlvObject( KeyAgreementTag, TlvObjects.EncodeMany( - new TlvObject(0x90, keyIdentifier), - new TlvObject(0x95, keyUsage), + new TlvObject(0x90, scpIdentifier), + new TlvObject(0x95, keyUsageQualifier), new TlvObject(0x80, keyType), - new TlvObject(0x81, keyLen) + new TlvObject(0x81, keyLength) )), new TlvObject(EckaTag, epkOceEckaEncodedPoint) ); @@ -105,7 +105,7 @@ internal static Scp11State CreateScpState( : new ExternalAuthenticateCommand( keyParameters.KeyReference.VersionNumber, keyParameters.KeyReference.Id, hostAuthenticateTlvEncodedData) as IYubiKeyCommand; - + // Issue the host authentication command var authenticateResponseApdu = pipeline.Invoke( authenticateCommand.CreateCommandApdu(), authenticateCommand.GetType(), typeof(ScpResponse)); @@ -139,10 +139,10 @@ internal static Scp11State CreateScpState( sdReceipt, epkSdEckaTlvEncodedData, hostAuthenticateTlvEncodedData, - keyUsage, + keyUsageQualifier, keyType, - keyLen); - + keyLength); + // Create the session keys var sessionKeys = new SessionKeys( macKey, @@ -239,8 +239,8 @@ private static (Memory encryptionKey, Memory macKey, Memory rM // Do AES CMAC using var cmacObj = CryptographyProviders.CmacPrimitivesCreator(CmacBlockCipherAlgorithm.Aes128); - - Span oceReceipt = stackalloc byte[16]; + + Span oceReceipt = stackalloc byte[16]; cmacObj.CmacInit(receiptVerificationKey); cmacObj.CmacUpdate(keyAgreementData); cmacObj.CmacFinal(oceReceipt); // Our generated receipt @@ -283,7 +283,7 @@ private static ECParameters ExtractPublicKeyEcParameters(ReadOnlyMemory ep /// The key reference to get the identifier for. /// The SCP identifier byte. /// Thrown when the key reference ID is not a valid SCP11 KID. - private static byte GetScpIdentifierByte(KeyReference keyReference) => + private static byte GetScpParameterByte(KeyReference keyReference) => keyReference.Id switch { ScpKeyIds.Scp11A => 0b01, @@ -295,6 +295,11 @@ private static byte GetScpIdentifierByte(KeyReference keyReference) => /// /// Performs the Security Operation command sequence required for SCP11a and SCP11c authentication. /// + /// + /// This command is used to submit CERT.OCE.ECKA, or possibly a chain of certificates ending with + /// CERT.OCE.ECKA. This is required as a precondition to the initiation of a SCP11a or SCP11c + /// secure channel. + /// /// The APDU pipeline for communication with the YubiKey. /// The key parameters containing certificates and references. /// Thrown when required key parameters are missing. @@ -326,7 +331,7 @@ private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyPa ? 0x80 : 0x00)); // Append 0x80 if more certificates remain to be sent - var securityOperationCommand = new SecurityOperationCommand( + var securityOperationCommand = new PerformSecurityOperationCommand( oceRef.VersionNumber, oceRefInput, certificates); @@ -334,7 +339,7 @@ private static void PerformSecurityOperation(IApduTransform pipeline, Scp11KeyPa // Send payload var responseSecurityOperation = pipeline.Invoke( securityOperationCommand.CreateCommandApdu(), - typeof(SecurityOperationCommand), + typeof(PerformSecurityOperationCommand), typeof(SecurityOperationResponse)); if (responseSecurityOperation.SW != SWConstants.Success) diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 0465b4c47..8f6fe082d 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -870,7 +870,7 @@ public void Reset() ins = InternalAuthenticateCommand.GpInternalAuthenticateIns; break; default: // 0x10, 0x20-0x2F - ins = SecurityOperationCommand.GpPerformSecurityOperationIns; + ins = PerformSecurityOperationCommand.GpPerformSecurityOperationIns; break; } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 0b000d84d..021f2c533 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -133,7 +133,7 @@ public void Scp11b_App_OtpSession_Operations_Succeeds( [SkippableTheory(typeof(DeviceNotFoundException))] [InlineData(StandardTestDevice.Fw5)] [InlineData(StandardTestDevice.Fw5Fips)] - public void Scp11b_YubiHsmSession_Operations_Succeeds( + public void Scp11b_App_YubiHsmSession_Operations_Succeeds( StandardTestDevice desiredDeviceType) { var testDevice = YhaTestUtilities.GetCleanDevice(desiredDeviceType); @@ -175,7 +175,7 @@ public void Scp11b_EstablishSecureConnection_Succeeds( } var leaf = certificateList.Last(); - var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!; var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); using (var session = new SecurityDomainSession(testDevice, keyParams)) @@ -640,7 +640,7 @@ private static Scp11KeyParameters Get_Scp11b_SecureConnection_Parameters( } var leaf = certificateList.Last(); - var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!.ExportParameters(false); + var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!; var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); return keyParams; From ead1811160fd758709a14b0701676ab5c7ed61ef Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Mon, 16 Dec 2024 18:52:57 +0100 Subject: [PATCH 47/53] misc: minor SCP work add validation and tests on keyId and keyVersionNumber (KeyReference) made replaceKvn into an optional parameter --- .../src/Yubico/YubiKey/Scp/KeyReference.cs | 15 +++++++- .../Yubico/YubiKey/Scp/Scp03KeyParameters.cs | 3 +- .../YubiKey/Scp/SecurityDomainSession.cs | 12 +++--- .../Yubico/YubiKey/Scp/Scp11Tests.cs | 1 + .../Yubico/YubiKey/Scp/KeyReferenceTests.cs | 37 +++++++++++++++++++ .../YubiKey/Scp/Scp03KeyParametersTests.cs | 1 - 6 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs index db9a6b48c..2bddfe8e5 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs @@ -35,13 +35,24 @@ public class KeyReference /// /// Initializes a new instance of the class. /// - /// The ID of the key (KID). - /// The version number of the key (KVN). + /// The ID of the key (KID). Accepted values depend on the usage. See + /// for a list of possible Key Id's. + /// The version number of the key (KVN). Must be between 1 and 127. /// See the GlobalPlatform Technology Card Specification v2.3 Amendment F §5.1 Cryptographic Keys for more information on the available KIDs. public KeyReference( byte id, byte versionNumber) { + if (versionNumber > 127) + { + throw new ArgumentException(nameof(versionNumber), "Key version number (KVN) must be between 1 and 127"); + } + + if (id > 127) + { + throw new ArgumentException(nameof(id), "Key ID (KID) must be between 0 and 127"); + } + Id = id; VersionNumber = versionNumber; } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs index 1f51a4107..9f9f3d03f 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/Scp03KeyParameters.cs @@ -22,6 +22,7 @@ namespace Yubico.YubiKey.Scp public sealed class Scp03KeyParameters : ScpKeyParameters, IDisposable { private const int DefaultKvn = 0xFF; + private const int DefaultKid = 0x01; /// /// The static keys shared with the device when initiating the connection. @@ -85,7 +86,7 @@ public Scp03KeyParameters( /// The static keys shared with the device. /// An instance of with key ID 0x03 and version number 0x01. public static Scp03KeyParameters FromStaticKeys(StaticKeys staticKeys) => - new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); + new Scp03KeyParameters(ScpKeyIds.Scp03, DefaultKid, staticKeys); /// /// This will clear all references and sensitive buffers diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs index 8f6fe082d..bcbac53f2 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/SecurityDomainSession.cs @@ -206,11 +206,11 @@ public SecurityDomainSession(IYubiKeyDevice yubiKey, ScpKeyParameters scpKeyPara /// /// The key reference identifying where to store the key. /// The new SCP03 key set to store. - /// The key version number to replace, or 0 for a new key. + /// The key version number to replace, or 0 for a new key (Default value is 0). /// Thrown when the KID is not 0x01 for SCP03 key sets. /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. - public void PutKey(KeyReference keyReference, StaticKeys staticKeys, int replaceKvn) + public void PutKey(KeyReference keyReference, StaticKeys staticKeys, int replaceKvn = 0) { Logger.LogInformation("Importing SCP03 key set into KeyReference {KeyReference}", keyReference); @@ -281,12 +281,12 @@ public void PutKey(KeyReference keyReference, StaticKeys staticKeys, int replace /// /// The key reference identifying where to store the key. /// The EC private key parameters to store. - /// The key version number to replace, or 0 for a new key. + /// The key version number to replace, or 0 for a new key (Default value is 0). /// Thrown when the private key is not of type NIST P-256. /// Thrown when no secure session is established. /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. - public void PutKey(KeyReference keyReference, ECPrivateKeyParameters privateKeyParameters, int replaceKvn) + public void PutKey(KeyReference keyReference, ECPrivateKeyParameters privateKeyParameters, int replaceKvn = 0) { Logger.LogInformation("Importing SCP11 private key into Key Reference: {KeyReference}", keyReference); @@ -348,12 +348,12 @@ public void PutKey(KeyReference keyReference, ECPrivateKeyParameters privateKeyP /// /// The key reference identifying where to store the key. /// The EC public key parameters to store. - /// The key version number to replace, or 0 for a new key. + /// The key version number to replace, or 0 for a new key (Default value is 0). /// Thrown when the public key is not of type SECP256R1. /// Thrown when no secure session is established. /// Thrown when the new key set's checksum failed to verify, or some other SCP related error /// described in the exception message. - public void PutKey(KeyReference keyReference, ECPublicKeyParameters publicKeyParameters, int replaceKvn) + public void PutKey(KeyReference keyReference, ECPublicKeyParameters publicKeyParameters, int replaceKvn = 0) { Logger.LogInformation("Importing SCP11 public key into KeyReference: {KeyReference}", keyReference); diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs index 021f2c533..742e13eeb 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/Scp/Scp11Tests.cs @@ -175,6 +175,7 @@ public void Scp11b_EstablishSecureConnection_Succeeds( } var leaf = certificateList.Last(); + // Remember to verify the cert chain var ecDsaPublicKey = leaf.PublicKey.GetECDsaPublicKey()!; var keyParams = new Scp11KeyParameters(keyReference, new ECPublicKeyParameters(ecDsaPublicKey)); 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..d1a99d4d3 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs @@ -0,0 +1,37 @@ +// 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 Constructor_InvalidParameters_ThrowsException() + { + Assert.Throws(() => new KeyReference(ScpKeyIds.Scp11B, 128)); + } + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs index f3e962312..a10d68d91 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/Scp03KeyParametersTests.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Linq; using Xunit; From 485b6268616f93473215d7ab591942a1987a6e71 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Mon, 16 Dec 2024 19:13:55 +0100 Subject: [PATCH 48/53] docs: added documentation for security domain application misc: factory method for creating KeyReferences --- .../security-domain-certificates.md | 213 +++++++++++++ .../security-domain-device.md | 283 ++++++++++++++++++ .../security-domain-keys.md | 172 +++++++++++ .../security-domain-overview.md | 73 +++++ .../security-domain-tasks.md | 267 +++++++++++++++++ .../secure-channel-protocol.md | 99 +++--- Yubico.YubiKey/docs/users-manual/toc.yml | 11 + .../src/Yubico/YubiKey/Scp/KeyReference.cs | 15 +- .../Yubico/YubiKey/Scp/KeyReferenceTests.cs | 28 +- 9 files changed, 1106 insertions(+), 55 deletions(-) create mode 100644 Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-certificates.md create mode 100644 Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-device.md create mode 100644 Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-keys.md create mode 100644 Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-overview.md create mode 100644 Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-tasks.md 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..5ded18877 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-certificates.md @@ -0,0 +1,213 @@ +--- +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 + +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 certificate requirements +- Only needs device certificate +- No mutual authentication + +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 +- Enhanced certificate support +- Similar to SCP11a but with additional features +- Supports offline authorization + +## 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. \ No newline at end of file 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..cdcd6491a --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-device.md @@ -0,0 +1,283 @@ +--- +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); + } + } +} +``` + +## Error Handling + +### Common Issues + +1. **Data Access Errors** + - Verify authentication status + - Check access permissions + - Validate tag values + - Handle missing data gracefully + +2. **Storage Errors** + - Check available space + - Validate data format + - Handle truncation + - Implement retry logic + +### Status Codes + +| Status | Description | Action | +|--------|-------------|--------| +| 0x6A82 | File not found | Verify tag exists | +| 0x6A86 | Wrong parameters | Check parameter values | +| 0x6982 | Security status not satisfied | Authenticate first | +| 0x6985 | Conditions not satisfied | Check prerequisites | + +> [!NOTE] +> Always validate data before storing and handle errors appropriately to maintain device integrity. + +## Security Considerations + +1. **Data Protection** + - Use secure channels for sensitive operations + - Validate data integrity + - Implement access controls + - Monitor data operations + +2. **Error Handling** + - Log security-relevant errors + - Implement secure error recovery + - Avoid information leakage + - Maintain audit trail + +3. **Access Control** + - Verify authentication + - Check permissions + - Validate operations + - Monitor access patterns + +> [!IMPORTANT] +> Some operations may permanently modify the YubiKey. Always validate operations before execution. \ 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..eb5b6854c --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-keys.md @@ -0,0 +1,172 @@ +--- +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 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, 0x03); +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. 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, 0x02); +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, 0x02); +var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); +``` + +## Security Considerations + +1. **Key Protection** + - Store keys securely + - Use unique keys per device when possible + - Consider using SCP11 for better key management + - Avoid storing sensitive keys in source code or configuration files + +2. **Key Version Management** + - Track which keys are loaded on each YubiKey + - Know if YubiKeys have custom keys from manufacturing + - Manage key distribution and storage + - 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..7a9061909 --- /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.z \ 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..41e7b30f5 --- /dev/null +++ b/Yubico.YubiKey/docs/users-manual/application-security-domain/security-domain-tasks.md @@ -0,0 +1,267 @@ +--- +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, 0x01); +defaultSession.PutKey(keyRef, newKeys, 0); +``` + +> [!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, 0x01); +var publicKey = session.GenerateEcKey(keyRef, 0); +``` + +### 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, 0x02); +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, 0x02); +var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); +``` + +## 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 +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** +```csharp +using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); + +// Replace SCP03 keys +var scp03Ref = KeyReference.Create(ScpKeyIds.Scp03, 0x01); +session.PutKey(scp03Ref, scp03Keys, 0); + +// Set up SCP11 +var scp11Ref = KeyReference.Create(ScpKeyIds.Scp11B, 0x01); +var scp11Public = session.GenerateEcKey(scp11Ref, 0); +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 + - Try fallback to default keys + +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 + - Confirm authentication level + +2. **Access Control Issues** + - Check allowlist configuration + - Verify certificate serials + - Validate certificate dates + - Confirm SCP variant support + +> [!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/sdk-programming-guide/secure-channel-protocol.md b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol.md index bbdab27d2..14d10e88b 100644 --- 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 @@ -28,7 +28,7 @@ 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 X509-certificates +- **SCP11** - An asymmetric protocol using elliptic curve cryptography (ECC) and X.509-certificates ## Protocol Overview @@ -41,11 +41,11 @@ Key characteristics: - Supported on YubiKey 5 Series with firmware 5.3+ ### SCP11 (Asymmetric) -SCP11[^1] 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: +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** - Enhanced mutual authentication with additional features +- **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 @@ -58,7 +58,6 @@ Secure channels are particularly valuable when: - Communicating with YubiKeys over networks or untrusted channels (e.g. NFC) - Managing YubiKeys remotely through card management systems -- Protecting sensitive operations in multi-tenant environments - 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. @@ -92,8 +91,15 @@ 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 = new KeyReference(keyId, keyVersionNumber); -var certificates = sdSession.GetCertificates(keyReference); // Get from YubiKey +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)); @@ -119,7 +125,7 @@ using (var pivSession = new PivSession(yubiKeyDevice, scp03params)) } // Using SCP11b -var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); using (var pivSession = new PivSession(yubiKeyDevice, scp11Params)) { // All PivSession-commands are now automatically protected by SCP11 @@ -138,7 +144,7 @@ using (var oathSession = new OathSession(yubiKeyDevice, scp03params)) } // Using SCP11b -var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); using (var oathSession = new OathSession(yubiKeyDevice, scp11Params)) { // All OathSession-commands are now automatically protected by SCP11 @@ -157,7 +163,7 @@ using (var otpSession = new OtpSession(yubiKeyDevice, scp03params)) } // Using SCP11b -var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); using (var otpSession = new OtpSession(yubiKeyDevice, scp11Params)) { // All OtpSession-commands are now automatically protected by SCP11 @@ -175,7 +181,7 @@ using (var yubiHsmSession = new YubiHsmAuthSession(yubiKeyDevice, scp03params)) } // Using SCP11b -var keyReference = new KeyReference(ScpKeyIds.Scp11B, kvn); +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, kvn); using (var yubiHsmSession = new YubiHsmSession(yubiKeyDevice, scp11Params)) { // All yubiHsmSession-commands are now automatically protected by SCP11 @@ -185,7 +191,7 @@ using (var yubiHsmSession = new YubiHsmSession(yubiKeyDevice, scp11Params)) ### Direct Connection -If you need lower-level control, you can establish secure connections directly using [`Connect`](xref:Yubico.YubiKey.IYubiKeyDevice.Connect): +If you need lower-level control, you can establish secure connections directly using [`Connect`](xref:Yubico.YubiKey.IYubiKeyDevice.Connect*): ```csharp // Using application ID @@ -228,10 +234,10 @@ session.StoreCertificates(keyReference, certificates); session.StoreAllowlist(keyReference, allowedSerials); // Import private key -session.PutKey(keyReference, privateKeyParameters, 0) +session.PutKey(keyReference, privateKeyParameters) // Import public key -session.PutKey(keyReference, publicKeyParameters, 0) +session.PutKey(keyReference, publicKeyParameters) // Reset to factory defaults session.Reset(); @@ -288,12 +294,12 @@ Use `SecurityDomainSession` to manage SCP03 key sets: 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, 0); +session.PutKey(newKeyParams.KeyReference, newKeyParams.StaticKeys); // Add another key set using var session = new SecurityDomainSession(yubiKeyDevice, existingKeyParams); -var keyRef = new KeyReference(ScpKeyIds.Scp03, 0x02); // KVN=2 -session.PutKey(keyRef, additionalKeys, 0); +var keyRef = KeyReference.Create(ScpKeyIds.Scp03, 0x02); // KVN=2 +session.PutKey(keyRef, additionalKeys); // Delete a key set session.DeleteKey(keyRef, false); @@ -306,23 +312,21 @@ session.Reset(); 1. **Key Version Numbers (KVN):** - Default key set: KVN=0xFF - - Custom key sets: KVN must be 1, 2, or 3 - - YubiKey enforces these specific values + - Accepted values are between 1 and 0x7F + +2. **Key Id's (KID)** + - Default value: 1 + - Accepted values are between 1 and 3 -2. **Default Key Replacement:** +3. **Default Key Replacement:** - When adding first custom key set, default keys are always removed - - Happens regardless of which KVN (1,2,3) is used - Cannot retain default keys alongside custom keys -3. **Multiple Key Sets:** +4. **Multiple Key Sets:** - After default keys are replaced, can have 1-3 custom key sets - - Each must have unique KVN + - Each must have unique KID - Can add/remove without affecting other sets -4. **Key Replacement:** - - Must authenticate with the key set being replaced - - Cannot replace KVN=1 while authenticated with KVN=2 - ### Example: Complete Key Management Flow ```csharp @@ -331,8 +335,8 @@ var defaultScp03Params = Scp03KeyParameters.DefaultKey; using (var session = new SecurityDomainSession(yubiKeyDevice, defaultScp03Params)) { // Add first custom key set (removes default) - var keyRef1 = new KeyReference(ScpKeyIds.Scp03, 0x01); - session.PutKey(keyRef1, newKeys, 0); + var keyRef1 = KeyReference.Create(ScpKeyIds.Scp03, 0x01); + session.PutKey(keyRef1, newKeys); } // Now authenticate with new keys @@ -340,8 +344,8 @@ var newScp03Params = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, newKeys); using (var session = new SecurityDomainSession(yubiKeyDevice, newScp03Params)) { // Add second key set - var keyRef2 = new KeyReference(ScpKeyIds.Scp03, 0x02); - session.PutKey(keyRef2, customKeys2, 0); + var keyRef2 = KeyReference.Create(ScpKeyIds.Scp03, 0x02); + session.PutKey(keyRef2, customKeys2); // Check current key information var keyInfo = session.GetKeyInformation(); @@ -364,12 +368,12 @@ The YubiKey provides no metadata about installed keys beyond what's available th ## SCP11 (Asymmetric Key Protocol) -SCP11[^1] 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: +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 -[^1]: 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/) +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: @@ -385,18 +389,15 @@ It comes in three variants, each offering different security properties: - **SCP11b**: YubiKey authenticates to host only - Simplest variant, no mutual authentication - - Uses only ephemeral key pairs - - No certificate chain required + - Uses both static and ephemeral key pairs - Suitable when host authentication isn't required - **SCP11c**: Enhanced mutual authentication with additional features - - Most secure variant with additional capabilities - 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 - - Includes transaction mechanism with rollback capability - Supports authorization rules in OCE certificates ### Key Benefits of SCP11 over SCP03 @@ -404,9 +405,8 @@ It comes in three variants, each offering different security properties: 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 all AES variants (128/192/256) +- Supports ECC for key establishment with AES-128 - Better suited for complex deployment scenarios -- Can operate through intermediary applications securely ### Key Parameters @@ -418,7 +418,7 @@ Unlike SCP03's static keys, SCP11 uses `Scp11KeyParameters` which can contain: ```csharp // SCP11b basic parameters -var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, 0x1); var scp11Params = new Scp11KeyParameters( keyReference, new ECPublicKeyParameters(publicKey)); @@ -440,12 +440,12 @@ Use `SecurityDomainSession` to manage SCP11 keys and certificates: using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); // Generate new EC key pair -var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x3); -var publicKey = session.GenerateEcKey(keyReference, 0); +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, 0); +session.PutKey(keyReference, privateKey); // Store certificates session.StoreCertificates(keyReference, certificates); @@ -464,14 +464,17 @@ Simplest variant, where YubiKey authenticates to host: ```csharp // Get certificates stored on YubiKey -var keyReference = new KeyReference(ScpKeyIds.Scp11B, 0x1); +var keyReference = KeyReference.Create(ScpKeyIds.Scp11B, 0x1); IReadOnlyCollection certificateList; using (var session = new SecurityDomainSession(yubiKeyDevice)) { certificateList = session.GetCertificates(keyReference); } -// Create parameters using leaf certificate +// 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( @@ -491,15 +494,15 @@ Full mutual authentication requires more setup: using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); const byte kvn = 0x03; -var keyRef = new KeyReference(ScpKeyIds.Scp11A, kvn); +var keyRef = KeyReference.Create(ScpKeyIds.Scp11A, kvn); // Generate new key pair on YubiKey -var newPublicKey = session.GenerateEcKey(keyRef, 0); +var newPublicKey = session.GenerateEcKey(keyRef); // Setup off-card entity (OCE) -var oceRef = new KeyReference(OceKid, kvn); +var oceRef = KeyReference.Create(OceKid, kvn); var ocePublicKey = new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()!); -session.PutKey(oceRef, ocePublicKey, 0); +session.PutKey(oceRef, ocePublicKey); // Store CA identifier var ski = GetSubjectKeyIdentifier(oceCerts.Ca); diff --git a/Yubico.YubiKey/docs/users-manual/toc.yml b/Yubico.YubiKey/docs/users-manual/toc.yml index 0081cd7bd..473fc1866 100644 --- a/Yubico.YubiKey/docs/users-manual/toc.yml +++ b/Yubico.YubiKey/docs/users-manual/toc.yml @@ -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/Yubico/YubiKey/Scp/KeyReference.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs index 2bddfe8e5..d66ae26fb 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Scp/KeyReference.cs @@ -32,6 +32,14 @@ public class KeyReference /// public byte VersionNumber { get; } + internal KeyReference( + byte id, + byte versionNumber) + { + Id = id; + VersionNumber = versionNumber; + } + /// /// Initializes a new instance of the class. /// @@ -39,9 +47,7 @@ public class KeyReference /// for a list of possible Key Id's. /// The version number of the key (KVN). Must be between 1 and 127. /// See the GlobalPlatform Technology Card Specification v2.3 Amendment F §5.1 Cryptographic Keys for more information on the available KIDs. - public KeyReference( - byte id, - byte versionNumber) + public static KeyReference Create(byte id, byte versionNumber) { if (versionNumber > 127) { @@ -53,8 +59,7 @@ public KeyReference( throw new ArgumentException(nameof(id), "Key ID (KID) must be between 0 and 127"); } - Id = id; - VersionNumber = versionNumber; + return new KeyReference(id, versionNumber); } /// diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs index d1a99d4d3..1d2f6d2a7 100644 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Scp/KeyReferenceTests.cs @@ -29,9 +29,33 @@ public void Constructor_ValidParameters_SetsProperties() } [Fact] - public void Constructor_InvalidParameters_ThrowsException() + public void FactoryMethod_ValidParameters_SetsProperties() { - Assert.Throws(() => new KeyReference(ScpKeyIds.Scp11B, 128)); + 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 From c3fd1e85d08974e4999d5875680074a0c022434a Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Mon, 16 Dec 2024 20:07:17 +0100 Subject: [PATCH 49/53] docs: adjusted security domain docs --- .../security-domain-certificates.md | 41 ++++++--- .../security-domain-device.md | 85 +++++-------------- .../security-domain-keys.md | 16 ++-- .../security-domain-tasks.md | 29 +++---- .../secure-channel-protocol.md | 9 +- 5 files changed, 73 insertions(+), 107 deletions(-) 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 index 5ded18877..147c61344 100644 --- 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 @@ -25,17 +25,18 @@ The Security Domain manages X.509 certificates primarily for SCP11 protocol oper ### 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 -{ +var certificates = new List +{ rootCert, - intermediateCert, - leafCert + intermediateCert, + leafCert }; session.StoreCertificates(keyReference, certificates); ``` @@ -57,7 +58,7 @@ session.StoreCaIssuer(keyReference, subjectKeyIdentifier); var certificates = session.GetCertificates(keyReference); // Get leaf certificate (last in chain) -var leafCert = certificates.Last(); +var leafCert = certificates.Last(); // Get supported CA identifiers var caIds = session.GetSupportedCaIdentifiers( @@ -70,11 +71,19 @@ var caIds = session.GetSupportedCaIdentifiers( ### 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 +var allowedSerials = new List { "7F4971B0AD51F84C9DA9928B2D5FEF5E16B2920A", "6B90028800909F9FFCD641346933242748FBE9AD" @@ -90,12 +99,14 @@ session.ClearAllowList(keyReference); 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); @@ -110,11 +121,12 @@ session.StoreCaIssuer(oceRef, skiBytes); ``` ### SCP11b -- Simplest certificate requirements -- Only needs device certificate -- No mutual authentication + +- 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); @@ -125,25 +137,29 @@ session.StoreCertificates(keyRef, new[] { deviceCert }); ``` ### SCP11c -- Enhanced certificate support + +- Mutual authentication - Similar to SCP11a but with additional features -- Supports offline authorization +- 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 @@ -198,6 +214,7 @@ session.StoreAllowlist(keyRef, newAllowedSerials); ### Troubleshooting 1. **Certificate Loading Issues** + - Verify certificate format (X.509 v3) - Check certificate chain order - Ensure sufficient storage space @@ -210,4 +227,4 @@ session.StoreAllowlist(keyRef, newAllowedSerials); - Validate certificate dates > [!NOTE] -> For additional details on secure channel establishment and certificate usage, refer to the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation. \ No newline at end of file +> 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 index cdcd6491a..1a6543f35 100644 --- 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 @@ -37,11 +37,11 @@ var cardData = session.GetCardRecognitionData(); The card recognition data follows this TLV structure: -| Tag | Description | -|-----|-------------| -| 0x66 | Card Data template | -| 0x73 | Card Recognition Data | -| ... | Card-specific data elements | +| Tag | Description | +| ---- | --------------------------- | +| 0x66 | Card Data template | +| 0x73 | Card Recognition Data | +| ... | Card-specific data elements | ## Generic Data Operations @@ -67,14 +67,14 @@ 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 | +| 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 @@ -100,13 +100,14 @@ var caIds = session.GetSupportedCaIdentifiers( ### 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 @@ -114,6 +115,7 @@ foreach (var entry in keyInfo) ``` 2. **Certificate Status** + ```csharp // Check certificate configuration foreach (var key in keyInfo.Keys) @@ -136,6 +138,7 @@ foreach (var key in keyInfo.Keys) ### TLV Data Handling 1. **Reading TLV Data** + ```csharp var tlvData = session.GetData(tag); var tlvReader = new TlvReader(tlvData); @@ -150,6 +153,7 @@ while (tlvReader.HasData) ``` 2. **Writing TLV Data** + ```csharp using var ms = new MemoryStream(); using var writer = new BinaryWriter(ms); @@ -229,55 +233,4 @@ public void CleanupDeviceData() } } } -``` - -## Error Handling - -### Common Issues - -1. **Data Access Errors** - - Verify authentication status - - Check access permissions - - Validate tag values - - Handle missing data gracefully - -2. **Storage Errors** - - Check available space - - Validate data format - - Handle truncation - - Implement retry logic - -### Status Codes - -| Status | Description | Action | -|--------|-------------|--------| -| 0x6A82 | File not found | Verify tag exists | -| 0x6A86 | Wrong parameters | Check parameter values | -| 0x6982 | Security status not satisfied | Authenticate first | -| 0x6985 | Conditions not satisfied | Check prerequisites | - -> [!NOTE] -> Always validate data before storing and handle errors appropriately to maintain device integrity. - -## Security Considerations - -1. **Data Protection** - - Use secure channels for sensitive operations - - Validate data integrity - - Implement access controls - - Monitor data operations - -2. **Error Handling** - - Log security-relevant errors - - Implement secure error recovery - - Avoid information leakage - - Maintain audit trail - -3. **Access Control** - - Verify authentication - - Check permissions - - Validate operations - - Monitor access patterns - -> [!IMPORTANT] -> Some operations may permanently modify the YubiKey. Always validate operations before execution. \ No newline at end of file +``` \ 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 index eb5b6854c..83dda69fa 100644 --- 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 @@ -27,7 +27,7 @@ For protocol details and secure channel implementation, see the [Secure Channel 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 used for authentication and key agreement +- **SCP11 Keys**: Asymmetric NIST P-256 keys and X.509-certificates used for authentication and key agreement ## SCP03 Key Management @@ -71,7 +71,7 @@ SCP11 uses NIST P-256 elliptic curve cryptography. Keys can be: ```csharp // Generate new EC key pair -var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, 0x03); +var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); var publicKey = session.GenerateEcKey(keyRef); ``` @@ -115,7 +115,7 @@ session.Reset(); ``` > [!WARNING] -> Resetting removes all custom keys and restores factory defaults. Ensure you have backups before resetting. +> Resetting removes all custom keys and restores factory defaults (within the Security Domain). Ensure you have backups before resetting. ## Key Rotation @@ -128,7 +128,7 @@ Regular key rotation is recommended for security. Here are typical rotation proc using var session = new SecurityDomainSession(yubiKeyDevice, currentScp03Params); // Replace with new keys -var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, 0x02); +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); session.PutKey(newKeyRef, newStaticKeys, currentKvn); ``` @@ -138,8 +138,8 @@ session.PutKey(newKeyRef, newStaticKeys, currentKvn); using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); // Generate new key pair -var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, 0x02); -var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); // Replaces oldKvn ``` ## Security Considerations @@ -147,13 +147,11 @@ var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); 1. **Key Protection** - Store keys securely - Use unique keys per device when possible - - Consider using SCP11 for better key management + - 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 - - Know if YubiKeys have custom keys from manufacturing - - Manage key distribution and storage - Track KVNs in use 3. **Recovery Planning** 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 index 41e7b30f5..dc0243869 100644 --- 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 @@ -44,8 +44,8 @@ using var defaultSession = new SecurityDomainSession( // Generate or obtain your secure keys var newKeys = new StaticKeys(newMacKey, newEncKey, newDekKey); -var keyRef = KeyReference.Create(ScpKeyIds.Scp03, 0x01); -defaultSession.PutKey(keyRef, newKeys, 0); +var keyRef = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); +defaultSession.PutKey(keyRef, newKeys); ``` > [!WARNING] @@ -62,8 +62,8 @@ Start with an authenticated SCP03 session: using var session = new SecurityDomainSession(yubiKeyDevice, scp03Params); // Generate SCP11b key pair -var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, 0x01); -var publicKey = session.GenerateEcKey(keyRef, 0); +var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var publicKey = session.GenerateEcKey(keyRef); ``` ### 2. Configure Certificate Chain @@ -94,7 +94,7 @@ session.StoreAllowlist(keyRef, allowedSerials); using var session = new SecurityDomainSession(yubiKeyDevice, currentScp03Params); // Replace with new keys -var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, 0x02); +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, newKvn); session.PutKey(newKeyRef, newStaticKeys, currentKvn); ``` @@ -104,8 +104,8 @@ session.PutKey(newKeyRef, newStaticKeys, currentKvn); using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); // Generate new key pair -var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, 0x02); -var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); +var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, newKvn); +var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); // Will be replaced ``` ## Recovery Operations @@ -137,7 +137,7 @@ foreach (var key in activeKeys) ### Factory Reset ```csharp -// Warning: This removes all custom keys +// Warning: This removes all custom keys in the Security Domain using var session = new SecurityDomainSession(yubiKeyDevice); session.Reset(); ``` @@ -181,17 +181,17 @@ var scp03Keys = GenerateSecureKeys(); var (privateKey, publicKey, certificates) = GenerateScp11Credentials(); ``` -2. **Configure YubiKey** +2. **Configure YubiKey for SCP11B** ```csharp using var session = new SecurityDomainSession(yubiKeyDevice, Scp03KeyParameters.DefaultKey); // Replace SCP03 keys -var scp03Ref = KeyReference.Create(ScpKeyIds.Scp03, 0x01); -session.PutKey(scp03Ref, scp03Keys, 0); +var scp03Ref = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); +session.PutKey(scp03Ref, scp03Keys); // Set up SCP11 -var scp11Ref = KeyReference.Create(ScpKeyIds.Scp11B, 0x01); -var scp11Public = session.GenerateEcKey(scp11Ref, 0); +var scp11Ref = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); +var scp11Public = session.GenerateEcKey(scp11Ref); session.StoreCertificates(scp11Ref, certificates); ``` @@ -241,7 +241,6 @@ foreach (var cert in certificates) - Verify key version numbers - Check key reference values - Confirm key components - - Try fallback to default keys 2. **Failed Key Import** - Validate key formats @@ -255,13 +254,11 @@ foreach (var cert in certificates) - Verify chain order - Check CA configuration - Validate certificate formats - - Confirm authentication level 2. **Access Control Issues** - Check allowlist configuration - Verify certificate serials - Validate certificate dates - - Confirm SCP variant support > [!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/sdk-programming-guide/secure-channel-protocol.md b/Yubico.YubiKey/docs/users-manual/sdk-programming-guide/secure-channel-protocol.md index 14d10e88b..606cd1c1d 100644 --- 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 @@ -501,7 +501,7 @@ var newPublicKey = session.GenerateEcKey(keyRef); // Setup off-card entity (OCE) var oceRef = KeyReference.Create(OceKid, kvn); -var ocePublicKey = new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()!); +var ocePublicKey = new ECPublicKeyParameters(oceCerts.Ca.PublicKey.GetECDsaPublicKey()); session.PutKey(oceRef, ocePublicKey); // Store CA identifier @@ -533,14 +533,15 @@ using var session = new SecurityDomainSession(yubiKeyDevice, scp11Params); - Imported keys must be properly protected 3. **Protocol Selection:** - - SCP11b: Simpler but less secure - - SCP11a: Better security through mutual auth - - SCP11c: Enhanced features but more complex + - 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 From 068560a0f5744ac05ae4adfff9ac4147c42a3e2a Mon Sep 17 00:00:00 2001 From: Elena Quijano Date: Mon, 16 Dec 2024 20:31:25 -0800 Subject: [PATCH 50/53] fixed header style, typos --- .../security-domain-certificates.md | 24 +++---- .../security-domain-device.md | 38 +++++------ .../security-domain-keys.md | 28 ++++---- .../security-domain-overview.md | 12 ++-- .../security-domain-tasks.md | 48 +++++++------- .../secure-channel-protocol.md | 66 +++++++++---------- 6 files changed, 108 insertions(+), 108 deletions(-) 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 index 147c61344..4d15cc4b3 100644 --- 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 @@ -16,13 +16,13 @@ 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. --> -# Security Domain Certificate Management +# 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 +## Certificate operations -### Storing Certificates +### Storing certificates Certificates are stored in chains associated with specific key references. A typical certificate chain includes: @@ -41,7 +41,7 @@ var certificates = new List session.StoreCertificates(keyReference, certificates); ``` -### CA Configuration +### CA configuration For SCP11a and SCP11c, you need to configure the Certificate Authority (CA) information: @@ -51,7 +51,7 @@ byte[] subjectKeyIdentifier = GetSkiFromCertificate(caCert); session.StoreCaIssuer(keyReference, subjectKeyIdentifier); ``` -### Retrieving Certificates +### Retrieving certificates ```csharp // Get all certificates for a key reference @@ -67,9 +67,9 @@ var caIds = session.GetSupportedCaIdentifiers( ); ``` -## Access Control +## Access control -### Certificate Allowlists +### Certificate allowlists Use of the allowlist can provide the following benefits: - A strong binding to one (or multiple) OCE(s) @@ -94,7 +94,7 @@ session.StoreAllowlist(keyReference, allowedSerials); session.ClearAllowList(keyReference); ``` -## Certificate Management by SCP11 Variant +## Certificate management by SCP11 variant Different SCP11 variants have different certificate requirements: @@ -142,7 +142,7 @@ session.StoreCertificates(keyRef, new[] { deviceCert }); - 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 +## Security considerations 1. **Certificate Validation** @@ -174,9 +174,9 @@ session.StoreCertificates(keyRef, new[] { deviceCert }); > [!IMPORTANT] > Most certificate operations require an authenticated session. Operations are typically only available when using SCP11a or SCP11c variants. -## Common Tasks +## Common tasks -### Initial Certificate Setup +### Initial certificate setup 1. Generate or obtain required certificates 2. Store certificate chain on YubiKey @@ -199,7 +199,7 @@ session.StoreCaIssuer(oceRef, GetSkiFromCertificate(caCert)); session.StoreAllowlist(keyRef, allowedSerials); ``` -### Certificate Rotation +### Certificate rotation ```csharp using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); 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 index 1a6543f35..932e8d0eb 100644 --- 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 @@ -16,15 +16,15 @@ 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. --> -# Security Domain Device Information +# 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 +## 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 +### Retrieving card data ```csharp using var session = new SecurityDomainSession(yubiKeyDevice); @@ -33,7 +33,7 @@ using var session = new SecurityDomainSession(yubiKeyDevice); var cardData = session.GetCardRecognitionData(); ``` -### Card Data Structure +### Card data structure The card recognition data follows this TLV structure: @@ -43,11 +43,11 @@ The card recognition data follows this TLV structure: | 0x73 | Card Recognition Data | | ... | Card-specific data elements | -## Generic Data Operations +## Generic data operations The Security Domain supports general-purpose data retrieval and storage using TLV (Tag-Length-Value) formatting. -### Retrieving Data +### Retrieving data ```csharp // Get data for a specific tag @@ -57,7 +57,7 @@ var data = session.GetData(tag); var data = session.GetData(tag, additionalData); ``` -### Storing Data +### Storing data ```csharp // Store TLV-formatted data @@ -65,7 +65,7 @@ byte[] tlvData = PrepareTlvData(); session.StoreData(tlvData); ``` -### Common Data Tags +### Common data tags | Tag | Description | Access Level | | ------ | ----------------- | ------------ | @@ -76,9 +76,9 @@ session.StoreData(tlvData); | 0xFF33 | KLOC Identifiers | Read/Write | | 0xFF34 | KLCC Identifiers | Read/Write | -## Device Configuration +## Device configuration -### Checking Capabilities +### Checking capabilities ```csharp // Check SCP11 support @@ -97,7 +97,7 @@ var caIds = session.GetSupportedCaIdentifiers( ); ``` -### Device Status Information +### Device status information 1. **Key Status** @@ -114,7 +114,7 @@ foreach (var entry in keyInfo) } ``` -2. **Certificate Status** +2. **Certificate status** ```csharp // Check certificate configuration @@ -133,9 +133,9 @@ foreach (var key in keyInfo.Keys) } ``` -## Data Management +## Data management -### TLV Data Handling +### TLV data handling 1. **Reading TLV Data** @@ -152,7 +152,7 @@ while (tlvReader.HasData) } ``` -2. **Writing TLV Data** +2. **Writing TLV data** ```csharp using var ms = new MemoryStream(); @@ -166,7 +166,7 @@ writer.Write(value); session.StoreData(ms.ToArray()); ``` -### Data Organization +### Data organization The Security Domain organizes data hierarchically: @@ -185,9 +185,9 @@ Root └── KLCC Data (0xFF34) ``` -## Maintenance Operations +## Maintenance operations -### Data Validation +### Data validation ```csharp // Validate stored data @@ -209,7 +209,7 @@ public void ValidateDeviceConfiguration() } ``` -### Data Cleanup +### Data cleanup ```csharp // Remove unused data 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 index 83dda69fa..2c06af9a8 100644 --- 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 @@ -16,20 +16,20 @@ 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. --> -# Security Domain Key Management +# 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 +## 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 +## SCP03 key management Each SCP03 key set consists of three AES-128 keys that work together to secure communications: @@ -39,7 +39,7 @@ Each SCP03 key set consists of three AES-128 keys that work together to secure c | Key-MAC | Channel MAC key for message authentication | | Key-DEK | Data encryption key for sensitive data | -### Managing SCP03 Keys +### Managing SCP03 keys ```csharp // Put a new SCP03 key set @@ -61,13 +61,13 @@ SCP03 key sets are identified by Key Version Numbers: > [!NOTE] > When adding the first custom key set, the default keys are automatically removed. -## SCP11 Key Management +## 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 +### Generating keys ```csharp // Generate new EC key pair @@ -75,7 +75,7 @@ var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); var publicKey = session.GenerateEcKey(keyRef); ``` -### Importing Keys +### Importing keys ```csharp // Import existing private key @@ -87,9 +87,9 @@ var publicKey = new ECPublicKeyParameters(ecdsaPublic); session.PutKey(keyRef, publicKey); ``` -## Key Management Operations +## Key management operations -### Querying Key Information +### Querying key information ```csharp // Get information about installed keys @@ -102,7 +102,7 @@ foreach (var entry in keyInfo) } ``` -### Deleting Keys +### Deleting keys Keys can be deleted individually or reset to factory defaults: @@ -117,11 +117,11 @@ session.Reset(); > [!WARNING] > Resetting removes all custom keys and restores factory defaults (within the Security Domain). Ensure you have backups before resetting. -## Key Rotation +## Key rotation Regular key rotation is recommended for security. Here are typical rotation procedures: -### SCP03 Key Rotation +### SCP03 key rotation ```csharp // Authenticate with current keys @@ -132,7 +132,7 @@ var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, keyVersionNumber); session.PutKey(newKeyRef, newStaticKeys, currentKvn); ``` -### SCP11 Key Rotation +### SCP11 key rotation ```csharp using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); @@ -142,7 +142,7 @@ var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); // Replaces oldKvn ``` -## Security Considerations +## Security considerations 1. **Key Protection** - Store keys securely 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 index 7a9061909..526978209 100644 --- 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 @@ -16,7 +16,7 @@ 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. --> -# Security Domain Overview +# 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. @@ -32,7 +32,7 @@ Hardware: Transport Protocols: - Smartcard over USB or NFC -## Core Features +## Core features The Security Domain provides: - Management of secure communication channels (SCP03 and SCP11) @@ -40,7 +40,7 @@ The Security Domain provides: - Certificate management for asymmetric protocols - Access control through certificate allowlists -## Basic Usage +## Basic usage ```csharp // Create session without SCP protection @@ -52,7 +52,7 @@ using var session = new SecurityDomainSession(yubiKeyDevice, scpKeyParameters); session.GenerateEcKey(parameters...); // Protected by secure channel ``` -## Documentation Structure +## Documentation structure The Security Domain functionality is documented in the following sections: @@ -61,7 +61,7 @@ The Security Domain functionality is documented in the following sections: - [Common Tasks](xref:SecurityDomainTasks) - Setup, configuration, and maintenance operations - [Device Information](xref:SecurityDomainDevice) - Device data and configuration management -## Basic Security Considerations +## Basic security considerations When working with the Security Domain: - Most operations require an authenticated session @@ -70,4 +70,4 @@ When working with the Security Domain: - Maintain proper key and certificate backups > [!NOTE] -> For detailed implementation guidance and best practices, refer to the [Secure Channel Protocol (SCP)](xref:UsersManualScp) documentation.z \ No newline at end of file +> 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 index dc0243869..bd0489952 100644 --- 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 @@ -16,13 +16,13 @@ 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. --> -# Security Domain Common Tasks +# 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 +## Setting up a new YubiKey -### 1. Initial State Assessment +### 1. Initial state assessment Check the current configuration of your YubiKey: @@ -32,7 +32,7 @@ var keyInfo = session.GetKeyInformation(); var hasDefaultKeys = keyInfo.Any(k => k.Key.VersionNumber == 0xFF); ``` -### 2. Replacing Default SCP03 Keys +### 2. Replacing default SCP03 keys Always replace default keys in production environments: @@ -51,9 +51,9 @@ defaultSession.PutKey(keyRef, newKeys); > [!WARNING] > Default keys provide no security. Replace them before deploying to production. -## Setting Up SCP11 +## Setting up SCP11 -### 1. Generate Initial Keys +### 1. Generate initial keys Start with an authenticated SCP03 session: @@ -66,7 +66,7 @@ var keyRef = KeyReference.Create(ScpKeyIds.Scp11B, keyVersionNumber); var publicKey = session.GenerateEcKey(keyRef); ``` -### 2. Configure Certificate Chain +### 2. Configure certificate chain ```csharp // Store certificates @@ -77,7 +77,7 @@ var caRef = KeyReference.Create(OceKid, kvn); session.StoreCaIssuer(caRef, skiBytes); ``` -### 3. Set Up Access Control +### 3. Set up access control ```csharp // Configure certificate allowlist @@ -85,9 +85,9 @@ var allowedSerials = GetAllowedCertificateSerials(); session.StoreAllowlist(keyRef, allowedSerials); ``` -## Key Management Tasks +## Key management tasks -### Rotating SCP03 Keys +### Rotating SCP03 keys ```csharp // Authenticate with current keys @@ -98,7 +98,7 @@ var newKeyRef = KeyReference.Create(ScpKeyIds.Scp03, newKvn); session.PutKey(newKeyRef, newStaticKeys, currentKvn); ``` -### Rotating SCP11 Keys +### Rotating SCP11 keys ```csharp using var session = new SecurityDomainSession(yubiKeyDevice, scpParams); @@ -108,9 +108,9 @@ var newKeyRef = KeyReference.Create(ScpKeyIds.Scp11B, newKvn); var newPublicKey = session.GenerateEcKey(newKeyRef, oldKvn); // Will be replaced ``` -## Recovery Operations +## Recovery operations -### Status Check +### Status check ```csharp using var session = new SecurityDomainSession(yubiKeyDevice); @@ -134,7 +134,7 @@ foreach (var key in activeKeys) } ``` -### Factory Reset +### Factory reset ```csharp // Warning: This removes all custom keys in the Security Domain @@ -145,9 +145,9 @@ session.Reset(); > [!IMPORTANT] > Resetting removes all custom keys and certificates. Have a recovery plan ready. -## Integration with Other Applications +## Integration with other applications -### PIV with Secure Channel +### PIV with secure channel ```csharp // Using SCP03 @@ -159,7 +159,7 @@ using var pivSession = new PivSession(yubiKeyDevice, scp11Params); pivSession.GenerateKeyPair(...); // Protected by SCP11 ``` -### OATH with Secure Channel +### OATH with secure channel ```csharp // Using SCP03 @@ -171,11 +171,11 @@ using var oathSession = new OathSession(yubiKeyDevice, scp11Params); oathSession.PutCredential(...); // Protected by SCP11 ``` -## Production Deployment Tasks +## Production deployment tasks -### Initial Provisioning +### Initial provisioning -1. **Prepare Keys and Certificates** +1. **Prepare keys and certificates** ```csharp var scp03Keys = GenerateSecureKeys(); var (privateKey, publicKey, certificates) = GenerateScp11Credentials(); @@ -195,7 +195,7 @@ var scp11Public = session.GenerateEcKey(scp11Ref); session.StoreCertificates(scp11Ref, certificates); ``` -3. **Validate Configuration** +3. **Validate configuration** ```csharp // Test new keys using var verifySession = new SecurityDomainSession( @@ -206,7 +206,7 @@ var keyInfo = verifySession.GetKeyInformation(); // Verify expected keys are present ``` -### Regular Maintenance +### Regular maintenance 1. **Monitor Key Status** ```csharp @@ -235,7 +235,7 @@ foreach (var cert in certificates) ## Troubleshooting -### Key Issues +### Key issues 1. **Unable to Authenticate** - Verify key version numbers @@ -248,7 +248,7 @@ foreach (var cert in certificates) - Verify available space - Confirm key compatibility -### Certificate Issues +### Certificate issues 1. **Certificate Chain Problems** - Verify chain order 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 index 606cd1c1d..fe9ae9780 100644 --- 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 @@ -30,9 +30,9 @@ 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 +## Protocol overview -### SCP03 (Symmetric) +### 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: @@ -40,7 +40,7 @@ Key characteristics: - Three keys per set: encryption, MAC, and data encryption - Supported on YubiKey 5 Series with firmware 5.3+ -### SCP11 (Asymmetric) +### 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 @@ -52,7 +52,7 @@ Key characteristics: - Certificate-based authentication - Supported on YubiKey 5 Series with firmware 5.7.2+ -## When to Use Secure Channels +## When to use secure channels Secure channels are particularly valuable when: @@ -62,7 +62,7 @@ Secure channels are particularly valuable when: 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 +## 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. @@ -72,11 +72,11 @@ SCP11, being asymmetric, simplifies key management but requires proper certifica 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 +## 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 +### 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. @@ -111,9 +111,9 @@ using (var pivSession = new PivSession(yubiKeyDevice, scp11Params)) } ``` -### Application Examples +### Application examples -#### PIV with Secure Channel +#### PIV with secure channel ```csharp // Using SCP03 @@ -121,7 +121,7 @@ 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 SCP11 + // All PivSession-commands are now automatically protected by SCP03 } // Using SCP11b @@ -132,7 +132,7 @@ using (var pivSession = new PivSession(yubiKeyDevice, scp11Params)) } ``` -#### OATH with Secure Channel +#### OATH with secure channel ```csharp // Using SCP03 @@ -140,7 +140,7 @@ 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 SCP11 + // All oathSession-commands are now automatically protected by SCP03 } // Using SCP11b @@ -151,7 +151,7 @@ using (var oathSession = new OathSession(yubiKeyDevice, scp11Params)) } ``` -#### OTP with Secure Channel +#### OTP with secure channel ```csharp // Using SCP03 @@ -159,7 +159,7 @@ 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 SCP11 + // All otpSession-commands are now automatically protected by SCP03 } // Using SCP11b @@ -170,7 +170,7 @@ using (var otpSession = new OtpSession(yubiKeyDevice, scp11Params)) } ``` -#### YubiHSM Auth with Secure Channel +#### YubiHSM Auth with secure channel ```csharp // Using SCP03 StaticKeys scp03Keys = RetrieveScp03KeySet(); // Your static keys @@ -189,7 +189,7 @@ using (var yubiHsmSession = new YubiHsmSession(yubiKeyDevice, scp11Params)) ``` -### Direct Connection +### Direct connection If you need lower-level control, you can establish secure connections directly using [`Connect`](xref:Yubico.YubiKey.IYubiKeyDevice.Connect*): @@ -217,7 +217,7 @@ if (yubiKeyDevice.TryConnect( } ``` -### Security Domain Management +### Security Domain management The [`SecurityDomainSession`](xref:Yubico.YubiKey.Scp.SecurityDomainSession) class provides methods to manage SCP configurations: @@ -248,9 +248,9 @@ session.Reset(); The next sections will detail specific key management and protocol details for both SCP03 and SCP11. -## SCP03 (Symmetric Key Protocol) +## SCP03 (symmetric key protocol) -### Static Keys Structure +### 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: @@ -265,7 +265,7 @@ var staticKeys = new StaticKeys(keyDataMac, keyDataEnc, keyDataDek); var scp03Params = new Scp03KeyParameters(ScpKeyIds.Scp03, 0x01, staticKeys); ``` -### Key Sets on the YubiKey +### 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): @@ -285,7 +285,7 @@ Standard YubiKeys are manufactured with a default key set (KVN=0xFF): The default keys are publicly known (0x40 41 42 ... 4F) and provide no security. You should replace them in production environments. -### Managing Key Sets +### Managing key sets Use `SecurityDomainSession` to manage SCP03 key sets: @@ -308,7 +308,7 @@ session.DeleteKey(keyRef, false); session.Reset(); ``` -### Key Set Rules +### Key set rules 1. **Key Version Numbers (KVN):** - Default key set: KVN=0xFF @@ -327,7 +327,7 @@ session.Reset(); - Each must have unique KID - Can add/remove without affecting other sets -### Example: Complete Key Management Flow +### Example: complete key management flow ```csharp // Start with default keys @@ -352,7 +352,7 @@ using (var session = new SecurityDomainSession(yubiKeyDevice, newScp03Params)) } ``` -### Key Management Responsibilities +### Key management responsibilities Your application must: - Track which keys are loaded on each YubiKey @@ -366,7 +366,7 @@ The YubiKey provides no metadata about installed keys beyond what's available th > [!NOTE] > Always use proper key management in production. Never store sensitive keys in source code or configuration files. -## SCP11 (Asymmetric Key Protocol) +## 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 @@ -378,7 +378,7 @@ Detailed information about SCP11 can be found in [GlobalPlatform Card Technology It comes in three variants, each offering different security properties: -### SCP11 Variants +### SCP11 variants - **SCP11a**: Full mutual authentication between host and YubiKey using certificates - Basic mutual authentication @@ -400,7 +400,7 @@ It comes in three variants, each offering different security properties: - Cryptographic operations remain on secure OCE server - Supports authorization rules in OCE certificates -### Key Benefits of SCP11 over SCP03 +### Key benefits of SCP11 over SCP03 SCP11 provides several advantages over SCP03: - Uses certificates instead of pre-shared keys for authentication @@ -408,7 +408,7 @@ SCP11 provides several advantages over SCP03: - Supports ECC for key establishment with AES-128 - Better suited for complex deployment scenarios -### Key Parameters +### Key parameters Unlike SCP03's static keys, SCP11 uses `Scp11KeyParameters` which can contain: - Public/private key pairs @@ -432,7 +432,7 @@ var scp11Params = new Scp11KeyParameters( certificateChain); // Certificate chain for authentication ``` -### Key Management +### Key management Use `SecurityDomainSession` to manage SCP11 keys and certificates: @@ -458,7 +458,7 @@ var serials = new List { session.StoreAllowlist(oceKeyReference, serials); ``` -### SCP11b Example +### SCP11b example Simplest variant, where YubiKey authenticates to host: @@ -485,7 +485,7 @@ var keyParams = new Scp11KeyParameters( using var pivSession = new PivSession(yubiKeyDevice, keyParams); ``` -### SCP11a Example +### SCP11a example Full mutual authentication requires more setup: @@ -520,7 +520,7 @@ var scp11Params = new Scp11KeyParameters( using var session = new SecurityDomainSession(yubiKeyDevice, scp11Params); ``` -### Security Considerations +### Security considerations 1. **Certificate Management:** - Proper certificate validation is crucial @@ -543,7 +543,7 @@ using var session = new SecurityDomainSession(yubiKeyDevice, scp11Params); - Clear allowlists when no longer needed - Can be used as a part of a certificate revocation stategy -### Checking SCP Support +### Checking SCP support ```csharp // Check firmware version for SCP11 support From 12021c8e99c8232611834b1fb2a3cbd312029352 Mon Sep 17 00:00:00 2001 From: Elena Quijano Date: Mon, 16 Dec 2024 20:37:05 -0800 Subject: [PATCH 51/53] fixed typos --- .../application-security-domain/security-domain-device.md | 4 ++-- .../application-security-domain/security-domain-tasks.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index 932e8d0eb..3e970c5e9 100644 --- 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 @@ -114,7 +114,7 @@ foreach (var entry in keyInfo) } ``` -2. **Certificate status** +2. **Certificate Status** ```csharp // Check certificate configuration @@ -152,7 +152,7 @@ while (tlvReader.HasData) } ``` -2. **Writing TLV data** +2. **Writing TLV Data** ```csharp using var ms = new MemoryStream(); 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 index bd0489952..2f0bb6bd8 100644 --- 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 @@ -175,7 +175,7 @@ oathSession.PutCredential(...); // Protected by SCP11 ### Initial provisioning -1. **Prepare keys and certificates** +1. **Prepare Keys and Certificates** ```csharp var scp03Keys = GenerateSecureKeys(); var (privateKey, publicKey, certificates) = GenerateScp11Credentials(); @@ -195,7 +195,7 @@ var scp11Public = session.GenerateEcKey(scp11Ref); session.StoreCertificates(scp11Ref, certificates); ``` -3. **Validate configuration** +3. **Validate Configuration** ```csharp // Test new keys using var verifySession = new SecurityDomainSession( From f80eca773460908649cee240dae17f2c2b74cd78 Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Tue, 17 Dec 2024 11:51:01 +0100 Subject: [PATCH 52/53] docs: added Security Domain Changes to be committed: modified: ../docs/index.md --- Yubico.YubiKey/docs/index.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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). From cf4b0ed01d31b90d7f458348a7deb804a66df80b Mon Sep 17 00:00:00 2001 From: Dennis Dyall Date: Tue, 17 Dec 2024 14:41:42 +0100 Subject: [PATCH 53/53] docs: fixed xref in whats-new.md removed unused docs --- .../docs/users-manual/getting-started/whats-new.md | 2 +- Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) 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 89f8a35f2..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: diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs index 709deca23..5fd02a3f5 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.cs @@ -224,12 +224,6 @@ public PivSession(IYubiKeyDevice yubiKey, ScpKeyParameters? keyParameters = null /// public Func? KeyCollector { get; set; } - /// - /// When the PivSession object goes out of scope, this method is called. - /// It will close the session. The most important function of closing a - /// session is to "un-authenticate" the management key and "un-verify" - /// the PIN. - /// protected override void Dispose(bool disposing) { if (disposing)