Skip to content
Open
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,52 @@ It contains the following fields:

- `signature`: the base64-encoded signature of the token (see the description below),

- `format`: the type identifier and version of the token format separated by a colon character '`:`', `web-eid:1.0` as of now; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version,
- `format`: the type identifier and version of the token format separated by a colon character '`:`', either `web-eid:1.0` or `web-eid:1.1`; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version,

- `appVersion`: the URL identifying the name and version of the application that issued the token; informative purpose, can be used to identify the affected application in case of faulty tokens.

The value that is signed by the user’s authentication private key and included in the `signature` field is `hash(origin)+hash(challenge)`. The hash function is used before concatenation to ensure field separation as the hash of a value is guaranteed to have a fixed length. Otherwise the origin `example.com` with challenge nonce `.eu1234` and another origin `example.com.eu` with challenge nonce `1234` would result in the same value after concatenation. The hash function `hash` is the same hash function that is used in the signature algorithm, for example SHA256 in case of RS256.

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, as well as the eID user's signing certificates. An extended token looks like the following example:

```json
{
"unverifiedCertificate": "MIIFozCCA4ugAwIBAgIQHFpdK-zCQsFW4...",
"unverifiedIntermediateCertificates": ["MIIFfhzYIBAAwIBAgIQHFHFp-AwIBFW4..."],
"algorithm": "RS256",
"signature": "HBjNXIaUskXbfhzYQHvwjKDUWfNu4yxXZha...",
"unverifiedSigningCertificates": [
{
"certificate": "MIIFoXIaUskXbfhzYIBAgIjKDUsdK-zQHFUKz...",
"intermediateCertificates": ["MIIFfhzYIBAAwIBAgIQHFHFp-AwIBFW4..."],
"supportedSignatureAlgorithms": [
{
"cryptoAlgorithm": "ECC",
"hashFunction": "SHA-384",
"paddingScheme": "NONE"
}
]
}
],
"format": "web-eid:1.1",
"appVersion": "https://web-eid.eu/web-eid-app/releases/v2.0.0"
}
```

In addition to the fields described above, the `web-eid:1.1` format defines the following additional fields:

- `unverifiedIntermediateCertificates`: an array of base64-encoded DER intermediate CA certificates that make up the trust chain of the authentication certificate in `unverifiedCertificate`. Like the authentication certificate, these certificates are received from the client side and cannot be trusted; they are only used as candidate certificates when building the certification path, which must still terminate at a trusted certificate authority. Every intermediate selected for the path is revocation-checked with OCSP or CRLs and the token is rejected if its status cannot be determined. The field is optional, but when present it must not be empty. When it is present, `unverifiedSigningCertificates` may be omitted.

- `unverifiedSigningCertificates`: an array of the eID user's signing certificates, presented alongside the authentication certificate. For `web-eid:1.1` tokens this field is required unless `unverifiedIntermediateCertificates` is present, and when present it must not be empty. Each entry contains:

- `certificate`: the base64-encoded DER signing certificate. During validation it must have the same subject and issuer as the authentication certificate, be valid, contain the non-repudiation key usage bit and be signed by a trusted certificate authority.

- `intermediateCertificates`: an optional array of base64-encoded DER intermediate CA certificates that make up the trust chain of this signing certificate. When present it must not be empty and, as with `unverifiedIntermediateCertificates`, the certificates are only used as candidates when building the certification path to a trusted CA and every selected intermediate is revocation-checked.

- `supportedSignatureAlgorithms`: the signature algorithms that the signing certificate's key supports. Each entry has a `cryptoAlgorithm` (`ECC` or `RSA`), a `hashFunction` (one of `SHA-224`, `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-224`, `SHA3-256`, `SHA3-384`, `SHA3-512`) and a `paddingScheme` (`NONE`, `PKCS1.5` or `PSS`).

The signature is always created with the authentication private key and verified using `unverifiedCertificate`; the signing certificates are not involved in the signature verification.


# Authentication token validation

Expand All @@ -323,6 +363,8 @@ 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.

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 website back end 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 back end 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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
public class UnverifiedSigningCertificate {

private String certificate;
private List<String> intermediateCertificates;
private List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms;

public String getCertificate() {
Expand All @@ -40,6 +41,14 @@ public void setCertificate(String certificate) {
this.certificate = certificate;
}

public List<String> getIntermediateCertificates() {
return intermediateCertificates;
}

public void setIntermediateCertificates(List<String> intermediateCertificates) {
this.intermediateCertificates = intermediateCertificates;
}

public List<SupportedSignatureAlgorithm> getSupportedSignatureAlgorithms() {
return supportedSignatureAlgorithms;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class WebEidAuthToken {
private String algorithm;
private String format;

private List<String> unverifiedIntermediateCertificates;
private List<UnverifiedSigningCertificate> unverifiedSigningCertificates;

public String getUnverifiedCertificate() {
Expand All @@ -44,6 +45,14 @@ public void setUnverifiedCertificate(String unverifiedCertificate) {
this.unverifiedCertificate = unverifiedCertificate;
}

public List<String> getUnverifiedIntermediateCertificates() {
return unverifiedIntermediateCertificates;
}

public void setUnverifiedIntermediateCertificates(List<String> unverifiedIntermediateCertificates) {
this.unverifiedIntermediateCertificates = unverifiedIntermediateCertificates;
}

public String getSignature() {
return signature;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ public static X509Certificate decodeCertificateFromBase64(String certificateInBa
}
}

public static List<X509Certificate> decodeCertificatesFromBase64(List<String> certificatesInBase64) throws CertificateDecodingException {
if (certificatesInBase64 == null || certificatesInBase64.isEmpty()) {
return List.of();
}
final List<X509Certificate> decodedCertificates = new ArrayList<>();
for (final String certificateInBase64 : certificatesInBase64) {
decodedCertificates.add(decodeCertificateFromBase64(certificateInBase64));
}
return decodedCertificates;
}

private CertificateLoader() {
throw new IllegalStateException("Utility class");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,26 @@
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertPath;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.PKIXParameters;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -60,6 +69,14 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif
Set<TrustAnchor> trustedCACertificateAnchors,
CertStore trustedCACertificateCertStore,
Date now) throws CertificateNotTrustedException, JceException, CertificateNotYetValidException, CertificateExpiredException {
return validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore, List.of(), now);
}

public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certificate,
Set<TrustAnchor> trustedCACertificateAnchors,
CertStore trustedCACertificateCertStore,
List<X509Certificate> additionalIntermediateCertificates,
Date now) throws CertificateNotTrustedException, JceException, CertificateNotYetValidException, CertificateExpiredException {
certificateIsValidOnDate(certificate, now, "User");

final X509CertSelector selector = new X509CertSelector();
Expand All @@ -71,25 +88,79 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif
pkixBuilderParameters.setRevocationEnabled(false);
pkixBuilderParameters.setDate(now);
pkixBuilderParameters.addCertStore(trustedCACertificateCertStore);
if (additionalIntermediateCertificates != null && !additionalIntermediateCertificates.isEmpty()) {
pkixBuilderParameters.addCertStore(buildCertStoreFromCertificates(additionalIntermediateCertificates));
}

// See the comment in buildCertStoreFromCertificates() below why we use the default JCE provider.
final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType());
final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters);

validateIntermediateCertificatesNotRevoked(
result,
trustedCACertificateAnchors,
trustedCACertificateCertStore,
additionalIntermediateCertificates,
now
);

final X509Certificate trustedCACert = result.getTrustAnchor().getTrustedCert();

// Verify that the trusted CA cert is presently valid before returning the result.
certificateIsValidOnDate(trustedCACert, now, "Trusted CA");

return trustedCACert;
return getIssuerCertificate(result, trustedCACert);

} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertificateException e) {
throw new JceException(e);
} catch (CertPathBuilderException e) {
} catch (CertPathBuilderException | CertPathValidatorException e) {
throw new CertificateNotTrustedException(certificate, e);
}
}

private static void validateIntermediateCertificatesNotRevoked(
PKIXCertPathBuilderResult pathBuilderResult,
Set<TrustAnchor> trustedCACertificateAnchors,
CertStore trustedCACertificateCertStore,
List<X509Certificate> additionalIntermediateCertificates,
Date now
) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertificateException,
CertPathValidatorException {
final List<? extends Certificate> certificatePath = pathBuilderResult.getCertPath().getCertificates();
if (certificatePath.size() <= 1) {
return; // leaf chains directly to a trust anchor; no non-anchor intermediate to validate
}

// The existing application OCSP flow checks the end-entity certificate. Validate only the CA suffix here,
// excluding both the end entity at index 0 and the trust anchor, which is not part of the built path.
final CertPath intermediateCertificatePath = CertificateFactory.getInstance("X.509")
.generateCertPath(certificatePath.subList(1, certificatePath.size()));
final PKIXParameters revocationCheckingParameters = new PKIXParameters(trustedCACertificateAnchors);
revocationCheckingParameters.setDate(now);
// An explicitly added checker is active regardless of this flag and avoids installing a second default checker.
revocationCheckingParameters.setRevocationEnabled(false);
revocationCheckingParameters.addCertStore(trustedCACertificateCertStore);
if (additionalIntermediateCertificates != null && !additionalIntermediateCertificates.isEmpty()) {
revocationCheckingParameters.addCertStore(CertStore.getInstance(
"Collection", new CollectionCertStoreParameters(additionalIntermediateCertificates)));
}

final CertPathValidator certPathValidator = CertPathValidator.getInstance(CertPathValidator.getDefaultType());
final PKIXRevocationChecker revocationChecker =
(PKIXRevocationChecker) certPathValidator.getRevocationChecker();
// The default checker prefers OCSP and falls back to CRLs. SOFT_FAIL is deliberately not enabled: a token-
// supplied intermediate whose revocation status cannot be established must not become part of a trusted path.
revocationCheckingParameters.addCertPathChecker(revocationChecker);
certPathValidator.validate(intermediateCertificatePath, revocationCheckingParameters);
}

private static X509Certificate getIssuerCertificate(PKIXCertPathBuilderResult path, X509Certificate trustedCACert) {
// The built path is ordered from the subject towards the anchor and excludes the anchor, so index 1 (when
// present) is the subject's direct issuer; otherwise the subject was issued directly by the trust anchor.
final List<? extends Certificate> certificatePath = path.getCertPath().getCertificates();
return certificatePath.size() > 1 ? (X509Certificate) certificatePath.get(1) : trustedCACert;
}

public static Set<TrustAnchor> buildTrustAnchorsFromCertificates(Collection<X509Certificate> certificates) {
return certificates.stream()
.map(cert -> new TrustAnchor(cert, null)).collect(Collectors.toUnmodifiableSet());
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class AuthTokenValidatorManager implements AuthTokenValidator {

// Use human-readable meaningful names for token length limits.
private static final int TOKEN_MIN_LENGTH = 100;
private static final int TOKEN_MAX_LENGTH = 10000;
private static final int TOKEN_MAX_LENGTH = 65536;

private static final ObjectReader TOKEN_READER = new ObjectMapper().readerFor(WebEidAuthToken.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer

// Use the clock instance so that the date can be mocked in tests.
final Date now = DateAndTime.DefaultClock.getInstance().now();
ocspService.validateResponderCertificate(responderCert, now);
ocspService.validateResponderCertificate(
responderCert,
trustValidator.getSubjectCertificateIssuerCertificate(),
trustValidator.getAdditionalIntermediateCertificates(),
now);

// 5. The time at which the status being indicated is known to be
// correct (thisUpdate) is sufficiently recent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.Set;

public final class SubjectCertificateTrustedValidator {
Expand All @@ -43,11 +44,17 @@ public final class SubjectCertificateTrustedValidator {

private final Set<TrustAnchor> trustedCACertificateAnchors;
private final CertStore trustedCACertificateCertStore;
private final List<X509Certificate> additionalIntermediateCertificates;
private X509Certificate subjectCertificateIssuerCertificate;

public SubjectCertificateTrustedValidator(Set<TrustAnchor> trustedCACertificateAnchors, CertStore trustedCACertificateCertStore) {
this(trustedCACertificateAnchors, trustedCACertificateCertStore, List.of());
}

public SubjectCertificateTrustedValidator(Set<TrustAnchor> trustedCACertificateAnchors, CertStore trustedCACertificateCertStore, List<X509Certificate> additionalIntermediateCertificates) {
this.trustedCACertificateAnchors = trustedCACertificateAnchors;
this.trustedCACertificateCertStore = trustedCACertificateCertStore;
this.additionalIntermediateCertificates = additionalIntermediateCertificates;
}

/**
Expand All @@ -67,6 +74,7 @@ public void validateCertificateTrusted(X509Certificate subjectCertificate) throw
subjectCertificate,
trustedCACertificateAnchors,
trustedCACertificateCertStore,
additionalIntermediateCertificates,
now
);
LOG.debug("Subject certificate is valid and signed by a trusted CA");
Expand All @@ -75,4 +83,8 @@ public void validateCertificateTrusted(X509Certificate subjectCertificate) throw
public X509Certificate getSubjectCertificateIssuerCertificate() {
return subjectCertificateIssuerCertificate;
}

public List<X509Certificate> getAdditionalIntermediateCertificates() {
return additionalIntermediateCertificates;
}
}
Loading
Loading