Skip to content

Commit 432c2e4

Browse files
authored
Change ValidateDer to iterative evaluation (#128874)
Since this change doesn't eagerly dive into CONSTRUCTED values, it will change when it encounters a value that gets rejected, but since it doesn't report where the invalid value was found, that doesn't matter.
1 parent ac69569 commit 432c2e4

2 files changed

Lines changed: 144 additions & 31 deletions

File tree

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Buffers;
56
using System.Collections.Generic;
67
using System.Diagnostics;
78
using System.Diagnostics.CodeAnalysis;
@@ -272,54 +273,84 @@ internal static void DisposeAll(this IEnumerable<IDisposable> disposables)
272273
}
273274
}
274275

275-
internal static void ValidateDer(ReadOnlySpan<byte> encodedValue)
276+
internal static unsafe void ValidateDer(ReadOnlySpan<byte> encodedValue)
276277
{
277278
try
278279
{
279-
Asn1Tag tag;
280-
ValueAsnReader reader = new ValueAsnReader(encodedValue, AsnEncodingRules.DER);
280+
const int StackQueueSize = 16;
281+
Span<(int Offset, int Length)> stack = stackalloc (int, int)[StackQueueSize];
282+
int stackCount = 0;
281283

282-
while (reader.HasData)
284+
stack[stackCount++] = (0, encodedValue.Length);
285+
286+
do
283287
{
284-
tag = reader.PeekTag();
288+
(int offset, int length) = stack[--stackCount];
289+
290+
ValueAsnReader reader = new ValueAsnReader(
291+
encodedValue.Slice(offset, length),
292+
AsnEncodingRules.DER);
285293

286-
// If the tag is in the UNIVERSAL class
287-
//
288-
// DER limits the constructed encoding to SEQUENCE and SET, as well as anything which gets
289-
// a defined encoding as being an IMPLICIT SEQUENCE.
290-
if (tag.TagClass == TagClass.Universal)
294+
while (reader.HasData)
291295
{
292-
switch ((UniversalTagNumber)tag.TagValue)
296+
Asn1Tag tag = reader.PeekTag();
297+
298+
// If the tag is in the UNIVERSAL class
299+
//
300+
// DER limits the constructed encoding to SEQUENCE and SET, as well as anything which gets
301+
// a defined encoding as being an IMPLICIT SEQUENCE.
302+
if (tag.TagClass == TagClass.Universal)
303+
{
304+
switch ((UniversalTagNumber)tag.TagValue)
305+
{
306+
case UniversalTagNumber.External:
307+
case UniversalTagNumber.Embedded:
308+
case UniversalTagNumber.Sequence:
309+
case UniversalTagNumber.Set:
310+
case UniversalTagNumber.UnrestrictedCharacterString:
311+
if (!tag.IsConstructed)
312+
{
313+
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
314+
}
315+
316+
break;
317+
default:
318+
if (tag.IsConstructed)
319+
{
320+
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
321+
}
322+
323+
break;
324+
}
325+
}
326+
327+
if (tag.IsConstructed)
293328
{
294-
case UniversalTagNumber.External:
295-
case UniversalTagNumber.Embedded:
296-
case UniversalTagNumber.Sequence:
297-
case UniversalTagNumber.Set:
298-
case UniversalTagNumber.UnrestrictedCharacterString:
299-
if (!tag.IsConstructed)
329+
ReadOnlySpan<byte> content = reader.PeekContentBytes();
330+
331+
if (content.Length > 0)
332+
{
333+
if (!encodedValue.Overlaps(content, out int contentOffset))
300334
{
301-
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
335+
Debug.Fail("Contents do not overlap original span");
336+
throw new CryptographicException();
302337
}
303338

304-
break;
305-
default:
306-
if (tag.IsConstructed)
339+
if (stackCount == stack.Length)
307340
{
308-
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
341+
Span<(int, int)> nextStack = new (int, int)[stack.Length * 2];
342+
stack.CopyTo(nextStack);
343+
stack = nextStack;
309344
}
310345

311-
break;
346+
stack[stackCount++] = (contentOffset, content.Length);
347+
}
312348
}
313-
}
314349

315-
if (tag.IsConstructed)
316-
{
317-
ValidateDer(reader.PeekContentBytes());
350+
// Skip past the current value.
351+
reader.ReadEncodedValue();
318352
}
319-
320-
// Skip past the current value.
321-
reader.ReadEncodedValue();
322-
}
353+
} while (stackCount > 0);
323354
}
324355
catch (AsnContentException e)
325356
{

src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestApiTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Formats.Asn1;
45
using System.Security.Cryptography.SLHDsa.Tests;
56
using System.Security.Cryptography.Tests;
67
using Xunit;
@@ -513,6 +514,87 @@ public static void InvalidDerInAttribute()
513514
}
514515
}
515516

517+
[Theory]
518+
[InlineData(false)]
519+
[InlineData(true)]
520+
public static void DeeplyNestedOtherRequestAttribute(bool invalid)
521+
{
522+
byte[] encoded = BuildAttributeValue();
523+
524+
if (invalid)
525+
{
526+
encoded[^2] = 0x10;
527+
}
528+
529+
using (ECDsa key = ECDsa.Create(EccTestData.Secp384r1Data.KeyParameters))
530+
{
531+
CertificateRequest req = new CertificateRequest(
532+
"CN=Test",
533+
key,
534+
HashAlgorithmName.SHA384);
535+
536+
req.OtherRequestAttributes.Add(
537+
new AsnEncodedData(
538+
new Oid("1.2.840.113549.1.9.7", null),
539+
encoded));
540+
541+
X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key);
542+
543+
if (invalid)
544+
{
545+
Assert.Throws<CryptographicException>(() => req.CreateSigningRequest());
546+
Assert.Throws<CryptographicException>(() => req.CreateSigningRequest(gen));
547+
}
548+
else
549+
{
550+
req.CreateSigningRequest();
551+
req.CreateSigningRequest(gen);
552+
}
553+
}
554+
555+
static byte[] BuildAttributeValue()
556+
{
557+
const int TargetDepth = 7500;
558+
559+
// SEQUENCE-OF
560+
// SEQUENCE-OF x7500 deep
561+
// BOOLEAN FALSE
562+
// SEQUENCE-OF x7500 deep
563+
564+
// Each of the two really deep values would cause a StackOverflow, use two of them to ensure coverage
565+
// of the deep validator queue reuse.
566+
567+
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER, initialCapacity: 64 * 1024);
568+
569+
writer.PushSequence();
570+
571+
for (int i = 0; i < TargetDepth; i++)
572+
{
573+
writer.PushSequence();
574+
}
575+
576+
writer.WriteBoolean(false);
577+
578+
for (int i = 0; i < TargetDepth; i++)
579+
{
580+
writer.PopSequence();
581+
}
582+
583+
for (int i = 0; i < TargetDepth; i++)
584+
{
585+
writer.PushSequence();
586+
}
587+
588+
for (int i = 0; i < TargetDepth; i++)
589+
{
590+
writer.PopSequence();
591+
}
592+
593+
writer.PopSequence();
594+
return writer.Encode();
595+
}
596+
}
597+
516598
[Fact]
517599
public static void LoadNullArray()
518600
{

0 commit comments

Comments
 (0)