Skip to content

Commit 085e75e

Browse files
Mihkel Kivisildmrts
authored andcommitted
fix: use system time in OcspResponseValidator.validateCertificateStatusUpdateTime()
WE2-868 Signed-off-by: Mihkel Kivisild [email protected]
1 parent c2c7a00 commit 085e75e

12 files changed

+258
-91
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ The following additional configuration options are available in `AuthTokenValida
363363
- `WithOcspRequestTimeout(TimeSpan ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
364364
- `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.
365365
- `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.
366-
366+
- `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.
367+
- `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.
367368
Extended configuration example:
368369

369370
```cs

src/WebEid.Security.Tests/TestUtils/AuthTokenValidators.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright © 2020-2024 Estonian Information System Authority
33
*
44
* Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -62,6 +62,9 @@ public static IAuthTokenValidator GetAuthTokenValidatorWithDisallowedEsteidPolic
6262
.WithDisallowedCertificatePolicies(EstIdemiaPolicy)
6363
.Build();
6464

65+
public static AuthTokenValidatorBuilder GetDefaultAuthTokenValidatorBuilder() =>
66+
GetAuthTokenValidatorBuilder(TokenOriginUrl, GetCaCertificates());
67+
6568
private static AuthTokenValidatorBuilder GetAuthTokenValidatorBuilder(string uri, X509Certificate2[] certificates) =>
6669
new AuthTokenValidatorBuilder()
6770
.WithSiteOrigin(new Uri(uri))

src/WebEid.Security.Tests/TestUtils/DateTimeExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright © 2020-2024 Estonian Information System Authority
33
*
44
* Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,12 +22,15 @@
2222
namespace WebEid.Security.Tests.TestUtils
2323
{
2424
using System;
25+
using Org.BouncyCastle.Asn1;
2526

2627
internal static class DateTimeExtensions
2728
{
2829
internal static DateTime TrimMilliseconds(this DateTime dt)
2930
{
3031
return dt.AddTicks(-dt.Ticks % TimeSpan.TicksPerSecond);
3132
}
33+
34+
internal static DerGeneralizedTime ToDerGenTime(this DateTime dateTime) => new(dateTime);
3235
}
3336
}

src/WebEid.Security.Tests/Validator/AuthTokenValidationConfigurationTests.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void AuthTokenValidationConfigurationWithoutSiteOriginThrowsArgumentExcep
3939
{
4040
var configuration = new AuthTokenValidationConfiguration();
4141
Assert.Throws<ArgumentNullException>(() => configuration.Validate())
42-
.WithMessage("Value cannot be null. (Parameter 'siteOrigin')");
42+
.WithMessage("Value cannot be null. (Parameter 'SiteOrigin')");
4343
}
4444

4545
[Test]
@@ -72,7 +72,34 @@ public void AuthTokenValidationConfigurationWithZeroOcspRequestTimeoutThrowsArgu
7272
configuration.TrustedCaCertificates.Add(new X509Certificate2(Array.Empty<byte>()));
7373
configuration.OcspRequestTimeout = TimeSpan.Zero;
7474
Assert.Throws<ArgumentOutOfRangeException>(() => configuration.Validate())
75-
.WithMessage("OCSP request timeout must be greater than zero (Parameter 'ocspRequestTimeout')");
75+
.WithMessage("OCSP request timeout must be greater than zero (Parameter 'timeSpan')");
76+
}
77+
78+
[Test]
79+
public void WhenInvalidOcspResponseTimeSkewThenValidationFails()
80+
{
81+
var builderWithInvalidOcspResponseTimeSkew =
82+
AuthTokenValidators.GetDefaultAuthTokenValidatorBuilder().WithAllowedOcspResponseTimeSkew(TimeSpan.FromMinutes(-1));
83+
Assert.Throws<ArgumentOutOfRangeException>(() => builderWithInvalidOcspResponseTimeSkew.Build())
84+
.HasMessageStartingWith("Allowed OCSP response time-skew must be greater than zero");
85+
}
86+
87+
[Test]
88+
public void WhenInvalidMaxOcspResponseThisUpdateAgeThenValidationFails()
89+
{
90+
var builderWithInvalidMaxOcspResponseThisUpdateAge =
91+
AuthTokenValidators.GetDefaultAuthTokenValidatorBuilder().WithMaxOcspResponseThisUpdateAge(TimeSpan.Zero);
92+
Assert.Throws<ArgumentOutOfRangeException>(() => builderWithInvalidMaxOcspResponseThisUpdateAge.Build())
93+
.HasMessageStartingWith("Max OCSP response thisUpdate age must be greater than zero");
94+
}
95+
96+
[Test]
97+
public void WhenInvalidOcspResponseTimeoutThenValidationFails()
98+
{
99+
var builderWithInvalidOcspResponseTimeout =
100+
AuthTokenValidators.GetDefaultAuthTokenValidatorBuilder().WithOcspRequestTimeout(TimeSpan.FromMinutes(-1));
101+
Assert.Throws<ArgumentOutOfRangeException>(() => builderWithInvalidOcspResponseTimeout.Build())
102+
.HasMessageStartingWith("OCSP request timeout must be greater than zero");
76103
}
77104

78105
[Test]

src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright © 2020-2024 Estonian Information System Authority
33
*
44
* Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -36,7 +36,7 @@ public class AuthTokenValidatorBuilderTest
3636
[Test]
3737
public void WhenOriginMissingThenBuildingFails() =>
3838
Assert.Throws<ArgumentNullException>(() => this.builder.Build())
39-
.WithMessage("Value cannot be null. (Parameter 'siteOrigin')");
39+
.WithMessage("Value cannot be null. (Parameter 'SiteOrigin')");
4040

4141
[Test]
4242
public void WhenRootCertificateAuthorityMissingThenBuildingFails()
Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright © 2020-2024 Estonian Information System Authority
33
*
44
* Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,57 +22,108 @@
2222
namespace WebEid.Security.Tests.Validator.Ocsp
2323
{
2424
using System;
25-
using Exceptions;
2625
using NUnit.Framework;
26+
using Org.BouncyCastle.Ocsp;
2727
using Security.Validator.Ocsp;
28-
using TestUtils;
28+
using WebEid.Security.Validator;
29+
using Org.BouncyCastle.Asn1;
30+
using Org.BouncyCastle.Asn1.Ocsp;
31+
using System.Globalization;
32+
using WebEid.Security.Exceptions;
33+
using WebEid.Security.Tests.TestUtils;
34+
using WebEid.Security.Util;
35+
using System.Runtime.CompilerServices;
2936

3037
[TestFixture]
3138
public class OcspResponseValidatorTests
3239
{
40+
private static TimeSpan timeSkew;
41+
private static TimeSpan maxThisUpdateAge;
42+
43+
[SetUp]
44+
public void SetUp()
45+
{
46+
var configuration = new AuthTokenValidationConfiguration();
47+
timeSkew = configuration.AllowedOcspResponseTimeSkew;
48+
maxThisUpdateAge = configuration.MaxOcspResponseThisUpdateAge;
49+
}
50+
51+
[Test]
52+
public void WhenThisAndNextUpdateWithinSkewThenValidationSucceeds()
53+
{
54+
var now = DateTimeProvider.UtcNow;
55+
var thisUpdateWithinAgeLimit = GetThisUpdateWithinAgeLimit(now);
56+
var nextUpdateWithinAgeLimit = now.Subtract(maxThisUpdateAge.Subtract(TimeSpan.FromSeconds(2)));
57+
58+
var mockedResponse = new SingleResp(new SingleResponse(null, null, thisUpdateWithinAgeLimit.ToDerGenTime(), nextUpdateWithinAgeLimit.ToDerGenTime(), null));
59+
60+
Assert.DoesNotThrow(() =>
61+
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge));
62+
}
63+
64+
[Test]
65+
public void WhenNextUpdateBeforeThisUpdateThenThrows()
66+
{
67+
var now = DateTimeProvider.UtcNow;
68+
var thisUpdateWithinAgeLimit = GetThisUpdateWithinAgeLimit(now);
69+
var beforeThisUpdate = thisUpdateWithinAgeLimit.Subtract(TimeSpan.FromSeconds(1));
70+
71+
var mockedResponse = new SingleResp(new SingleResponse(null, null, thisUpdateWithinAgeLimit.ToDerGenTime(), beforeThisUpdate.ToDerGenTime(), null));
72+
73+
Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
74+
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
75+
.HasMessageStartingWith("User certificate revocation check has failed: "
76+
+ "Certificate status update time check failed: "
77+
+ $"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)}");
78+
}
79+
3380
[Test]
34-
public void WhenThisUpdateDayBeforeProducedAtThenThrows()
81+
public void WhenThisUpdateHalfHourBeforeNowThenThrows()
3582
{
36-
var thisUpdate = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
37-
var producedAt = new DateTime(2021, 9, 2, 0, 0, 0, DateTimeKind.Utc);
83+
var now = DateTimeProvider.UtcNow;
84+
var halfHourBeforeNow = now.Subtract(TimeSpan.FromMinutes(30));
85+
var mockedResponse = new SingleResp(new SingleResponse(null, null, halfHourBeforeNow.ToDerGenTime(), null, null));
86+
3887
Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
39-
OcspResponseValidator.ValidateCertificateStatusUpdateTime(thisUpdate, null, producedAt))
40-
.WithMessage("User certificate revocation check has failed: "
41-
+ "Certificate status update time check failed: "
42-
+ "notAllowedBefore: 2021-09-01 23:45:00 +00:00, "
43-
+ "notAllowedAfter: 2021-09-02 00:15:00 +00:00, "
44-
+ "thisUpdate: 2021-09-01 00:00:00 +00:00, "
45-
+ "nextUpdate: null");
88+
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
89+
.HasMessageStartingWith("User certificate revocation check has failed: "
90+
+ "Certificate status update time check failed: "
91+
+ $"thisUpdate {halfHourBeforeNow.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is too old, minimum time allowed: ");
4692
}
93+
4794
[Test]
48-
public void WhenThisUpdateDayAfterProducedAtThenThrows()
95+
public void WhenThisUpdateHalfHourAfterNowThenThrows()
4996
{
50-
var thisUpdate = new DateTime(2021, 9, 2, 0, 0, 0, DateTimeKind.Utc);
51-
var producedAt = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
97+
var now = DateTimeProvider.UtcNow;
98+
var halfHourAfterNow = now.Add(TimeSpan.FromMinutes(30));
99+
var mockedResponse = new SingleResp(new SingleResponse(null, null, halfHourAfterNow.ToDerGenTime(), null, null));
100+
52101
Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
53-
OcspResponseValidator.ValidateCertificateStatusUpdateTime(thisUpdate, null, producedAt))
54-
.WithMessage("User certificate revocation check has failed: "
55-
+ "Certificate status update time check failed: "
56-
+ "notAllowedBefore: 2021-08-31 23:45:00 +00:00, "
57-
+ "notAllowedAfter: 2021-09-01 00:15:00 +00:00, "
58-
+ "thisUpdate: 2021-09-02 00:00:00 +00:00, "
59-
+ "nextUpdate: null");
102+
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
103+
.HasMessageStartingWith("User certificate revocation check has failed: "
104+
+ "Certificate status update time check failed: "
105+
+ $"thisUpdate {halfHourAfterNow.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is too far in the future, latest allowed: ");
60106
}
61107

62108
[Test]
63-
public void WhenNextUpdateDayBeforeProducedAtThenThrows()
109+
public void WhenNextUpdateHalfHourBeforeNowThenThrows()
64110
{
65-
var thisUpdate = new DateTime(2021, 9, 2, 0, 0, 0, DateTimeKind.Utc);
66-
var nextUpdate = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
67-
var producedAt = new DateTime(2021, 9, 1, 0, 0, 0, DateTimeKind.Utc);
111+
var now = DateTimeProvider.UtcNow;
112+
var thisUpdateWithinAgeLimit = GetThisUpdateWithinAgeLimit(now);
113+
var halfHourBeforeNow = now.Subtract(TimeSpan.FromMinutes(30));
114+
var mockedResponse = new SingleResp(new SingleResponse(null, null, thisUpdateWithinAgeLimit.ToDerGenTime(), halfHourBeforeNow.ToDerGenTime(), null));
115+
68116
Assert.Throws<UserCertificateOcspCheckFailedException>(() =>
69-
OcspResponseValidator.ValidateCertificateStatusUpdateTime(thisUpdate, nextUpdate, producedAt))
70-
.WithMessage("User certificate revocation check has failed: "
71-
+ "Certificate status update time check failed: "
72-
+ "notAllowedBefore: 2021-08-31 23:45:00 +00:00, "
73-
+ "notAllowedAfter: 2021-09-01 00:15:00 +00:00, "
74-
+ "thisUpdate: 2021-09-02 00:00:00 +00:00, "
75-
+ "nextUpdate: 2021-09-01 00:00:00 +00:00");
117+
OcspResponseValidator.ValidateCertificateStatusUpdateTime(mockedResponse, timeSkew, maxThisUpdateAge))
118+
.HasMessageStartingWith("User certificate revocation check has failed: "
119+
+ "Certificate status update time check failed: "
120+
+ $"nextUpdate {halfHourBeforeNow.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)} is in the past");
121+
}
122+
123+
private static DateTime GetThisUpdateWithinAgeLimit(DateTime now)
124+
{
125+
var maxThisUpdateAgeMinusOne = maxThisUpdateAge.Subtract(TimeSpan.FromSeconds(1));
126+
return now.Subtract(maxThisUpdateAgeMinusOne);
76127
}
77128
}
78129
}

0 commit comments

Comments
 (0)