-
Notifications
You must be signed in to change notification settings - Fork 138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OCSP responder to serve status check for itself using latest CRL #4504
Changes from 7 commits
b9a9eb0
57df77e
e6a1e0c
7ad255e
d54adc9
1615339
2b31525
4366f72
584cc11
3513512
47f268c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// --- BEGIN COPYRIGHT BLOCK --- | ||
// This program is free software; you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation; version 2 of the License. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License along | ||
// with this program; if not, write to the Free Software Foundation, Inc., | ||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
// | ||
// (C) 2023 Red Hat, Inc. | ||
// All rights reserved. | ||
// --- END COPYRIGHT BLOCK --- | ||
package com.netscape.cms.ocsp; | ||
|
||
import java.security.InvalidKeyException; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.Security; | ||
import java.security.SignatureException; | ||
import java.security.cert.CertificateException; | ||
import java.security.cert.X509CRLEntry; | ||
import java.util.Enumeration; | ||
|
||
import org.mozilla.jss.crypto.X509Certificate; | ||
import org.mozilla.jss.netscape.security.x509.X509CRLImpl; | ||
import org.mozilla.jss.netscape.security.x509.X509CertImpl; | ||
import org.mozilla.jss.ssl.SSLCertificateApprovalCallback; | ||
|
||
import com.netscape.certsrv.base.EBaseException; | ||
import com.netscape.certsrv.dbs.crldb.ICRLIssuingPointRecord; | ||
|
||
public class CRLLdapValidator implements SSLCertificateApprovalCallback { | ||
|
||
public static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CRLLdapValidator.class); | ||
|
||
private LDAPStore crlStore; | ||
|
||
|
||
|
||
public CRLLdapValidator(LDAPStore crlStore) { | ||
super(); | ||
this.crlStore = crlStore; | ||
} | ||
|
||
|
||
@Override | ||
public boolean approve(X509Certificate certificate, ValidityStatus currentStatus) { | ||
logger.info("CRLLdapValidator: validate of peer's certificate for the connection " + certificate.getSubjectDN()); | ||
ICRLIssuingPointRecord pt = null; | ||
try { | ||
Enumeration<ICRLIssuingPointRecord> eCRL = crlStore.searchAllCRLIssuingPointRecord(-1); | ||
while (eCRL.hasMoreElements() && pt == null) { | ||
ICRLIssuingPointRecord tPt = eCRL.nextElement(); | ||
logger.debug("CRLLdapValidator: CRL check issuer " + tPt.getId()); | ||
if(tPt.getId().equals(certificate.getIssuerDN().toString())) { | ||
try { | ||
X509CertImpl caCert = new X509CertImpl(tPt.getCACert()); | ||
X509CertImpl certToVerify = new X509CertImpl(certificate.getEncoded()); | ||
certToVerify.verify(caCert.getPublicKey(), Security.getProvider("Mozilla-JSS")); | ||
pt = tPt; | ||
} catch (CertificateException | InvalidKeyException | NoSuchAlgorithmException | ||
| SignatureException e) { | ||
logger.error("CRLLdapValidator: issuer certificate cannot verify the certificate signature." ); | ||
} | ||
} | ||
} | ||
} catch (EBaseException e) { | ||
logger.error("CRLLdapValidator: problem find CRL issuing point. " + e.getMessage(), e); | ||
return false; | ||
} | ||
if (pt == null) { | ||
logger.error("CRLLdapValidator: CRL issuing point not found for " + certificate.getIssuerDN()); | ||
return false; | ||
} | ||
try { | ||
X509CRLImpl crl = new X509CRLImpl(pt.getCRL()); | ||
X509CRLEntry crlentry = crl.getRevokedCertificate(certificate.getSerialNumber()); | ||
|
||
if (crlentry == null) { | ||
if (crlStore.isNotFoundGood()) { | ||
return true; | ||
} | ||
} | ||
} catch (Exception e) { | ||
logger.error("CRLLdapValidator: crl check error. " + e.getMessage(), e); | ||
} | ||
logger.info("CRLLdapValidator: peer certificate not valid"); | ||
return false; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,6 @@ | |
// --- END COPYRIGHT BLOCK --- | ||
package com.netscape.cms.ocsp; | ||
|
||
import java.lang.Integer; | ||
import java.math.BigInteger; | ||
import java.security.MessageDigest; | ||
import java.security.cert.X509CRL; | ||
|
@@ -82,6 +81,7 @@ public class LDAPStore implements IDefStore, IExtendedPluginInfo { | |
private static final String DEF_CA_CERT_ATTR = "cACertificate;binary"; | ||
private static final String PROP_HOST = "host"; | ||
private static final String PROP_PORT = "port"; | ||
private static final String PROP_VALIDATE_CONNECTION_WITH_CRL = "validateConnCertWithCRL"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The option When it is set to If the option There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about put the info right here in the code where PROP_VALIDATE_CONNECTION_WITH_CRL = "validateConnCertWithCRL" is assigned? Comments in code are longer lasting and provide info others later. I will also be using it for the doc changes. |
||
|
||
private final static String PROP_NOT_FOUND_GOOD = "notFoundAsGood"; | ||
private final static String PROP_INCLUDE_NEXT_UPDATE = | ||
|
@@ -94,6 +94,8 @@ public class LDAPStore implements IDefStore, IExtendedPluginInfo { | |
private String mCACertAttr = null; | ||
protected Hashtable<String, Long> mReqCounts = new Hashtable<>(); | ||
private Hashtable<X509CertImpl, X509CRLImpl> mCRLs = new Hashtable<>(); | ||
private boolean mValidateConnection = true; | ||
|
||
|
||
/** | ||
* Constructs the default store. | ||
|
@@ -137,6 +139,7 @@ public void init(IConfigStore config, DBSubsystem dbSubsystem) throws EBaseExcep | |
DEF_CA_CERT_ATTR); | ||
mByName = mConfig.getBoolean(PROP_BY_NAME, true); | ||
|
||
mValidateConnection = mConfig.getBoolean(PROP_VALIDATE_CONNECTION_WITH_CRL, true); | ||
} | ||
|
||
/** | ||
|
@@ -238,6 +241,9 @@ public void startup() throws EBaseException { | |
|
||
updater.start(); | ||
} | ||
if(mValidateConnection) { | ||
CMS.getCMSEngine().setApprovalCallback(new CRLLdapValidator(this)); | ||
} | ||
} | ||
|
||
@Override | ||
|
@@ -490,6 +496,11 @@ public void setConfigParameters(NameValuePairs pairs) | |
mConfig.put(key, pairs.get(key)); | ||
} | ||
} | ||
|
||
public boolean isCRLCheckAvailable() { | ||
return mValidateConnection; | ||
} | ||
|
||
} | ||
|
||
class CRLUpdater extends Thread { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,10 +18,27 @@ | |
|
||
package org.dogtagpki.server.ocsp; | ||
|
||
import java.security.InvalidKeyException; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.Security; | ||
import java.security.SignatureException; | ||
import java.security.cert.CertificateException; | ||
import java.security.cert.X509CRLEntry; | ||
import java.security.cert.X509Certificate; | ||
import java.util.Enumeration; | ||
|
||
import javax.security.auth.x500.X500Principal; | ||
import javax.servlet.annotation.WebListener; | ||
|
||
import org.mozilla.jss.netscape.security.x509.X509CRLImpl; | ||
import org.mozilla.jss.netscape.security.x509.X509CertImpl; | ||
import org.mozilla.jss.ssl.SSLCertificateApprovalCallback.ValidityStatus; | ||
|
||
import com.netscape.certsrv.base.EBaseException; | ||
import com.netscape.certsrv.base.IConfigStore; | ||
import com.netscape.certsrv.base.ISubsystem; | ||
import com.netscape.certsrv.dbs.crldb.ICRLIssuingPointRecord; | ||
import com.netscape.cms.ocsp.LDAPStore; | ||
import com.netscape.cmscore.apps.CMS; | ||
import com.netscape.cmscore.apps.CMSEngine; | ||
import com.netscape.cmscore.apps.EngineConfig; | ||
|
@@ -63,5 +80,122 @@ public void initSubsystem(ISubsystem subsystem, IConfigStore subsystemConfig) th | |
} | ||
|
||
super.initSubsystem(subsystem, subsystemConfig); | ||
if (subsystem instanceof OCSPAuthority) { | ||
subsystem.startup(); | ||
} | ||
} | ||
|
||
|
||
protected void startupSubsystems() throws Exception { | ||
|
||
for (ISubsystem subsystem : subsystems.values()) { | ||
logger.info("CMSEngine: Starting " + subsystem.getId() + " subsystem"); | ||
if (!(subsystem instanceof OCSPAuthority)) | ||
subsystem.startup(); | ||
} | ||
|
||
// global admin servlet. (anywhere else more fit for this ?) | ||
} | ||
@Override | ||
protected void initSequence() throws Exception { | ||
|
||
initDebug(); | ||
init(); | ||
initPasswordStore(); | ||
initSubsystemListeners(); | ||
initSecurityProvider(); | ||
initPluginRegistry(); | ||
initLogSubsystem(); | ||
initDatabase(); | ||
initJssSubsystem(); | ||
initDBSubsystem(); | ||
initUGSubsystem(); | ||
initOIDLoaderSubsystem(); | ||
initX500NameSubsystem(); | ||
// skip TP subsystem; | ||
// problem in needing dbsubsystem in constructor. and it's not used. | ||
initRequestSubsystem(); | ||
|
||
|
||
startupSubsystems(); | ||
|
||
initAuthSubsystem(); | ||
initAuthzSubsystem(); | ||
initJobsScheduler(); | ||
|
||
configureAutoShutdown(); | ||
configureServerCertNickname(); | ||
configureExcludedLdapAttrs(); | ||
|
||
initSecurityDomain(); | ||
} | ||
|
||
@Override | ||
public boolean isRevoked(X509Certificate[] certificates) { | ||
LDAPStore crlStore = null; | ||
for (ISubsystem subsystem : subsystems.values()) { | ||
if (subsystem instanceof OCSPAuthority) { | ||
OCSPAuthority ocsp = (OCSPAuthority) subsystem; | ||
if (ocsp.getDefaultStore() instanceof LDAPStore) { | ||
crlStore = (LDAPStore) ocsp.getDefaultStore(); | ||
} | ||
break; | ||
} | ||
} | ||
|
||
if (crlStore == null || !crlStore.isCRLCheckAvailable()) { | ||
return super.isRevoked(certificates); | ||
} | ||
|
||
for (X509Certificate cert: certificates) { | ||
if(!crlCertValid(crlStore, cert, null)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
|
||
} | ||
|
||
|
||
private boolean crlCertValid(LDAPStore crlStore, X509Certificate certificate, ValidityStatus currentStatus) { | ||
logger.info("OCSPEngine: validate of peer's certificate for the connection " + certificate.getSubjectX500Principal()); | ||
ICRLIssuingPointRecord pt = null; | ||
try { | ||
Enumeration<ICRLIssuingPointRecord> eCRL = crlStore.searchAllCRLIssuingPointRecord(-1); | ||
while (eCRL.hasMoreElements() && pt == null) { | ||
ICRLIssuingPointRecord tPt = eCRL.nextElement(); | ||
logger.debug("OCSPEngine: CRL check issuer " + tPt.getId()); | ||
if(certificate.getIssuerX500Principal().equals(new X500Principal(tPt.getId()))) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment as above regarding using CA's public key hash. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same approach as above. |
||
try { | ||
X509CertImpl caCert = new X509CertImpl(tPt.getCACert()); | ||
certificate.verify(caCert.getPublicKey(), Security.getProvider("Mozilla-JSS")); | ||
pt = tPt; | ||
} catch (CertificateException | InvalidKeyException | NoSuchAlgorithmException | ||
| SignatureException e) { | ||
logger.error("OCSPEngine: issuer certificate cannot verify the certificate signature." ); | ||
} | ||
} | ||
} | ||
} catch (EBaseException e) { | ||
logger.error("OCSPEngine: problem find CRL issuing point for " + certificate.getIssuerX500Principal().toString()); | ||
return false; | ||
} | ||
if (pt == null) { | ||
logger.error("OCSPEngine: CRL issuing point not found for " + certificate.getIssuerX500Principal().toString()); | ||
return false; | ||
} | ||
try { | ||
X509CRLImpl crl = new X509CRLImpl(pt.getCRL()); | ||
X509CRLEntry crlentry = crl.getRevokedCertificate(certificate.getSerialNumber()); | ||
|
||
if (crlentry == null && crlStore.isNotFoundGood()) { | ||
return true; | ||
} | ||
} catch (Exception e) { | ||
logger.error("OCSPEngine: crl check error. " + e.getMessage()); | ||
} | ||
logger.info("OCSPEngine: peer certificate not valid"); | ||
return false; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain about issuerDN string comparison here. If you look at the processRequest(...) method in ocsp/src/main/java/com/netscape/cms/ocsp/LDAPStore.java, it uses the CA's public key hash as key to locate the right CRL, which is more assuring, isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have implemented this using the CheckCertServlet as reference where the
ICRLIssuingPointRecord
is identified using the common name. Nevertheless, using the key hash is definitely a stronger mechanism. I will try to use theprocessRequest
method or extract some code in a separate method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ladycfu I did not find an easy way to get the issuer key hash from the certificate but to be more confident I have verified that the CA cert is the one signing the certificate. The test is done only if the DNs match to avoid the overhead of encoding/decoding certificates. I have performed the operation considering Mozilla-JSS provider is available. If this is not always the case some extra step are needed. Is this approach viable for the job?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @fmarco76 , I think in general, it's a better practice to avoid string comparison when it comes to DN names, due to encoding differences. Looking closely, the processRequest was for processing an ocsp request which contains the CertID, so it's not exactly what we have here. We have the peer cert, X509Certificate certificate, and we have the ca cert, X509CertImpl ca_certImpl = new X509CertImpl(tPt.getCACert()); and we can get the ca's public key PublicKey ca_pubkey = ca_certImpl.getPublicKey(); where PublicKey is java.security.PublicKey. Meanwhile, the peer cert X509Certificate certificate can have it's public key gotten with PublicKey peerCert_pubkey = certificate.getPublicKey(); So now you can compare. I'm not sure where you said you did signature verification. I could be wrong, but that sounds more expensive than the PublicKey comparison. Anyway, I think this patch is as good as it gets for the time we have, so you could leave this as an improvement for later if you want if you donn't have time for it now. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC String in java have the same encoding independently of how they are stored in the certificate. The encoding is converted when they are read/write. The format could be different but this is related to the used classes so it is known (although it could change moving to different versions of Java).
The problem here is that you get the PublicKey of the CA and the PublicKey of the peer certificate. They are different so there is no way to compare. The OCSP request contain the hash of the issuer PublicKey so it is possible to get the hash of the CA PublicKey and compare.
From the peer certificate I found no way to get the issuer PublicKey to compare with the CA.
The approach I implemented is in line 61-63 of CRLLdapValidator.
It is definitely more expensive of the PublicKey comparison and this is the reason I do only if the DN match. However, considering a low number of CA for an OCSP we could skip the DN match and do always the signature check. Is this assumption correct?
I can work on this until the end of the sprint so we still have several days before merge.