implement writing sorted keys in maps
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Tue, 21 Apr 2020 12:57:02 +0000 (13:57 +0100)
committerEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 29 Apr 2020 14:30:06 +0000 (15:30 +0100)
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs

index a23ddcb..915ff78 100644 (file)
@@ -86,6 +86,82 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [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)
         {
@@ -97,6 +173,22 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [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)]
@@ -176,7 +268,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [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();
index 185097e..40a8432 100644 (file)
@@ -6,6 +6,13 @@ using System.Diagnostics;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
 {
+    internal enum CborConformanceLevel
+    {
+        NoConformance = 0,
+        Rfc7049Canonical = 1,
+        Ctap2Canonical = 2,
+    }
+
     internal enum CborMajorType : byte
     {
         UnsignedInteger = 0,
index 39b54f2..4ef072b 100644 (file)
@@ -18,7 +18,6 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
 
             WriteUnsignedInteger(CborMajorType.Array, (ulong)definiteLength);
-            AdvanceDataItemCounters();
             PushDataItem(CborMajorType.Array, (uint)definiteLength);
         }
 
@@ -33,13 +32,14 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 EnsureWriteCapacity(1);
                 _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
             }
+
+            AdvanceDataItemCounters();
         }
 
         public void WriteStartArrayIndefiniteLength()
         {
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.Array, CborAdditionalInfo.IndefiniteLength));
-            AdvanceDataItemCounters();
             PushDataItem(CborMajorType.Array, expectedNestedItems: null);
         }
     }
index 2ebe219..fe80513 100644 (file)
@@ -3,12 +3,16 @@
 // 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)
@@ -17,13 +21,12 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
 
             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");
             }
@@ -38,14 +41,184 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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);
+            }
         }
     }
 }
index 733b281..2a36f37 100644 (file)
@@ -46,7 +46,6 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.ByteString, CborAdditionalInfo.IndefiniteLength));
-            AdvanceDataItemCounters();
             PushDataItem(CborMajorType.ByteString, expectedNestedItems: null);
         }
 
@@ -56,13 +55,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             // append break byte
             EnsureWriteCapacity(1);
             _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
+            AdvanceDataItemCounters();
         }
 
         public void WriteStartTextStringIndefiniteLength()
         {
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.TextString, CborAdditionalInfo.IndefiniteLength));
-            AdvanceDataItemCounters();
             PushDataItem(CborMajorType.TextString, expectedNestedItems: null);
         }
 
@@ -72,6 +71,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             // append break byte
             EnsureWriteCapacity(1);
             _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
+            AdvanceDataItemCounters();
         }
     }
 }
index ff2d81f..2fd2e50 100644 (file)
@@ -21,15 +21,21 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // 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;
@@ -105,10 +111,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         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)
@@ -118,9 +127,9 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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.");
             }
@@ -135,16 +144,35 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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)
@@ -156,7 +184,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             if (_nestedDataItemStack != null && _nestedDataItemStack.Count > 0)
             {
-                CborMajorType parentType = _nestedDataItemStack.Peek().type;
+                CborMajorType parentType = _nestedDataItemStack.Peek().MajorType;
 
                 switch (parentType)
                 {
@@ -206,5 +234,28 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             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; }
+        }
     }
 }