}
[Theory]
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "a3030302020101")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "a3010102020303")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "a3010102020303")]
+ // indefinite length string payload
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "a561620002006161007f616360ff000100")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "a5010002006161006162007f616360ff00")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "a5010002006161006162007f616360ff00")]
+ // CBOR sorting rules do not match canonical string sorting
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "aa", 0, "z", 0 }, "a262616100617a00")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "aa", 0, "z", 0 }, "a2617a0062616100")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "aa", 0, "z", 0 }, "a2617a0062616100")]
+ // Test case distinguishing between RFC7049 and CTAP2 sorting rules
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "", 0, 255, 0 }, "a2600018ff00")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "", 0, 255, 0 }, "a2600018ff00")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "", 0, 255, 0 }, "a218ff006000")]
+ internal static void WriteMap_SimpleValues_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding)
+ {
+ byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+ using var writer = new CborWriter(level);
+ Helpers.WriteValue(writer, value);
+ byte[] actualEncoding = writer.ToArray();
+ AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+ }
+
+ [Theory]
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "a42000a30303020201010061610019010000")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "a4200061610019010000a301010202030300")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "a4190100002000616100a301010202030300")]
+ internal static void WriteMap_NestedValues_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding)
+ {
+ byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+ using var writer = new CborWriter(level);
+ Helpers.WriteValue(writer, value);
+ byte[] actualEncoding = writer.ToArray();
+ AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+ }
+
+ [Theory]
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "bf030302020101ff")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "bf010102020303ff")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "bf010102020303ff")]
+ // indefinite length string payload
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "bf61620002006161007f616360ff000100ff")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "bf010002006161006162007f616360ff00ff")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "bf010002006161006162007f616360ff00ff")]
+ // CBOR sorting rules do not match canonical string sorting
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "aa", 0, "z", 0 }, "bf62616100617a00ff")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "aa", 0, "z", 0 }, "bf617a0062616100ff")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "aa", 0, "z", 0 }, "bf617a0062616100ff")]
+ // Test case distinguishing between RFC7049 and CTAP2 sorting rules
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "", 0, 255, 0 }, "bf600018ff00ff")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "", 0, 255, 0 }, "bf600018ff00ff")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "", 0, 255, 0 }, "bf18ff006000ff")]
+ internal static void WriteMap_SimpleValues_IndefiniteLength_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding)
+ {
+ byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+ using var writer = new CborWriter(level);
+ Helpers.WriteValue(writer, value, useDefiniteLengthCollections: false);
+ byte[] actualEncoding = writer.ToArray();
+ AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+ }
+
+ [Theory]
+ [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "bf2000bf030302020101ff0061610019010000ff")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "bf200061610019010000bf010102020303ff00ff")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "bf190100002000616100bf010102020303ff00ff")]
+ internal static void WriteMap_NestedValues_IndefiniteLength_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding)
+ {
+ byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+ using var writer = new CborWriter(level);
+ Helpers.WriteValue(writer, value, useDefiniteLengthCollections: false);
+ byte[] actualEncoding = writer.ToArray();
+ AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+ }
+
+ [Theory]
[InlineData(new object[] { Map, "a", 1, "a", 2 }, "a2616101616102")]
public static void WriteMap_DuplicateKeys_ShouldSucceed(object[] values, string expectedHexEncoding)
{
}
[Theory]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, 42)]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, 42)]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, "foobar")]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, "foobar")]
+ [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { new string[] { "x", "y" } })]
+ [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { new string[] { "x", "y" } })]
+ internal static void WriteMap_DuplicateKeys_StrictConformance_ShouldFail(CborConformanceLevel level, object dupeKey)
+ {
+ using var writer = new CborWriter(level);
+ writer.WriteStartMap(2);
+ Helpers.WriteValue(writer, dupeKey);
+ writer.WriteInt32(0);
+ Assert.Throws<InvalidOperationException>(() => Helpers.WriteValue(writer, dupeKey));
+ }
+
+ [Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(3)]
[InlineData(0)]
[InlineData(3)]
[InlineData(10)]
- public static void EndWriteMap_IndefiniteLength_EvenItems_ShouldThrowInvalidOperationException(int length)
+ public static void EndWriteMap_IndefiniteLength_OddItems_ShouldThrowInvalidOperationException(int length)
{
using var writer = new CborWriter();
writer.WriteStartMapIndefiniteLength();
// See the LICENSE file in the project root for more information.
#nullable enable
-using System.Text;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
namespace System.Security.Cryptography.Encoding.Tests.Cbor
{
internal partial class CborWriter
{
+ private KeyEncodingComparer? _comparer;
+
public void WriteStartMap(int definiteLength)
{
if (definiteLength < 0)
}
WriteUnsignedInteger(CborMajorType.Map, (ulong)definiteLength);
- AdvanceDataItemCounters();
PushDataItem(CborMajorType.Map, 2 * (uint)definiteLength);
}
public void WriteEndMap()
{
- if (!_isEvenNumberOfDataItemsWritten)
+ if (_currentValueOffset != null)
{
throw new InvalidOperationException("CBOR Map types require an even number of key/value combinations");
}
EnsureWriteCapacity(1);
_buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
}
+
+ AdvanceDataItemCounters();
}
public void WriteStartMapIndefiniteLength()
{
EnsureWriteCapacity(1);
WriteInitialByte(new CborInitialByte(CborMajorType.Map, CborAdditionalInfo.IndefiniteLength));
- AdvanceDataItemCounters();
PushDataItem(CborMajorType.Map, expectedNestedItems: null);
+ _currentKeyOffset = _offset;
+ _currentValueOffset = null;
+ }
+
+ //
+ // Map encoding conformance
+ //
+
+ private bool ConformanceRequiresSortedKeys()
+ {
+ return ConformanceLevel switch
+ {
+ CborConformanceLevel.Rfc7049Canonical => true,
+ CborConformanceLevel.Ctap2Canonical => true,
+ CborConformanceLevel.NoConformance => false,
+ _ => false,
+ };
+ }
+
+ private SortedSet<(int offset, int keyLength, int keyValueLength)> GetKeyValueEncodingRanges()
+ {
+ // TODO consider pooling set allocations?
+
+ if (_keyValueEncodingRanges == null)
+ {
+ _comparer ??= new KeyEncodingComparer(this);
+ return _keyValueEncodingRanges = new SortedSet<(int offset, int keyLength, int keyValueLength)>(_comparer);
+ }
+
+ return _keyValueEncodingRanges;
+ }
+
+ private void HandleKeyWritten()
+ {
+ Debug.Assert(_currentKeyOffset != null && _currentValueOffset == null);
+
+ _currentValueOffset = _offset;
+
+ if (ConformanceRequiresSortedKeys())
+ {
+ // check for key uniqueness
+ SortedSet<(int offset, int keyLength, int keyValueLength)> ranges = GetKeyValueEncodingRanges();
+
+ (int offset, int keyLength, int valueLength) currentKeyRange =
+ (_currentKeyOffset.Value,
+ _currentValueOffset.Value - _currentKeyOffset.Value,
+ 0);
+
+ if (ranges.Contains(currentKeyRange))
+ {
+ // TODO: check if rollback is necessary here
+ throw new InvalidOperationException("Duplicate key encoding in CBOR map.");
+ }
+ }
+ }
+
+ private void HandleValueWritten()
+ {
+ Debug.Assert(_currentKeyOffset != null && _currentValueOffset != null);
+
+ if (ConformanceRequiresSortedKeys())
+ {
+ Debug.Assert(_keyValueEncodingRanges != null);
+
+ (int offset, int keyLength, int keyValueLength) currentKeyRange =
+ (_currentKeyOffset.Value,
+ _currentValueOffset.Value - _currentKeyOffset.Value,
+ _offset - _currentKeyOffset.Value);
+
+ _keyValueEncodingRanges.Add(currentKeyRange);
+ }
+
+ // reset state
+ _currentKeyOffset = _offset;
+ _currentValueOffset = null;
+ }
+
+ private void SortKeyValuePairEncodings()
+ {
+ if (_keyValueEncodingRanges == null)
+ {
+ return;
+ }
+
+ int totalMapPayloadEncodingLength = _offset - _frameOffset;
+ byte[] tempBuffer = s_bufferPool.Rent(totalMapPayloadEncodingLength);
+ Span<byte> tmpSpan = tempBuffer.AsSpan(0, totalMapPayloadEncodingLength);
+
+ // copy sorted ranges to temporary buffer
+ Span<byte> s = tmpSpan;
+ foreach((int, int, int) range in _keyValueEncodingRanges)
+ {
+ ReadOnlySpan<byte> kvEnc = GetKeyValueEncoding(range);
+ kvEnc.CopyTo(s);
+ s = s.Slice(kvEnc.Length);
+ }
+ Debug.Assert(s.IsEmpty);
+
+ // now copy back to the original buffer segment
+ tmpSpan.CopyTo(_buffer.AsSpan(_frameOffset, totalMapPayloadEncodingLength));
+
+ s_bufferPool.Return(tempBuffer, clearArray: true);
+ }
+
+ private ReadOnlySpan<byte> GetKeyEncoding((int offset, int keyLength, int valueLength) keyValueRange)
+ {
+ return _buffer.AsSpan(keyValueRange.offset, keyValueRange.keyLength);
+ }
+
+ private ReadOnlySpan<byte> GetKeyValueEncoding((int offset, int keyLength, int keyValueLength) keyValueRange)
+ {
+ return _buffer.AsSpan(keyValueRange.offset, keyValueRange.keyValueLength);
+ }
+
+ private static int CompareEncodings(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right, CborConformanceLevel level)
+ {
+ Debug.Assert(!left.IsEmpty && !right.IsEmpty);
+
+ switch (level)
+ {
+ case CborConformanceLevel.Rfc7049Canonical:
+ // Implements key sorting according to
+ // https://tools.ietf.org/html/rfc7049#section-3.9
+
+ if (left.Length != right.Length)
+ {
+ return left.Length - right.Length;
+ }
+
+ return left.SequenceCompareTo(right);
+
+ case CborConformanceLevel.Ctap2Canonical:
+ // Implements key sorting according to
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding
+
+ int leftMt = (int)new CborInitialByte(left[0]).MajorType;
+ int rightMt = (int)new CborInitialByte(right[0]).MajorType;
+
+ if (leftMt != rightMt)
+ {
+ return leftMt - rightMt;
+ }
+
+ if (left.Length != right.Length)
+ {
+ return left.Length - right.Length;
+ }
+
+ return left.SequenceCompareTo(right);
+
+ default:
+ Debug.Fail("Invalid conformance level used in encoding sort.");
+ throw new Exception("Invalid conformance level used in encoding sort.");
+ }
+ }
+
+ private class KeyEncodingComparer : IComparer<(int, int, int)>
+ {
+ private readonly CborWriter _writer;
+
+ public KeyEncodingComparer(CborWriter writer)
+ {
+ _writer = writer;
+ }
+
+ public int Compare((int, int, int) x, (int, int, int) y)
+ {
+ return CompareEncodings(_writer.GetKeyEncoding(x), _writer.GetKeyEncoding(y), _writer.ConformanceLevel);
+ }
}
}
}
// with null representing indefinite length data items.
// The root context ony permits one data item to be written.
private uint? _remainingDataItems = 1;
- private bool _isEvenNumberOfDataItemsWritten = true; // required for indefinite-length map writes
- private Stack<(CborMajorType type, bool isEvenNumberOfDataItemsWritten, uint? remainingDataItems)>? _nestedDataItemStack;
+ private Stack<StackFrame>? _nestedDataItemStack;
+ private int _frameOffset = 0; // buffer offset particular to the current data item context
private bool _isTagContext = false; // true if writer is expecting a tagged value
- public CborWriter()
- {
+ // Map-specific bookkeeping
+ private int? _currentKeyOffset = null;
+ private int? _currentValueOffset = null;
+ private SortedSet<(int offset, int keyLength, int keyValueLength)>? _keyValueEncodingRanges = null;
+ public CborWriter(CborConformanceLevel conformanceLevel = CborConformanceLevel.NoConformance)
+ {
+ ConformanceLevel = conformanceLevel;
}
+ public CborConformanceLevel ConformanceLevel { get; }
public int BytesWritten => _offset;
// Returns true iff a complete CBOR document has been written to buffer
public bool IsWriteCompleted => _remainingDataItems == 0 && (_nestedDataItemStack?.Count ?? 0) == 0;
private void PushDataItem(CborMajorType type, uint? expectedNestedItems)
{
- _nestedDataItemStack ??= new Stack<(CborMajorType, bool, uint?)>();
- _nestedDataItemStack.Push((type, _isEvenNumberOfDataItemsWritten, _remainingDataItems));
+ _nestedDataItemStack ??= new Stack<StackFrame>();
+ _nestedDataItemStack.Push(new StackFrame(type, _frameOffset, _remainingDataItems, _currentKeyOffset, _currentValueOffset, _keyValueEncodingRanges));
+ _frameOffset = _offset;
_remainingDataItems = expectedNestedItems;
- _isEvenNumberOfDataItemsWritten = true;
+ _currentKeyOffset = (type == CborMajorType.Map) ? new int?(_offset) : null;
+ _currentValueOffset = null;
+ _keyValueEncodingRanges = null;
}
private void PopDataItem(CborMajorType expectedType)
throw new InvalidOperationException("No active CBOR nested data item to pop");
}
- (CborMajorType actualType, bool isEvenNumberOfDataItemsWritten, uint? remainingItems) = _nestedDataItemStack.Peek();
+ StackFrame frame = _nestedDataItemStack.Peek();
- if (expectedType != actualType)
+ if (expectedType != frame.MajorType)
{
throw new InvalidOperationException("Unexpected major type in nested CBOR data item.");
}
throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete.");
}
+ if (expectedType == CborMajorType.Map && ConformanceRequiresSortedKeys())
+ {
+ SortKeyValuePairEncodings();
+ }
+
_nestedDataItemStack.Pop();
- _remainingDataItems = remainingItems;
- _isEvenNumberOfDataItemsWritten = isEvenNumberOfDataItemsWritten;
+ _frameOffset = frame.FrameOffset;
+ _remainingDataItems = frame.RemainingDataItems;
+ _currentKeyOffset = frame.CurrentKeyOffset;
+ _currentValueOffset = frame.CurrentValueOffset;
+ _keyValueEncodingRanges = frame.KeyValueEncodingRanges;
}
private void AdvanceDataItemCounters()
{
_remainingDataItems--;
_isTagContext = false;
- _isEvenNumberOfDataItemsWritten = !_isEvenNumberOfDataItemsWritten;
+
+ if (_currentKeyOffset != null) // this is a map context
+ {
+ if (_currentValueOffset == null)
+ {
+ HandleKeyWritten();
+ }
+ else
+ {
+ HandleValueWritten();
+ }
+ }
}
private void WriteInitialByte(CborInitialByte initialByte)
if (_nestedDataItemStack != null && _nestedDataItemStack.Count > 0)
{
- CborMajorType parentType = _nestedDataItemStack.Peek().type;
+ CborMajorType parentType = _nestedDataItemStack.Peek().MajorType;
switch (parentType)
{
return (_offset == 0) ? Array.Empty<byte>() : _buffer.AsSpan(0, _offset).ToArray();
}
+
+ private readonly struct StackFrame
+ {
+ public StackFrame(CborMajorType type, int frameOffset, uint? remainingDataItems,
+ int? currentKeyOffset, int? currentValueOffset,
+ SortedSet<(int, int, int)>? keyValueEncodingRanges)
+ {
+ MajorType = type;
+ FrameOffset = frameOffset;
+ RemainingDataItems = remainingDataItems;
+ CurrentKeyOffset = currentKeyOffset;
+ CurrentValueOffset = currentValueOffset;
+ KeyValueEncodingRanges = keyValueEncodingRanges;
+ }
+
+ public CborMajorType MajorType { get; }
+ public int FrameOffset { get; }
+ public uint? RemainingDataItems { get; }
+
+ public int? CurrentKeyOffset { get; }
+ public int? CurrentValueOffset { get; }
+ public SortedSet<(int, int, int)>? KeyValueEncodingRanges { get; }
+ }
}
}