|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
3 | 3 | using System.IO; |
| 4 | +using System.Linq; |
4 | 5 | using System.Numerics; |
5 | 6 | using System.Security.Cryptography; |
6 | 7 | using System.Text; |
@@ -176,20 +177,23 @@ public Asn1Builder AddSequence(ReadOnlySpan<Byte> value) { |
176 | 177 | return this; |
177 | 178 | } |
178 | 179 | /// <summary> |
179 | | - /// Adds ASN.1 SET value. |
| 180 | + /// Adds ASN.1 SET value with proper DER canonical sorting. |
180 | 181 | /// </summary> |
181 | 182 | /// <param name="value"> |
182 | | - /// Value to encode. |
| 183 | + /// Value to encode. This should be the concatenated raw content (without SET tag/length wrapper). |
183 | 184 | /// </param> |
184 | 185 | /// <exception cref="ArgumentNullException"> |
185 | 186 | /// <strong>value</strong> parameter is null. |
186 | 187 | /// </exception> |
187 | 188 | /// <returns>Current instance with added value.</returns> |
188 | 189 | /// <remarks> |
189 | 190 | /// 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). |
190 | 193 | /// </remarks> |
191 | 194 | 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)); |
193 | 197 |
|
194 | 198 | return this; |
195 | 199 | } |
@@ -533,19 +537,24 @@ public Asn1Builder AddSequence(Func<Asn1Builder, Asn1Builder> selector) { |
533 | 537 | return this; |
534 | 538 | } |
535 | 539 | /// <summary> |
536 | | - /// Adds constructed SET. |
| 540 | + /// Adds constructed SET with proper DER canonical sorting. |
537 | 541 | /// </summary> |
538 | 542 | /// <param name="selector">Lambda expression to fill nested content.</param> |
539 | 543 | /// <exception cref="ArgumentNullException"> |
540 | 544 | /// <strong>selector</strong> parameter is null. |
541 | 545 | /// </exception> |
542 | 546 | /// <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> |
543 | 551 | public Asn1Builder AddSet(Func<Asn1Builder, Asn1Builder> selector) { |
544 | 552 | if (selector is null) { |
545 | 553 | throw new ArgumentNullException(nameof(selector)); |
546 | 554 | } |
547 | 555 | 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)); |
549 | 558 | return this; |
550 | 559 | } |
551 | 560 | /// <summary> |
@@ -663,6 +672,64 @@ public ReadOnlyMemory<Byte> GetRawDataAsMemory() { |
663 | 672 | return getEncoded(0, false); |
664 | 673 | } |
665 | 674 |
|
| 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 | + |
666 | 733 | /// <summary> |
667 | 734 | /// Creates a default instance of <strong>Asn1Builder</strong> class. |
668 | 735 | /// </summary> |
|
0 commit comments