Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ The following additional configuration options are available in `AuthTokenValida
- `WithOcspRequestTimeout(TimeSpan ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
- `WithDisallowedCertificatePolicies(params string[] policies)` – adds the given policies to the list of disallowed user certificate policies. In order for the user certificate to be considered valid, it must not contain any policies present in this list. Contains the Estonian Mobile-ID policies by default as it must not be possible to authenticate with a Mobile-ID certificate when an eID smart card is expected.
- `WithNonceDisabledOcspUrls(params Uri[] urls)` – adds the given URLs to the list of OCSP URLs for which the nonce protocol extension will be disabled. Some OCSP services don't support the nonce extension.

- `WithAllowedOcspResponseTimeSkew(TimeSpan allowedTimeSkew)` - sets the allowed time skew for OCSP response's `thisUpdate` and `nextUpdate` times to allow discrepancies between the system clock and the OCSP responder's clock or revocation updates that are not published in real time. The default allowed time skew is 15 minutes. The relatively long default is specifically chosen to account for one particular OCSP responder that used CRLs for authoritative revocation info, these CRLs were updated every 15 minutes.
- `WithMaxOcspResponseThisUpdateAge(TimeSpan maxThisUpdateAge)` - sets the maximum age for the OCSP response's `thisUpdate` time before it is considered too old to rely on. The default maximum age is 2 minutes.
Extended configuration example:

```cs
Expand Down
5 changes: 4 additions & 1 deletion src/WebEid.Security.Tests/TestUtils/AuthTokenValidators.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright © 2020-2024 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down Expand Up @@ -62,6 +62,9 @@ public static IAuthTokenValidator GetAuthTokenValidatorWithDisallowedEsteidPolic
.WithDisallowedCertificatePolicies(EstIdemiaPolicy)
.Build();

public static AuthTokenValidatorBuilder GetDefaultAuthTokenValidatorBuilder() =>
GetAuthTokenValidatorBuilder(TokenOriginUrl, GetCaCertificates());

private static AuthTokenValidatorBuilder GetAuthTokenValidatorBuilder(string uri, X509Certificate2[] certificates) =>
new AuthTokenValidatorBuilder()
.WithSiteOrigin(new Uri(uri))
Expand Down
5 changes: 4 additions & 1 deletion src/WebEid.Security.Tests/TestUtils/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright © 2020-2024 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -22,12 +22,15 @@
namespace WebEid.Security.Tests.TestUtils
{
using System;
using Org.BouncyCastle.Asn1;

internal static class DateTimeExtensions
{
internal static DateTime TrimMilliseconds(this DateTime dt)
{
return dt.AddTicks(-dt.Ticks % TimeSpan.TicksPerSecond);
}

internal static DerGeneralizedTime ToDerGenTime(this DateTime dateTime) => new(dateTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void AuthTokenValidationConfigurationWithoutSiteOriginThrowsArgumentExcep
{
var configuration = new AuthTokenValidationConfiguration();
Assert.Throws<ArgumentNullException>(() => configuration.Validate())
.WithMessage("Value cannot be null. (Parameter 'siteOrigin')");
.WithMessage("Value cannot be null. (Parameter 'SiteOrigin')");
}

[Test]
Expand Down Expand Up @@ -72,7 +72,34 @@ public void AuthTokenValidationConfigurationWithZeroOcspRequestTimeoutThrowsArgu
configuration.TrustedCaCertificates.Add(new X509Certificate2(Array.Empty<byte>()));
configuration.OcspRequestTimeout = TimeSpan.Zero;
Assert.Throws<ArgumentOutOfRangeException>(() => configuration.Validate())
.WithMessage("OCSP request timeout must be greater than zero (Parameter 'ocspRequestTimeout')");
.WithMessage("OCSP request timeout must be greater than zero (Parameter 'timeSpan')");
}

[Test]
public void WhenInvalidOcspResponseTimeSkewThenValidationFails()
{
var builderWithInvalidOcspResponseTimeSkew =
AuthTokenValidators.GetDefaultAuthTokenValidatorBuilder().WithAllowedOcspResponseTimeSkew(TimeSpan.FromMinutes(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => builderWithInvalidOcspResponseTimeSkew.Build())
.HasMessageStartingWith("Allowed OCSP response time-skew must be greater than zero");
}

[Test]
public void WhenInvalidMaxOcspResponseThisUpdateAgeThenValidationFails()
{
var builderWithInvalidMaxOcspResponseThisUpdateAge =
AuthTokenValidators.GetDefaultAuthTokenValidatorBuilder().WithMaxOcspResponseThisUpdateAge(TimeSpan.Zero);
Assert.Throws<ArgumentOutOfRangeException>(() => builderWithInvalidMaxOcspResponseThisUpdateAge.Build())
.HasMessageStartingWith("Max OCSP response thisUpdate age must be greater than zero");
}

[Test]
public void WhenInvalidOcspResponseTimeoutThenValidationFails()
{
var builderWithInvalidOcspResponseTimeout =
AuthTokenValidators.GetDefaultAuthTokenValidatorBuilder().WithOcspRequestTimeout(TimeSpan.FromMinutes(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => builderWithInvalidOcspResponseTimeout.Build())
.HasMessageStartingWith("OCSP request timeout must be greater than zero");
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright © 2020-2024 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down Expand Up @@ -36,7 +36,7 @@ public class AuthTokenValidatorBuilderTest
[Test]
public void WhenOriginMissingThenBuildingFails() =>
Assert.Throws<ArgumentNullException>(() => this.builder.Build())
.WithMessage("Value cannot be null. (Parameter 'siteOrigin')");
.WithMessage("Value cannot be null. (Parameter 'SiteOrigin')");

[Test]
public void WhenRootCertificateAuthorityMissingThenBuildingFails()
Expand Down
119 changes: 85 additions & 34 deletions src/WebEid.Security.Tests/Validator/Ocsp/OcspResponseValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright © 2020-2024 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -22,57 +22,108 @@
namespace WebEid.Security.Tests.Validator.Ocsp
{
using System;
using Exceptions;
using NUnit.Framework;
using Org.BouncyCastle.Ocsp;
using Security.Validator.Ocsp;
using TestUtils;
using WebEid.Security.Validator;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Ocsp;
using System.Globalization;
using WebEid.Security.Exceptions;
using WebEid.Security.Tests.TestUtils;
using WebEid.Security.Util;
using System.Runtime.CompilerServices;

[TestFixture]
public class OcspResponseValidatorTests
{
private static TimeSpan timeSkew;
private static TimeSpan maxThisUpdateAge;

[SetUp]
public void SetUp()
{
var configuration = new AuthTokenValidationConfiguration();
timeSkew = configuration.AllowedOcspResponseTimeSkew;
maxThisUpdateAge = configuration.MaxOcspResponseThisUpdateAge;
}

[Test]
public void WhenThisAndNextUpdateWithinSkewThenValidationSucceeds()
{
var now = DateTimeProvider.UtcNow;
var thisUpdateWithinAgeLimit = GetThisUpdateWithinAgeLimit(now);
var nextUpdateWithinAgeLimit = now.Subtract(maxThisUpdateAge.Subtract(TimeSpan.FromSeconds(2)));

var mockedResponse = new SingleResp(new SingleResponse(null, null, thisUpdateWithinAgeLimit.ToDerGenTime(), nextUpdateWithinAgeLimit.ToDerGenTime(), null));

Assert.DoesNotThrow(() =>
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge));
}

[Test]
public void WhenNextUpdateBeforeThisUpdateThenThrows()
{
var now = DateTimeProvider.UtcNow;
var thisUpdateWithinAgeLimit = GetThisUpdateWithinAgeLimit(now);
var beforeThisUpdate = thisUpdateWithinAgeLimit.Subtract(TimeSpan.FromSeconds(1));

var mockedResponse = new SingleResp(new SingleResponse(null, null, thisUpdateWithinAgeLimit.ToDerGenTime(), beforeThisUpdate.ToDerGenTime(), null));

Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
.HasMessageStartingWith("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ $"nextUpdate {beforeThisUpdate.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is before thisUpdate {thisUpdateWithinAgeLimit.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)}");
}

[Test]
public void WhenThisUpdateDayBeforeProducedAtThenThrows()
public void WhenThisUpdateHalfHourBeforeNowThenThrows()
{
var thisUpdate = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
var producedAt = new DateTime(2021, 9, 2, 0, 0, 0, DateTimeKind.Utc);
var now = DateTimeProvider.UtcNow;
var halfHourBeforeNow = now.Subtract(TimeSpan.FromMinutes(30));
var mockedResponse = new SingleResp(new SingleResponse(null, null, halfHourBeforeNow.ToDerGenTime(), null, null));

Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
OcspResponseValidator.ValidateCertificateStatusUpdateTime(thisUpdate, null, producedAt))
.WithMessage("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ "notAllowedBefore: 2021-09-01 23:45:00 +00:00, "
+ "notAllowedAfter: 2021-09-02 00:15:00 +00:00, "
+ "thisUpdate: 2021-09-01 00:00:00 +00:00, "
+ "nextUpdate: null");
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
.HasMessageStartingWith("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ $"thisUpdate {halfHourBeforeNow.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is too old, minimum time allowed: ");
}

[Test]
public void WhenThisUpdateDayAfterProducedAtThenThrows()
public void WhenThisUpdateHalfHourAfterNowThenThrows()
{
var thisUpdate = new DateTime(2021, 9, 2, 0, 0, 0, DateTimeKind.Utc);
var producedAt = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
var now = DateTimeProvider.UtcNow;
var halfHourAfterNow = now.Add(TimeSpan.FromMinutes(30));
var mockedResponse = new SingleResp(new SingleResponse(null, null, halfHourAfterNow.ToDerGenTime(), null, null));

Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
OcspResponseValidator.ValidateCertificateStatusUpdateTime(thisUpdate, null, producedAt))
.WithMessage("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ "notAllowedBefore: 2021-08-31 23:45:00 +00:00, "
+ "notAllowedAfter: 2021-09-01 00:15:00 +00:00, "
+ "thisUpdate: 2021-09-02 00:00:00 +00:00, "
+ "nextUpdate: null");
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
.HasMessageStartingWith("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ $"thisUpdate {halfHourAfterNow.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is too far in the future, latest allowed: ");
}

[Test]
public void WhenNextUpdateDayBeforeProducedAtThenThrows()
public void WhenNextUpdateHalfHourBeforeNowThenThrows()
{
var thisUpdate = new DateTime(2021, 9, 2, 0, 0, 0, DateTimeKind.Utc);
var nextUpdate = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
var producedAt = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
var now = DateTimeProvider.UtcNow;
var thisUpdateWithinAgeLimit = GetThisUpdateWithinAgeLimit(now);
var halfHourBeforeNow = now.Subtract(TimeSpan.FromMinutes(30));
var mockedResponse = new SingleResp(new SingleResponse(null, null, thisUpdateWithinAgeLimit.ToDerGenTime(), halfHourBeforeNow.ToDerGenTime(), null));

Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
OcspResponseValidator.ValidateCertificateStatusUpdateTime(thisUpdate, nextUpdate, producedAt))
.WithMessage("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ "notAllowedBefore: 2021-08-31 23:45:00 +00:00, "
+ "notAllowedAfter: 2021-09-01 00:15:00 +00:00, "
+ "thisUpdate: 2021-09-02 00:00:00 +00:00, "
+ "nextUpdate: 2021-09-01 00:00:00 +00:00");
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
.HasMessageStartingWith("User certificate revocation check has failed: "
+ "Certificate status update time check failed: "
+ $"nextUpdate {halfHourBeforeNow.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is in the past");
}

private static DateTime GetThisUpdateWithinAgeLimit(DateTime now)
{
var maxThisUpdateAgeMinusOne = maxThisUpdateAge.Subtract(TimeSpan.FromSeconds(1));
return now.Subtract(maxThisUpdateAgeMinusOne);
}
}
}
Loading