Skip to content
Open
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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ The authentication token validation process consists of two stages:
- First, **user certificate validation**: the validator parses the token and extracts the user certificate from the *unverifiedCertificate* field. Then it checks the certificate expiration, purpose and policies. Next it checks that the certificate is signed by a trusted CA and checks the certificate status with OCSP.
- Second, **token signature validation**: the validator validates that the token signature was created using the provided user certificate by reconstructing the signed data `hash(origin)+hash(challenge)` and using the public key from the certificate to verify the signature in the `signature` field. If the signature verification succeeds, then the origin and challenge nonce have been implicitly and correctly verified without the need to implement any additional security checks.

Starting from format version `web-eid:1.1`, the authentication token may additionally carry the intermediate CA certificates needed to build the trust chains of the certificates it contains, in the `unverifiedIntermediateCertificates` field, and the eID user's signing certificates in the `unverifiedSigningCertificates` field, where each entry may carry its own `intermediateCertificates`. Like the authentication certificate, these certificates are received from the client side and cannot be trusted; the intermediate certificates are only used as candidate certificates when building the certification path, which must still terminate at a trusted certificate authority.

When the token is in the `web-eid:1.1` format and contains `unverifiedSigningCertificates`, the validator additionally validates each signing certificate: that it has the same subject and issuer as the authentication certificate, is currently valid, is suitable for digital signatures (contains the non-repudiation key usage bit) and is signed by a trusted certificate authority, building the chain from the certificate's `intermediateCertificates` to a trusted CA. The signing certificate itself is deliberately not checked for revocation during authentication: its revocation status matters at signing time and is validated by the signature creation and validation services.

When a token supplies intermediate CA certificates and a certification path is built through them, the validator checks the revocation status of the intermediate CA certificates in the path with the .NET platform's `X509Chain` revocation checker, which uses OCSP and CRLs, and rejects the token when the status of an intermediate is revoked or cannot be established. This check runs during path validation for both the authentication and the signing certificate chains, independently of the user certificate OCSP check configuration, because token-supplied intermediates are untrusted input. Deployments that need to accept tokens with intermediate certificates must therefore allow the network access that OCSP or CRL fetching requires.

When the user certificate OCSP check uses an AIA OCSP responder, the responder is authorized according to [RFC 6960 section 4.2.2.2](https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.2.2): the response must be signed either by the CA that issued the user certificate, in which case the OCSP-signing extended key usage is not required, or by a responder whose certificate contains the id-kp-OCSPSigning extended key usage and chains to a trusted CA through the certificate that issued the user certificate. Responder certificates are deliberately not checked for revocation, following the id-pkix-ocsp-nocheck convention of RFC 6960 section 4.2.2.2.1 and because querying an OCSP service about its own signer would be circular.

The website backend must lookup the challenge nonce from its local store using an identifier specific to the browser session, to guarantee that the authentication token was received from the same browser to which the corresponding challenge nonce was issued. The website backend must guarantee that the challenge nonce lifetime is limited and that its expiration is checked, and that it can be used only once by removing it from the store during validation.

## Basic usage
Expand Down Expand Up @@ -474,9 +482,10 @@ The following additional configuration options are available in `AuthTokenValida

- `WithoutUserCertificateRevocationCheckWithOcsp()` – turns off user certificate revocation check with OCSP. OCSP check is enabled by default and the OCSP responder access location URL is extracted from the user certificate AIA extension unless a designated OCSP service is activated.
- `WithDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration)` – activates the provided designated OCSP responder service configuration for user certificate revocation check with OCSP. The designated service is only used for checking the status of the certificates whose issuers are supported by the service, for other certificates the default AIA extension service access location will be used. See configuration examples in tests.
- `WithOcspRequestTimeout(TimeSpan ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
- `WithOcspRequestTimeout(TimeSpan ocspRequestTimeout)` – sets the network timeout for user certificate OCSP requests and platform OCSP or CRL retrievals used to check intermediate certificates. 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.
- `WithAiaOcspResponderIssuerMatchingPolicy(ResponderIssuerMatchingPolicy matchingPolicy)` – controls how an AIA OCSP responder's issuer is matched against the user certificate's issuer. The default `ExactCertificate` policy requires the same X.509 certificate. Use `SubjectAndPublicKey` to accept equivalent cross-certificates with the same subject and public key; this also enables revocation checking for non-anchor intermediate certificates in the responder's certification path.
- `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:
Expand All @@ -495,6 +504,8 @@ AuthTokenValidator validator = new AuthTokenValidatorBuilder(logger)

Unless a designated OCSP responder service is in use, it is required that the AIA extension that contains the certificate’s OCSP responder access location is present in the user certificate. The AIA OCSP URL will be used to check the certificate revocation status with OCSP.

By default, the certificate that directly signs an AIA OCSP response, or issues a delegated AIA OCSP responder, must exactly match the certificate that issued the user certificate. Deployments that require equivalent cross-certificates can explicitly select `ResponderIssuerMatchingPolicy.SubjectAndPublicKey` with `WithAiaOcspResponderIssuerMatchingPolicy()`. This policy also requires the revocation status of every non-anchor intermediate certificate in the responder's certification path to be established.

Note that there may be limitations to using AIA URLs as the services behind these URLs provide different security and SLA guarantees than dedicated OCSP responder services. In case you need a SLA guarantee, use a designated OCSP responder service.

## Logging
Expand Down
Binary file not shown.
Binary file not shown.
19 changes: 19 additions & 0 deletions src/WebEid.Security.Tests/TestUtils/AuthTokenValidators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace WebEid.Security.Tests.TestUtils
using System;
using System.Security.Cryptography.X509Certificates;
using Security.Validator;
using Security.Validator.Ocsp;

public static class AuthTokenValidators
{
Expand Down Expand Up @@ -81,6 +82,24 @@ public static IAuthTokenValidator GetAuthTokenValidatorForFinnishIdCard() =>
Certificates.CertificateLoader.LoadCertificatesFromResources("DVV TEST Certificates - G5E.crt", "VRK TEST CA for Test Purposes - G4.crt")
);

public static IAuthTokenValidator GetAuthTokenValidatorForBelgianIdCardWithOcspCheck(IOcspClient ocspClient) =>
GetAuthTokenValidatorBuilder(
"https://47f0-46-131-86-189.ngrok-free.app",
Certificates.CertificateLoader.LoadCertificatesFromResources("eID TEST EC Citizen CA.cer"))
// The recorded OCSP response used in tests was created without a nonce.
.WithNonceDisabledOcspUrls(new Uri("http://eiddevcards.zetescards.be:8888"))
.WithOcspClient(ocspClient)
.Build();

public static IAuthTokenValidator GetAuthTokenValidatorForFinnishIdCardWithOcspCheck(IOcspClient ocspClient) =>
GetAuthTokenValidatorBuilder(
"https://47f0-46-131-86-189.ngrok-free.app",
Certificates.CertificateLoader.LoadCertificatesFromResources("DVV TEST Certificates - G5E.crt", "VRK TEST CA for Test Purposes - G4.crt"))
// The recorded OCSP response used in tests was created without a nonce.
.WithNonceDisabledOcspUrls(new Uri("http://ocsptest.fineid.fi/dvvtp5ec"))
.WithOcspClient(ocspClient)
.Build();


public static AuthTokenValidatorBuilder GetDefaultAuthTokenValidatorBuilder() =>
GetAuthTokenValidatorBuilder(TokenOriginUrl, GetCaCertificates());
Expand Down
41 changes: 41 additions & 0 deletions src/WebEid.Security.Tests/TestUtils/RecordedResponseOcspClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright © 2025-2025 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace WebEid.Security.Tests.TestUtils
{
using System;
using System.Threading.Tasks;
using Org.BouncyCastle.Ocsp;
using Security.Validator.Ocsp;

/// <summary>
/// An <see cref="IOcspClient"/> that always returns a pre-recorded OCSP response, mirroring the recorded-response
/// client used by the Belgian and Finnish AIA OCSP tests in the Java reference implementation.
/// </summary>
internal sealed class RecordedResponseOcspClient(byte[] responseBytes) : IOcspClient
{
public void Dispose()
{
}

public Task<OcspResp> Request(Uri uri, OcspReq ocspReq) => Task.FromResult(new OcspResp(responseBytes));
}
}
110 changes: 110 additions & 0 deletions src/WebEid.Security.Tests/TestUtils/TestCertificateGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright © 2025-2025 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace WebEid.Security.Tests.TestUtils
{
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using BcX509 = Org.BouncyCastle.Asn1.X509;

/// <summary>
/// Generates ephemeral certificate hierarchies for certification-path and OCSP responder tests.
/// Mirrors the BouncyCastle-generated fixtures of the corresponding Java tests, but builds the
/// certificates with <see cref="CertificateRequest"/> and ECDSA keys so that they can be validated by
/// the <see cref="System.Security.Cryptography.X509Certificates.X509Chain"/>-based library implementation.
/// </summary>
internal static class TestCertificateGenerator
{
private const string OcspSigningEku = "1.3.6.1.5.5.7.3.9";
private const string AuthorityInformationAccessOid = "1.3.6.1.5.5.7.1.1";

/// <summary>
/// Generates a self-signed CA certificate that can act as a trust anchor.
/// </summary>
public static X509Certificate2 GenerateSelfSignedCa(string commonName,
DateTimeOffset notBefore, DateTimeOffset notAfter)
{
var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var request = new CertificateRequest($"CN={commonName}", key, HashAlgorithmName.SHA256);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
request.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
return request.CreateSelfSigned(notBefore, notAfter);
}

/// <summary>
/// Generates a certificate issued by the given issuer.
/// </summary>
/// <param name="commonName">The subject common name.</param>
/// <param name="issuer">The issuing certificate; it must carry its private key.</param>
/// <param name="isCa">Whether the generated certificate is a CA certificate.</param>
/// <param name="notBefore">Start of the validity window.</param>
/// <param name="notAfter">End of the validity window.</param>
/// <param name="ocspSigning">Whether to add the OCSP-signing extended key usage.</param>
/// <param name="ocspUrl">When set, adds an Authority Information Access OCSP responder URL.</param>
/// <param name="key">When set, reuses the given key pair (used to build equivalent cross-certificates).</param>
public static X509Certificate2 GenerateCertificate(string commonName,
X509Certificate2 issuer,
bool isCa,
DateTimeOffset notBefore,
DateTimeOffset notAfter,
bool ocspSigning = false,
string ocspUrl = null,
ECDsa key = null)
{
key ??= ECDsa.Create(ECCurve.NamedCurves.nistP256);
var request = new CertificateRequest($"CN={commonName}", key, HashAlgorithmName.SHA256);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(isCa, false, 0, true));
if (isCa)
{
request.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
}
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
request.CertificateExtensions.Add(
X509AuthorityKeyIdentifierExtension.CreateFromCertificate(issuer, true, false));
if (ocspSigning)
{
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension([new Oid(OcspSigningEku)], false));
}
if (ocspUrl != null)
{
var authorityInformationAccess = new BcX509.AuthorityInformationAccess(
new BcX509.AccessDescription(BcX509.AccessDescription.IdADOcsp,
new BcX509.GeneralName(BcX509.GeneralName.UniformResourceIdentifier, ocspUrl)));
request.CertificateExtensions.Add(new X509Extension(
new Oid(AuthorityInformationAccessOid), authorityInformationAccess.GetDerEncoded(), false));
}

var serialNumber = RandomNumberGenerator.GetBytes(16);
serialNumber[0] &= 0x7F; // Keep the serial number positive.
// Sign with an explicit signature generator rather than the issuer-certificate Create overload: the latter
// requires the subject validity window to nest inside the issuer's, which prevents generating a currently
// valid leaf under an expired or not-yet-valid intermediate.
var generator = X509SignatureGenerator.CreateForECDsa(issuer.GetECDsaPrivateKey());
var certificate = request.Create(issuer.SubjectName, generator, notBefore, notAfter, serialNumber);
return certificate.CopyWithPrivateKey(key);
}
}
}
Loading
Loading