Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow outdated and cert chain bug fixes #109

Merged
merged 5 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 47 additions & 18 deletions CoseHandler/CoseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,18 @@ payloadStream is not null ?
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
/// <exception cref="CoseValidationException">The exception thrown if validation failed</exception>
public static ValidationResult Validate(
byte[] signature,
byte[]? payload,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null,
bool allowUntrusted = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted), payload);
bool allowUntrusted = false,
bool allowOutdated = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated), payload);

/// <summary>
/// Validates a detached COSE signature in memory.
Expand All @@ -348,14 +351,17 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
public static ValidationResult Validate(
byte[] signature,
Stream payload,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null,
bool allowUntrusted = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted), payload);
bool allowUntrusted = false,
bool allowOutdated = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated), payload);

/// <summary>
/// Validates a detached or embedded COSE signature in memory.
Expand All @@ -365,14 +371,17 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
public static ValidationResult Validate(
Stream signature,
byte[]? payload,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null,
bool allowUntrusted = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted), payload);
bool allowUntrusted = false,
bool allowOutdated = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated), payload);

/// <summary>
/// Validates a COSE signature file.
Expand All @@ -382,16 +391,19 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
public static ValidationResult Validate(
FileInfo signature,
FileInfo? payload,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null,
bool allowUntrusted = false)
bool allowUntrusted = false,
bool allowOutdated = false)
=> ValidateInternal(signatureBytes: null, signatureStream: null, signatureFile: signature,
payloadBytes: null, payloadStream: null, payloadFile: payload, out _,
GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted));
GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated));

/// <summary>
/// Validates a detached COSE signature in memory.
Expand All @@ -401,14 +413,17 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
public static ValidationResult Validate(
Stream signature,
Stream? payload,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null,
bool allowUntrusted = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted), payload);
bool allowUntrusted = false,
bool allowOutdated = false)
=> Validate(signature, GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated), payload);

/// <summary>
/// Validates a detached or embedded COSE signature in memory.
Expand Down Expand Up @@ -484,15 +499,19 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to during validation, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
/// <returns>The decoded payload as a string.</returns>
public static string? GetPayload(
byte[] signature,
out ValidationResult result,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null)
string? requiredCommonName = null,
bool allowUntrusted = false,
bool allowOutdated = false)
=> GetPayload(signature,
GetValidator(roots, revocationMode, requiredCommonName),
GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated),
out result);

/// <summary>
Expand All @@ -503,15 +522,19 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to during validation, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
/// <returns>The decoded payload as a string.</returns>
public static string? GetPayload(
Stream signature,
out ValidationResult result,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null)
string? requiredCommonName = null,
bool allowUntrusted = false,
bool allowOutdated = false)
=> GetPayload(signature,
GetValidator(roots, revocationMode, requiredCommonName),
GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated),
out result);

/// <summary>
Expand All @@ -522,15 +545,19 @@ public static ValidationResult Validate(
/// <param name="roots">Optional. A set of root certificates to try to chain the signing certificate to during validation, in addition to the certificates installed on the host machine.</param>
/// <param name="revocationMode">Optional. Revocation mode to use when validating the certificate chain.</param>
/// <param name="requiredCommonName">Optional. Requires the signing certificate to match the specified Common Name.</param>
/// <param name="allowUntrusted">True to allow untrusted certificates.</param>
/// <param name="allowOutdated">True to allow signatures with expired certificates to pass validation unless the expired certificate has a lifetime EKU.</param>
/// <returns>The decoded payload as a string.</returns>
public static string? GetPayload(
FileInfo signature,
out ValidationResult result,
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null)
string? requiredCommonName = null,
bool allowUntrusted = false,
bool allowOutdated = false)
=> GetPayloadInternal(signatureBytes: null, signatureStream: null, signatureFile: signature,
GetValidator(roots, revocationMode, requiredCommonName),
GetValidator(roots, revocationMode, requiredCommonName, allowUntrusted, allowOutdated),
out result);

/// <summary>
Expand Down Expand Up @@ -792,14 +819,16 @@ internal static CoseSign1MessageValidator GetValidator(
List<X509Certificate2>? roots = null,
X509RevocationMode revocationMode = X509RevocationMode.Online,
string? requiredCommonName = null,
bool allowUntrusted = false)
bool allowUntrusted = false,
bool allowOutdated = false)
{
// Create a validator for the certificate trust chain.
CoseSign1MessageValidator chainTrustValidator = new X509ChainTrustValidator(
roots,
revocationMode,
allowUnprotected: true,
allowUntrusted: allowUntrusted);
allowUntrusted: allowUntrusted,
allowOutdated: allowOutdated);

// If validating CommonName, we'll do that first, and set it to call for chain trust validation when it finishes.
if (!string.IsNullOrWhiteSpace(requiredCommonName))
Expand Down
7 changes: 4 additions & 3 deletions CoseSign1.Certificates.Tests/X509ChainTrustValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,10 @@
CoseSign1Message message = CreateCoseSign1MessageWithChainedCert();

// Mock the ChainBuilder to always return success
Mock<ICertificateChainBuilder> mockBuilder = new(MockBehavior.Strict);
Mock<ICertificateChainBuilder> mockBuilder = new();
mockBuilder.Setup(x => x.Build(It.IsAny<X509Certificate2>())).Returns(true);
mockBuilder.Setup(x => x.ChainElements).Returns([.. DefaultTestChain]);
mockBuilder.Setup(x => x.ChainPolicy).Returns(new X509ChainPolicy());

// Validate
X509ChainTrustValidator Validator = new(mockBuilder.Object);
Expand Down Expand Up @@ -302,7 +303,7 @@
results[0].Includes.Should().NotBeNull();
results[0].Includes?.Count.Should().Be(1);
X509ChainStatus? status = results[0].Includes?.Cast<X509ChainStatus>().FirstOrDefault();
status.Value.Status.Should().Be(X509ChainStatusFlags.PartialChain);
status.Value.Status.Should().Be(X509ChainStatusFlags.UntrustedRoot);
Dismissed Show dismissed Hide dismissed
}

// Prove that an untrusted, self-signed cert passes only when the same cert is passed as a root or AllowUntrusted is ON
Expand Down Expand Up @@ -396,7 +397,7 @@
results[0].Includes.Should().NotBeNull();
results[0].Includes?.Count.Should().Be(1);
X509ChainStatus? status = results[0].Includes?.Cast<X509ChainStatus>().FirstOrDefault();
status.Value.Status.Should().Be(X509ChainStatusFlags.PartialChain);
status.Value.Status.Should().Be(X509ChainStatusFlags.UntrustedRoot);
Dismissed Show dismissed Hide dismissed

// Revoked cert case //
Mock<ICertificateChainBuilder> builder = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ protected override CoseSign1ValidationResult ValidateCertificate(
List<X509Certificate2>? certChain,
List<X509Certificate2>? extraCertificates)
{

// If there are user-supplied roots, add them to the ExtraCerts collection.
bool hasRoots = false;
if (Roots?.Count > 0)
Expand All @@ -111,6 +110,16 @@ protected override CoseSign1ValidationResult ValidateCertificate(
Roots.ForEach(c => ChainBuilder.ChainPolicy.ExtraStore.Add(c));
}

if (certChain?.Count > 0)
{
ChainBuilder.ChainPolicy.ExtraStore.AddRange(certChain.ToArray());
}

if (extraCertificates?.Count > 0)
{
ChainBuilder.ChainPolicy.ExtraStore.AddRange(extraCertificates.ToArray());
}

// Build the cert chain. If Build succeeds, return success.
if (ChainBuilder.Build(signingCertificate))
{
Expand Down
5 changes: 3 additions & 2 deletions CoseSign1.Tests.Common/TestCertificateUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ public static X509Certificate2Collection CreateTestChain(
[CallerMemberName] string? testName = "none",
bool useEcc = false,
int? keySize = null,
bool leafFirst = false)
bool leafFirst = false,
TimeSpan? rootDuration = null)
{
X509Certificate2 testRoot = CreateCertificate($"Test Root: {testName}", useEcc: useEcc, keySize: keySize);
X509Certificate2 testRoot = CreateCertificate($"Test Root: {testName}", useEcc: useEcc, keySize: keySize, duration: rootDuration);
X509Certificate2 issuer = CreateCertificate($"Test Issuer: {testName}", testRoot, useEcc: useEcc, keySize: keySize);
X509Certificate2 leaf = CreateCertificate($"Test Leaf: {testName}", issuer, useEcc: useEcc, keySize: keySize);

Expand Down
Loading
Loading