Skip to content

Commit 08bd9d8

Browse files
committed
Implemented SET lexicographical sorting for DER and updated unit tests
1 parent 1370054 commit 08bd9d8

2 files changed

Lines changed: 83 additions & 14 deletions

File tree

Asn1Parser/Asn1Builder.cs

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Numerics;
56
using System.Security.Cryptography;
67
using System.Text;
@@ -176,20 +177,23 @@ public Asn1Builder AddSequence(ReadOnlySpan<Byte> value) {
176177
return this;
177178
}
178179
/// <summary>
179-
/// Adds ASN.1 SET value.
180+
/// Adds ASN.1 SET value with proper DER canonical sorting.
180181
/// </summary>
181182
/// <param name="value">
182-
/// Value to encode.
183+
/// Value to encode. This should be the concatenated raw content (without SET tag/length wrapper).
183184
/// </param>
184185
/// <exception cref="ArgumentNullException">
185186
/// <strong>value</strong> parameter is null.
186187
/// </exception>
187188
/// <returns>Current instance with added value.</returns>
188189
/// <remarks>
189190
/// In the current implementation, SET is encoded using constructed form only.
191+
/// According to DER (X.690), SET and SET OF elements are sorted in ascending order
192+
/// by their complete encoded representation (lexicographic ordering of DER encodings).
190193
/// </remarks>
191194
public Asn1Builder AddSet(ReadOnlySpan<Byte> value) {
192-
_rawData.Add(Asn1Utils.Encode(value, 0x31));
195+
ReadOnlyMemory<Byte> sortedElements = sortSetElements(new ReadOnlyMemory<Byte>(value.ToArray()));
196+
_rawData.Add(Asn1Utils.Encode(sortedElements.Span, 0x31));
193197

194198
return this;
195199
}
@@ -533,19 +537,24 @@ public Asn1Builder AddSequence(Func<Asn1Builder, Asn1Builder> selector) {
533537
return this;
534538
}
535539
/// <summary>
536-
/// Adds constructed SET.
540+
/// Adds constructed SET with proper DER canonical sorting.
537541
/// </summary>
538542
/// <param name="selector">Lambda expression to fill nested content.</param>
539543
/// <exception cref="ArgumentNullException">
540544
/// <strong>selector</strong> parameter is null.
541545
/// </exception>
542546
/// <returns>Current instance with added value.</returns>
547+
/// <remarks>
548+
/// According to DER (X.690), SET and SET OF elements are sorted in ascending order
549+
/// by their complete encoded representation (lexicographic ordering of DER encodings).
550+
/// </remarks>
543551
public Asn1Builder AddSet(Func<Asn1Builder, Asn1Builder> selector) {
544552
if (selector is null) {
545553
throw new ArgumentNullException(nameof(selector));
546554
}
547555
Asn1Builder b = selector(new Asn1Builder());
548-
_rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, 0x31));
556+
ReadOnlyMemory<Byte> sortedElements = sortSetElements(b.GetRawDataAsMemory());
557+
_rawData.Add(Asn1Utils.Encode(sortedElements.Span, 0x31));
549558
return this;
550559
}
551560
/// <summary>
@@ -663,6 +672,64 @@ public ReadOnlyMemory<Byte> GetRawDataAsMemory() {
663672
return getEncoded(0, false);
664673
}
665674

675+
// Helper method to sort SET elements according to DER canonical ordering
676+
static ReadOnlyMemory<Byte> sortSetElements(ReadOnlyMemory<Byte> rawElements) {
677+
if (rawElements.Length == 0) {
678+
return rawElements;
679+
}
680+
681+
// rawElements is unencoded content (concatenated children without TL wrapper)
682+
// Temporarily wrap in SEQUENCE to parse individual elements
683+
ReadOnlyMemory<Byte> tempSequence = Asn1Utils.Encode(rawElements.Span, 0x30);
684+
685+
var elements = new List<ReadOnlyMemory<Byte>>();
686+
var reader = new Asn1Reader(tempSequence);
687+
688+
if (!reader.IsConstructed || reader.GetNestedNodeCount() == 0) {
689+
// No children to sort
690+
return rawElements;
691+
}
692+
693+
// Move into the SEQUENCE to access its children
694+
if (!reader.MoveNext()) {
695+
return rawElements;
696+
}
697+
698+
// Collect all immediate child elements using MoveNextSibling
699+
elements.Add(reader.GetTagRawDataAsMemory());
700+
while (reader.MoveNextSibling()) {
701+
elements.Add(reader.GetTagRawDataAsMemory());
702+
}
703+
704+
// Sort elements lexicographically by their DER encoding
705+
elements.Sort((a, b) => {
706+
Int32 minLength = Math.Min(a.Length, b.Length);
707+
ReadOnlySpan<Byte> spanA = a.Span;
708+
ReadOnlySpan<Byte> spanB = b.Span;
709+
710+
for (Int32 i = 0; i < minLength; i++) {
711+
Int32 cmp = spanA[i].CompareTo(spanB[i]);
712+
if (cmp != 0) {
713+
return cmp;
714+
}
715+
}
716+
// If all bytes are equal up to minLength, shorter comes first
717+
return a.Length.CompareTo(b.Length);
718+
});
719+
720+
// Concatenate sorted elements
721+
Int32 totalLength = elements.Sum(element => element.Length);
722+
723+
Byte[] result = new Byte[totalLength];
724+
Int32 offset = 0;
725+
foreach (ReadOnlyMemory<Byte> element in elements) {
726+
element.Span.CopyTo(result.AsSpan(offset));
727+
offset += element.Length;
728+
}
729+
730+
return result;
731+
}
732+
666733
/// <summary>
667734
/// Creates a default instance of <strong>Asn1Builder</strong> class.
668735
/// </summary>

tests/Asn1Parser.Tests/Builder/Asn1BuilderTests.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ static Asn1Builder BuildTestSimple() {
3232
.AddRelativeOid(".5")
3333
.AddSequence(builder => builder.AddInteger(1).AddInteger(2))
3434
.AddSequence(new Asn1Integer(5).GetRawDataAsMemory().Span)
35-
.AddSet(builder => builder.AddInteger(1).AddInteger(2))
35+
// in KAT data, SET is sorted, i.e. 1 is after 2. But we will add them in the order of 1, 2,
36+
// and expect that builder will sort them correctly.
37+
.AddSet(builder => builder.AddInteger(2).AddInteger(1))
3638
.AddSet(new Asn1Integer(5).GetRawDataAsMemory().Span)
3739
.AddNumericString("555 555")
3840
.AddPrintableString("PrintableString")
@@ -72,7 +74,7 @@ public void TestBuilderSimple() {
7274
Asn1BuilderTestBase.AssertNull(reader, useSibling: true);
7375
Asn1BuilderTestBase.AssertObjectIdentifier(reader, "1.3.6.1.5.5.7.3.1");
7476
Asn1BuilderTestBase.AssertEnumerated(reader, 3);
75-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.UTF8String, Asn1Type.UTF8String.ToString());
77+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.UTF8String, nameof(Asn1Type.UTF8String));
7678
Asn1BuilderTestBase.AssertRelativeOid(reader, ".5");
7779
Asn1BuilderTestBase.AssertSequence(reader);
7880
nestedReader = reader.GetReader();
@@ -89,14 +91,14 @@ public void TestBuilderSimple() {
8991
nestedReader = reader.GetReader();
9092
Asn1BuilderTestBase.AssertInteger(nestedReader, 5);
9193
Asn1BuilderTestBase.AssertString(reader, Asn1Type.NumericString, "555 555", useSibling: true);
92-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.PrintableString, Asn1Type.PrintableString.ToString());
93-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.TeletexString, Asn1Type.TeletexString.ToString());
94-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.VideotexString, Asn1Type.VideotexString.ToString());
95-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.IA5String, Asn1Type.IA5String.ToString());
94+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.PrintableString, nameof(Asn1Type.PrintableString));
95+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.TeletexString, nameof(Asn1Type.TeletexString));
96+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.VideotexString, nameof(Asn1Type.VideotexString));
97+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.IA5String, nameof(Asn1Type.IA5String));
9698
Asn1BuilderTestBase.AssertDateTime(reader, DateTime.Parse("2025-01-01 20:00:00"), true);
9799
Asn1BuilderTestBase.AssertDateTime(reader, DateTime.Parse("2025-01-01 20:00:00"), false);
98-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.VisibleString, Asn1Type.VisibleString.ToString());
99-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.UniversalString, Asn1Type.UniversalString.ToString());
100-
Asn1BuilderTestBase.AssertString(reader, Asn1Type.BMPString, Asn1Type.BMPString.ToString());
100+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.VisibleString, nameof(Asn1Type.VisibleString));
101+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.UniversalString, nameof(Asn1Type.UniversalString));
102+
Asn1BuilderTestBase.AssertString(reader, Asn1Type.BMPString, nameof(Asn1Type.BMPString));
101103
}
102104
}

0 commit comments

Comments
 (0)