Skip to content

Commit

Permalink
[#3588] Enable OCSP revocation check for trusted CA
Browse files Browse the repository at this point in the history
Extended Device Registry Management API to support configuring properties on a tenant's Trust Anchor that are necessary for checking revocation status of client certificates during device authentication.
Extended protocol adapters to perform OCSP based certificate revocation check for devices using an X.509 client certificate for authentication.
Amended the Concept section covering certificate based authentication accordingly.

Signed-off-by: Vit Holasek <[email protected]>
  • Loading branch information
kyberpunk authored Mar 6, 2024
1 parent 2a6cf4d commit 7dcf993
Show file tree
Hide file tree
Showing 21 changed files with 1,128 additions and 45 deletions.
34 changes: 32 additions & 2 deletions adapter-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-config-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>

<!-- testing -->
<dependency>
Expand Down Expand Up @@ -138,14 +142,40 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<!--
Copy legal documents from "legal" module to "target/classes" folder
so that we make sure to include legal docs in all modules.
-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<!--
Execution and configuration for copying certificates from related module
to "target/classes" folder so that we can include them in the image.
-->
<id>copy_demo_certs</id>
<phase>generate-test-resources</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeArtifactIds>hono-demo-certs</includeArtifactIds>
<outputDirectory>${project.build.directory}/certs</outputDirectory>
<excludes>META-INF/**</excludes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,26 @@

package org.eclipse.hono.adapter.auth.device.x509;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertPathValidatorException.BasicReason;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.PKIXRevocationChecker.Option;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.eclipse.hono.service.auth.X509CertificateChainValidator;
import org.eclipse.hono.util.RevocableTrustAnchor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -70,26 +77,82 @@ public Future<Void> validate(final List<X509Certificate> chain, final Set<TrustA

final Promise<Void> result = Promise.promise();

try {
final PKIXParameters params = new PKIXParameters(trustAnchors);
// TODO do we need to check for revocation?
params.setRevocationEnabled(false);
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
final CertPath path = factory.generateCertPath(chain);
final CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(path, params);
LOG.debug("validation of device certificate [subject DN: {}] succeeded",
chain.get(0).getSubjectX500Principal().getName());
result.complete();
} catch (GeneralSecurityException e) {
LOG.debug("validation of device certificate [subject DN: {}] failed",
chain.get(0).getSubjectX500Principal().getName(), e);
if (e instanceof CertificateException) {
result.fail(e);
} else {
result.fail(new CertificateException("validation of device certificate failed", e));
Exception lastException = null;
// Need to validate each anchor separately to be able to configure specific revocation check settings
for (TrustAnchor anchor : trustAnchors) {
try {
validateSingleAnchor(chain, anchor);
// Successfully validated using this anchor
result.complete();
return result.future();
} catch (CertPathValidatorException e) {
lastException = e;
if (e.getReason() == BasicReason.REVOKED || e.getReason() == BasicReason.UNDETERMINED_REVOCATION_STATUS) {
// Certificate trusted but revoked, exit now
LOG.warn("Certificate [subject DN: {}] revocation check failed.",
chain.get(0).getSubjectX500Principal().getName(), e);
break;
}
} catch (GeneralSecurityException e) {
lastException = e;
}
}
LOG.debug("validation of device certificate [subject DN: {}] failed",
chain.get(0).getSubjectX500Principal().getName(), lastException);
if (lastException instanceof CertificateException) {
result.fail(lastException);
} else {
result.fail(new CertificateException("validation of device certificate failed", lastException));
}
return result.future();
}

private void validateSingleAnchor(final List<X509Certificate> chain, final TrustAnchor anchor)
throws GeneralSecurityException {
final PKIXParameters params = new PKIXParameters(Set.of(anchor));
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
final CertPath path = factory.generateCertPath(chain);
final CertPathValidator validator = CertPathValidator.getInstance("PKIX");
configureRevocationCheck(validator, params, anchor);
validator.validate(path, params);
LOG.debug("validation of device certificate [subject DN: {}] succeeded",
chain.get(0).getSubjectX500Principal().getName());
}

private void configureRevocationCheck(final CertPathValidator validator, final PKIXParameters parameters,
final TrustAnchor trustAnchor) throws CertificateException {
if (trustAnchor instanceof RevocableTrustAnchor revocableTrustAnchor
&& revocableTrustAnchor.isOcspEnabled()) {
// Provide custom revocation check configuration per trusted anchor as described here:
// https://docs.oracle.com/javase/8/docs/api/java/security/cert/CertPathValidator.html
final PKIXRevocationChecker revocationChecker = (PKIXRevocationChecker) validator.getRevocationChecker();
final Set<Option> options = EnumSet.noneOf(Option.class);
parameters.setRevocationEnabled(true);
if (revocableTrustAnchor.isOcspEnabled()) {
if (revocableTrustAnchor.getOcspResponderUri() != null) {
revocationChecker.setOcspResponder(revocableTrustAnchor.getOcspResponderUri());
}
if (revocableTrustAnchor.getOcspResponderCert() != null) {
revocationChecker.setOcspResponderCert(revocableTrustAnchor.getOcspResponderCert());
}
// We always check end entities only for revocation in current implementation
options.add(Option.ONLY_END_ENTITY);
if (revocableTrustAnchor.isOcspNonceEnabled()) {
try {
// With default Java implementation, nonce can be enabled only by global java system property
// for all connections, we need to set extension manually to do it per trusted anchor
revocationChecker.setOcspExtensions(List.of(new OCSPNonceExtension()));
} catch (IOException e) {
throw new CertificateException("Cannot process OCSP nonce.", e);
}
}
// CRL is currently not supported
options.add(Option.NO_FALLBACK);
}
revocationChecker.setOptions(options);
parameters.addCertPathChecker(revocationChecker);
} else {
parameters.setRevocationEnabled(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*******************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/

package org.eclipse.hono.adapter.auth.device.x509;

import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;

import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.Extension;

/**
* Implementation of OCSP nonce extension (RFC 8954) which is used to avoid replay attacks to OCSP requests.
* We need custom implementation, because the Java implementation if from legacy sun package.
*/
public final class OCSPNonceExtension implements java.security.cert.Extension {
/**
* According to RFC 8954 must be from 1 to 32.
*/
private static final int OCSP_NONCE_SIZE = 16;
private final byte[] value;
private final Extension extension;

/**
* Creates a new instance of OCSP nonce extension.
*
* @throws IOException on failure when encoding nonce value.
*/
public OCSPNonceExtension() throws IOException {
final SecureRandom random = new SecureRandom();
final byte[] nonce = new byte[OCSP_NONCE_SIZE];
random.nextBytes(nonce);
final DEROctetString derValue = new DEROctetString(nonce);
value = derValue.getEncoded();
extension = Extension.create(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derValue);
}

@Override
public String getId() {
return OCSPObjectIdentifiers.id_pkix_ocsp_nonce.getId();
}

@Override
public boolean isCritical() {
return false;
}

@Override
public byte[] getValue() {
return value;
}

@Override
public void encode(final OutputStream out) throws IOException {
extension.encodeTo(out);
}
}
Loading

0 comments on commit 7dcf993

Please sign in to comment.