Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature: Security Domain and SCP11a/b/c features #164

Open
wants to merge 37 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6e537c7
initial Security Domain implementation
DennisDyallo Aug 15, 2024
2bd752f
feat(scp11b): Import Key
DennisDyallo Oct 30, 2024
366aa8b
feat(scp): Import Static Keys
DennisDyallo Oct 31, 2024
5e9ec88
feat(scp11ab): Initialize SCP Connection
DennisDyallo Nov 5, 2024
d2d1cf5
feat(scp): remaining SCP Commands implemented
DennisDyallo Nov 6, 2024
4017ae4
docs: updated docstrings and manual
DennisDyallo Nov 7, 2024
093f919
tests, misc: added and fixed tests, misc updates
DennisDyallo Nov 11, 2024
81799ca
feat(scp): supports apps over SCP
DennisDyallo Nov 18, 2024
01b6ab2
docs: add comments to TlvObject
DennisDyallo Nov 27, 2024
c41fff1
docs: fix incorrect values in docs for RSA key sizes and their identi…
DennisDyallo Nov 27, 2024
00cdeae
docs: add docs to ApplicationSession.cs
DennisDyallo Nov 27, 2024
f90fbee
logs: change log level in ConnectionFactory
DennisDyallo Nov 27, 2024
f5fbbbf
misc: return ReadOnlyMemory<byte> in ECPublicKeyParameters.cs
DennisDyallo Nov 27, 2024
a786bef
misc: OtpSession cleanup
DennisDyallo Nov 27, 2024
7bd0b36
misc: ScpApduTransform.cs edits
DennisDyallo Nov 27, 2024
23af160
misc: PivSession.cs edits
DennisDyallo Nov 27, 2024
cd13afd
misc: ExternalAuthenticateResponse.cs edits
DennisDyallo Nov 27, 2024
d9cc85f
misc: InitializeUpdateCommand.cs edits
DennisDyallo Nov 27, 2024
660095d
docs: added docs to PutKeyCommand
DennisDyallo Nov 27, 2024
6f8b74e
docs: adding comments and docs
DennisDyallo Nov 27, 2024
de43147
misc: misc edits
DennisDyallo Nov 27, 2024
ff36d90
tests: test edits
DennisDyallo Nov 27, 2024
d77bc6a
docs, misc: edits in ScpApduTransform
DennisDyallo Nov 27, 2024
11b87d9
misc: edits to StoreDataCommand and ChannelEncryption
DennisDyallo Nov 27, 2024
3b2753d
Merge remote-tracking branch 'origin' into feature/scp11
DennisDyallo Nov 28, 2024
7e125ca
misc: remove bad tests
DennisDyallo Nov 28, 2024
1f13c2c
misc: no need to check for null
DennisDyallo Nov 28, 2024
e39cd5a
docs: removed unintended comments in pivsession
DennisDyallo Nov 28, 2024
207a45d
docs: added docstring for Scp11State
DennisDyallo Nov 28, 2024
263a232
misc: scp03state edits
DennisDyallo Nov 28, 2024
35a3bba
tests: rename test methods for Scp03Tests.cs
DennisDyallo Nov 28, 2024
2b6cd76
misc: use ReadOnlyMemory
DennisDyallo Nov 28, 2024
d643d33
misc: typos and formatting
DennisDyallo Nov 28, 2024
8360fb1
Merge branch 'develop' into feature/scp11
DennisDyallo Nov 28, 2024
5be4499
misc: re-add deprecated file
DennisDyallo Nov 28, 2024
d491ea1
misc: minor edits
DennisDyallo Nov 29, 2024
15e7efa
tests: possibility to select by transport
DennisDyallo Nov 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
csharp_style_var_when_type_is_apparent = true:warning
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public IDisposable BeginTransaction(out bool cardWasReset)
uint result = SCardBeginTransaction(_cardHandle);
_log.SCardApiCall(nameof(SCardBeginTransaction), result);

// Sometime the smart card is left in a state where it needs to be reset prior to beginning
// Sometimes the smart card is left in a state where it needs to be reset prior to beginning
// a transaction. We should automatically handle this case.
if (result == ErrorCode.SCARD_W_RESET_CARD)
{
Expand Down
198 changes: 198 additions & 0 deletions Yubico.Core/src/Yubico/Core/Tlv/TlvObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright 2024 Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.IO;
using System.Linq;

namespace Yubico.Core.Tlv
{
/// <summary>
/// Tag, length, Value structure that helps to parse APDU response data.
/// This class handles BER-TLV encoded data with determinate length.
/// </summary>
public class TlvObject
{
/// <summary>
/// Returns the tag.
/// </summary>
public int Tag { get; }

/// <summary>
/// Returns the value.
/// </summary>
public Memory<byte> Value => _bytes.Skip(_offset).Take(Length).ToArray();

/// <summary>
/// Returns the length of the value.
/// </summary>
public int Length { get; }

private readonly byte[] _bytes;
private readonly int _offset;

/// <summary>
/// Creates a new TLV (Tag-Length-Value) object with the specified tag and value.
/// </summary>
/// <param name="tag">The tag value, must be between 0x00 and 0xFFFF.</param>
/// <param name="value">The value data as a read-only span of bytes.</param>
/// <exception cref="TlvException">Thrown when the tag value is outside the supported range (0x00-0xFFFF).</exception>
/// <remarks>
/// 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
/// </remarks>
public TlvObject(int tag, ReadOnlySpan<byte> 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();
}

/// <summary>
/// Returns a copy ofthe Tlv as a BER-TLV encoded byte array.
/// </summary>
public Memory<byte> GetBytes() => _bytes.ToArray();

/// <summary>
/// Parse a Tlv from a BER-TLV encoded byte array.
/// </summary>
/// <param name="data">A byte array containing the TLV encoded data (and nothing more).</param>
/// <returns>The parsed Tlv</returns>
public static TlvObject Parse(ReadOnlySpan<byte> data)
{
ReadOnlySpan<byte> buffer = data;
return ParseFrom(ref buffer);
}

/// <summary>
/// Parses a TLV from a BER-TLV encoded byte array.
/// </summary>
/// <param name="buffer">A byte array containing the TLV encoded data.</param>
/// <returns>The parsed <see cref="TlvObject"/></returns>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentException">Thrown if the buffer does not contain a valid TLV.</exception>
internal static TlvObject ParseFrom(ref ReadOnlySpan<byte> buffer)
{
// The first byte of the TLV is the tag.
int tag = buffer[0];

// Determine if the tag is in long form.
// Long form tags have more than one byte, starting with 0x1F.
if ((buffer[0] & 0x1F) == 0x1F)
{
// Ensure there is enough data for a long form tag.
if (buffer.Length < 2)
{
throw new ArgumentException("Insufficient data for long form tag");
}
// Combine the first two bytes to form the tag.
tag = (buffer[0] << 8) | buffer[1];
buffer = buffer[2..]; // Skip the tag bytes
}
else
{
buffer = buffer[1..]; // Skip the tag byte
}

if (buffer.Length < 1)
{
throw new ArgumentException("Insufficient data for length");
}

// Read the length of the TLV value.
int length = buffer[0];
buffer = buffer[1..];

// If the length is more than one byte, process remaining bytes.
if (length > 0x80)
{
int lengthLn = length - 0x80;
length = 0;
for (int i = 0; i < lengthLn; i++)
{
length = (length << 8) | buffer[0];
buffer = buffer[1..];
}
}

ReadOnlySpan<byte> value = buffer[..length];
buffer = buffer[length..]; // Advance the buffer to the end of the value

return new TlvObject(tag, value);
}

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
/// <remarks>
/// The string is of the form <c>Tlv(0xTAG, LENGTH, VALUE)</c>.
/// <para>
/// The tag is written out in hexadecimal, prefixed by 0x.
/// The length is written out in decimal.
/// The value is written out in hexadecimal.
/// </para>
/// </remarks>
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
}
}
}
97 changes: 97 additions & 0 deletions Yubico.Core/src/Yubico/Core/Tlv/TlvObjects.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.IO;

namespace Yubico.Core.Tlv
{
/// <summary>
/// Utility methods to encode and decode BER-TLV data.
/// </summary>
public static class TlvObjects
{
/// <summary>
/// Decodes a sequence of BER-TLV encoded data into a list of Tlvs.
/// </summary>
/// <param name="data">Sequence of TLV encoded data</param>
/// <returns>List of <see cref="TlvObject"/></returns>
public static IReadOnlyList<TlvObject> DecodeList(ReadOnlySpan<byte> data)
{
var tlvs = new List<TlvObject>();
ReadOnlySpan<byte> buffer = data;
while (!buffer.IsEmpty)
{
var tlv = TlvObject.ParseFrom(ref buffer);
tlvs.Add(tlv);
}

return tlvs.AsReadOnly();
}

/// <summary>
/// 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.
/// </summary>
/// <param name="data">Sequence of TLV encoded data</param>
/// <returns>Dictionary of Tag-Value pairs</returns>
public static IReadOnlyDictionary<int, ReadOnlyMemory<byte>> DecodeMap(ReadOnlySpan<byte> data)
{
var tlvs = new Dictionary<int, ReadOnlyMemory<byte>>();
ReadOnlySpan<byte> buffer = data;
while (!buffer.IsEmpty)
{
var tlv = TlvObject.ParseFrom(ref buffer);
tlvs[tlv.Tag] = tlv.Value;
}

return tlvs;
}

/// <summary>
/// Encodes a list of Tlvs into a sequence of BER-TLV encoded data.
/// </summary>
/// <param name="list">List of Tlvs to encode</param>
/// <returns>BER-TLV encoded list</returns>
public static byte[] EncodeList(IEnumerable<TlvObject> list)
{
if (list is null)
{
throw new ArgumentNullException(nameof(list));
}

using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
foreach (TlvObject? tlv in list)
{
ReadOnlyMemory<byte> bytes = tlv.GetBytes();
writer.Write(bytes.Span.ToArray());
}

return stream.ToArray();
}

/// <summary>
/// Encodes an array of Tlvs into a sequence of BER-TLV encoded data.
/// </summary>
/// <param name="tlvs">Array of Tlvs to encode</param>
/// <returns>BER-TLV encoded array</returns>
public static byte[] EncodeMany(params TlvObject[] tlvs) => EncodeList(tlvs);

/// <summary>
/// Decode a single TLV encoded object, returning only the value.
/// </summary>
/// <param name="expectedTag">The expected tag value of the given TLV data</param>
/// <param name="tlvData">The TLV data</param>
/// <returns>The value of the TLV</returns>
/// <exception cref="InvalidOperationException">If the TLV tag differs from expectedTag</exception>
public static Memory<byte> UnpackValue(int expectedTag, ReadOnlySpan<byte> tlvData)
{
var tlv = TlvObject.Parse(tlvData);
if (tlv.Tag != expectedTag)
{
throw new InvalidOperationException($"Expected tag: {expectedTag:X2}, got {tlv.Tag:X2}");
}

return tlv.Value.ToArray();
}
}
}
Loading