Skip to content

Commit

Permalink
Reworked Certificate checking to be more flexible and streamlined
Browse files Browse the repository at this point in the history
  • Loading branch information
phax committed Nov 10, 2024
1 parent 4e894de commit 486128c
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 358 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ They depend on several other libraries so I suggest you are going for the Maven
* Moved method `PeppolCertificateHelper.getAllTrustedCertificates` to class `PeppolKeyStoreHelper`
* Added new methods to support Peppol Policy for use of Identifiers 4.3.0
* Added new annotations `@Pfuoi420` and `@Pfuoi430` to hint methods that are specification specific
* Added support for eB2B AP Pilot Trust Store as a predefined truststore
* Renamed class `CertificateRevocationChecker` to `CertificateRevocationCheckerDefaults`
* Made class `RevocationCheckBuilder` a top-level class
* Totally reworked class `PeppolCertificateChecker` to add flexibility and support multiple Peppol CAs
* Added new class `PeppolCAChecker` to support in the verification of Peppol certificates
* v9.5.1 - 2024-08-11
* Make sure that wildcard lookups including a "*" in the Customization ID will always fail
* Added additional `SMPClientReadOnly.getWildcardServiceMetadataOrNull` overload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public abstract class AbstractRevocationCheckBuilder <IMPLTYPE extends AbstractR
private Consumer <? super List <CertPathValidatorException>> m_aSoftFailExceptionHdl;
private ETriState m_eExecuteInSynchronizedBlock = ETriState.UNDEFINED;
private Duration m_aExecutionDurationWarn = DEFAULT_EXECUTION_WARN_DURATION;
private CRLCache m_aCRLCache = CertificateRevocationChecker.getDefaultCRLCache ();
private CRLCache m_aCRLCache = CertificateRevocationCheckerDefaults.getDefaultCRLCache ();

public AbstractRevocationCheckBuilder ()
{}
Expand Down Expand Up @@ -146,6 +146,22 @@ public final IMPLTYPE validCAs (@Nullable final Iterable <? extends X509Certific
return thisAsT ();
}

/**
* Set the valid CAs to be checked against. All previous trusted CAs are
* removed.
*
* @param a
* The array of CA certificates to be checked against. May be
* <code>null</code>.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE validCAs (@Nullable final X509Certificate... a)
{
m_aValidCAs.setAll (a);
return thisAsT ();
}

/**
* Set the valid CAs to be checked against from the provided trust store. All
* previous trusted CAs are removed.
Expand Down Expand Up @@ -191,6 +207,21 @@ public final IMPLTYPE addValidCAs (@Nullable final Iterable <? extends X509Certi
return thisAsT ();
}

/**
* Add valid CAs to be checked against. All previously contained valid CAs are
* kept.
*
* @param a
* A CA certificates to be checked against. May be <code>null</code>.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE addValidCAs (@Nullable final X509Certificate... a)
{
m_aValidCAs.addAll (a);
return thisAsT ();
}

/**
* Add the valid CAs to be checked against from the provided trust store. All
* previously contained valid CAs are kept.
Expand Down Expand Up @@ -370,7 +401,7 @@ public final IMPLTYPE softFailExceptionHandler (@Nullable final Consumer <? supe
* @param b
* <code>true</code> to enable it, <code>false</code> to disable it.
* @return this for chaining
* @see CertificateRevocationChecker#isExecuteInSynchronizedBlock()
* @see CertificateRevocationCheckerDefaults#isExecuteInSynchronizedBlock()
*/
@Nonnull
public final IMPLTYPE executeInSynchronizedBlock (final boolean b)
Expand Down Expand Up @@ -438,31 +469,31 @@ public final IMPLTYPE crlCache (@Nonnull final CRLCache a)
* performed:
* <ul>
* <li>checkMode -
* {@link CertificateRevocationChecker#getRevocationCheckMode()}</li>
* {@link CertificateRevocationCheckerDefaults#getRevocationCheckMode()}</li>
* <li>exceptionHandler -
* {@link CertificateRevocationChecker#getExceptionHdl()}</li>
* {@link CertificateRevocationCheckerDefaults#getExceptionHdl()}</li>
* <li>allowSoftFail -
* {@link CertificateRevocationChecker#isAllowSoftFail()}</li>
* {@link CertificateRevocationCheckerDefaults#isAllowSoftFail()}</li>
* <li>softFailExceptionHandler -
* {@link CertificateRevocationChecker#getSoftFailExceptionHdl()}</li>
* {@link CertificateRevocationCheckerDefaults#getSoftFailExceptionHdl()}</li>
* <li>executeInSynchronizedBlock -
* {@link CertificateRevocationChecker#isExecuteInSynchronizedBlock()}</li>
* {@link CertificateRevocationCheckerDefaults#isExecuteInSynchronizedBlock()}</li>
* </ul>
*/
@Nonnull
public ERevoked build ()
{
// Fallback to global settings where possible
final ERevocationCheckMode eRealCheckMode = m_eCheckMode != null ? m_eCheckMode
: CertificateRevocationChecker.getRevocationCheckMode ();
: CertificateRevocationCheckerDefaults.getRevocationCheckMode ();
final Consumer <? super GeneralSecurityException> aRealExceptionHdl = m_aExceptionHdl != null ? m_aExceptionHdl
: CertificateRevocationChecker.getExceptionHdl ();
: CertificateRevocationCheckerDefaults.getExceptionHdl ();
final boolean bAllowSoftFail = m_eAllowSoftFail.isDefined () ? m_eAllowSoftFail.getAsBooleanValue ()
: CertificateRevocationChecker.isAllowSoftFail ();
: CertificateRevocationCheckerDefaults.isAllowSoftFail ();
final Consumer <? super List <CertPathValidatorException>> aRealSoftFailExceptionHdl = m_aSoftFailExceptionHdl != null ? m_aSoftFailExceptionHdl
: CertificateRevocationChecker.getSoftFailExceptionHdl ();
: CertificateRevocationCheckerDefaults.getSoftFailExceptionHdl ();
final boolean bExecuteSync = m_eExecuteInSynchronizedBlock.isDefined () ? m_eExecuteInSynchronizedBlock.getAsBooleanValue ()
: CertificateRevocationChecker.isExecuteInSynchronizedBlock ();
: CertificateRevocationCheckerDefaults.isExecuteInSynchronizedBlock ();

// Consistency checks
if (m_aCert == null)
Expand Down Expand Up @@ -606,7 +637,7 @@ public ERevoked build ()
if (bExecuteSync)
{
// Synchronize because the change of the Security.property is global
synchronized (CertificateRevocationChecker.class)
synchronized (CertificateRevocationCheckerDefaults.class)
{
aPerformer.run ();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

import java.security.GeneralSecurityException;
import java.security.cert.CertPathValidatorException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

import org.slf4j.Logger;
Expand All @@ -34,21 +34,25 @@
import com.helger.commons.concurrent.SimpleReadWriteLock;

/**
* A generic class to check certificates against OCSP and CRL servers.
* A helper class with certificate revocation check defaults. Name prior to
* v9.6.0 was <code>CertificateRevocationChecker<code>
*
* @author Philip Helger
* @since 8.5.2
*/
@ThreadSafe
public final class CertificateRevocationChecker
public final class CertificateRevocationCheckerDefaults
{
// By default only CRL is used
// https://github.com/phax/phase4/issues/124
public static final ERevocationCheckMode DEFAULT_REVOCATION_CHECK_MODE = ERevocationCheckMode.CRL;
public static final boolean DEFAULT_ALLOW_SOFT_FAIL = false;
public static final boolean DEFAULT_ALLOW_EXEC_SYNC = true;
public static final boolean DEFAULT_CACHE_REVOCATION_CHECK_RESULTS = true;

private static final Logger LOGGER = LoggerFactory.getLogger (CertificateRevocationChecker.class);
public static final Duration DEFAULT_REVOCATION_CHECK_CACHING_DURATION = Duration.ofHours (6);

private static final Logger LOGGER = LoggerFactory.getLogger (CertificateRevocationCheckerDefaults.class);

private static final SimpleReadWriteLock RW_LOCK = new SimpleReadWriteLock ();
@GuardedBy ("RW_LOCK")
Expand All @@ -63,8 +67,10 @@ public final class CertificateRevocationChecker
private static final AtomicBoolean ALLOW_EXEC_SYNC = new AtomicBoolean (DEFAULT_ALLOW_EXEC_SYNC);
@GuardedBy ("RW_LOCK")
private static CRLCache s_aDefaultCRLCache = CRLCache.createDefault ();
// Revocation checking stuff
private static final AtomicBoolean CACHE_REVOCATION_RESULTS = new AtomicBoolean (DEFAULT_CACHE_REVOCATION_CHECK_RESULTS);

private CertificateRevocationChecker ()
private CertificateRevocationCheckerDefaults ()
{}

/**
Expand Down Expand Up @@ -221,19 +227,36 @@ public static void setDefaultCRLCache (@Nonnull final CRLCache aCRLCache)
}

/**
* @return A new {@link RevocationCheckBuilder} instance.
* @return <code>true</code> if OSCP results may be cached, <code>false</code>
* if not. The default is
* {@value #DEFAULT_CACHE_REVOCATION_CHECK_RESULTS}.
* @since 9.6.0
*/
public static RevocationCheckBuilder revocationCheck ()
public static boolean isCacheRevocationCheckResults ()
{
return new RevocationCheckBuilder ();
return CACHE_REVOCATION_RESULTS.get ();
}

/**
* A generic revocation check builder that works with arbitrary certificates.
* Enable or disable caching of OSCP results.
*
* @author Philip Helger
* @param bCache
* <code>true</code> to enable caching, <code>false</code> to disable
* it.
* @since 9.6.0
*/
@NotThreadSafe
public static class RevocationCheckBuilder extends AbstractRevocationCheckBuilder <RevocationCheckBuilder>
{}
public static void setCacheRevocationCheckResults (final boolean bCache)
{
CACHE_REVOCATION_RESULTS.set (bCache);
LOGGER.info ("Global cache revocation check results enabled: " + bCache);
}

/**
* @return A new {@link RevocationCheckBuilder} instance.
*/
@Deprecated (forRemoval = true, since = "9.6.0")
public static RevocationCheckBuilder revocationCheck ()
{
return new RevocationCheckBuilder ();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (C) 2015-2024 Philip Helger
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.peppol.utils;

import java.security.cert.X509Certificate;
import java.time.OffsetDateTime;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.datetime.PDTFactory;
import com.helger.commons.state.EChange;
import com.helger.commons.state.ETriState;

/**
* This is a specific helper class to check the validity of Peppol certificates
* for a specific PA. See {@link PeppolCertificateChecker} for predefined
* instances of this class.
*
* @author Philip Helger
* @since 9.6.0
*/
public final class PeppolCAChecker
{
private final TrustedCACertificates m_aTrustedCAs = new TrustedCACertificates ();
private final PeppolRevocationCache m_aRevocationCache;

/**
* Constructor
*
* @param aCACerts
* The trusted CA certificates to be used. May neither be
* <code>null</code> nor empty.
*/
public PeppolCAChecker (@Nonnull final X509Certificate... aCACerts)
{
ValueEnforcer.notNullNoNullValue (aCACerts, "CACerts");
for (final X509Certificate aCACert : aCACerts)
m_aTrustedCAs.addTrustedCACertificate (aCACert);
// The cache always uses "now" as the checking date and time
m_aRevocationCache = new PeppolRevocationCache (aCert -> new RevocationCheckBuilder ().certificate (aCert)
.validCAs (aCACerts)
.checkMode (CertificateRevocationCheckerDefaults.getRevocationCheckMode ())
.build (),
CertificateRevocationCheckerDefaults.DEFAULT_REVOCATION_CHECK_CACHING_DURATION);
}

/**
* Check if the provided certificate is a valid Peppol certificate according
* to the configured CA.
*
* @param aCert
* The certificate to be checked. May be <code>null</code>.
* @return {@link EPeppolCertificateCheckResult} and never <code>null</code>.
*/
@Nonnull
public EPeppolCertificateCheckResult checkCertificate (@Nullable final X509Certificate aCert)
{
return checkCertificate (aCert, PDTFactory.getCurrentOffsetDateTime ());
}

/**
* Check if the provided certificate is a valid Peppol certificate according
* to the configured CA.
*
* @param aCert
* The certificate to be checked. May be <code>null</code>.
* @param aCheckDT
* The check date and time to use. May be <code>null</code> which means
* "now".
* @return {@link EPeppolCertificateCheckResult} and never <code>null</code>.
*/
@Nonnull
public EPeppolCertificateCheckResult checkCertificate (@Nullable final X509Certificate aCert,
@Nullable final OffsetDateTime aCheckDT)
{
return checkCertificate (aCert, aCheckDT, ETriState.UNDEFINED, null);
}

/**
* Check if the provided certificate is a valid Peppol certificate according
* to the configured CA.
*
* @param aCert
* The certificate to be checked. May be <code>null</code>.
* @param aCheckDT
* The check date and time to use. May be <code>null</code> which means
* "now".
* @param eCacheRevocationCheckResult
* Define whether to cache the revocation check results or not. Use
* {@link ETriState#UNDEFINED} to solely use the default.
* @param eCheckMode
* Possibility to override the revocation checking mode for each check.
* May be <code>null</code> to use the global state from
* {@link CertificateRevocationCheckerDefaults#getRevocationCheckMode()}.
* @return {@link EPeppolCertificateCheckResult} and never <code>null</code>.
*/
@Nonnull
public EPeppolCertificateCheckResult checkCertificate (@Nullable final X509Certificate aCert,
@Nullable final OffsetDateTime aCheckDT,
@Nonnull final ETriState eCacheRevocationCheckResult,
@Nullable final ERevocationCheckMode eCheckMode)
{
final boolean bUseRevocationCache = eCacheRevocationCheckResult.isUndefined () ? CertificateRevocationCheckerDefaults.isCacheRevocationCheckResults ()
: eCacheRevocationCheckResult.isTrue ();

return PeppolCertificateChecker.checkCertificate (m_aTrustedCAs.getAllTrustedCAIssuers (),
bUseRevocationCache && aCheckDT == null ? m_aRevocationCache
: null,
new RevocationCheckBuilder ().certificate (aCert)
.checkDate (aCheckDT)
.validCAs (m_aTrustedCAs.getAllTrustedCACertificates ())
.checkMode (eCheckMode));
}

@Nonnull
public EChange clearRevocationCache ()
{
return m_aRevocationCache.clearCache ();
}
}
Loading

0 comments on commit 486128c

Please sign in to comment.