Last active
June 3, 2020 07:45
-
-
Save as1an/c84b13b0afc740dde14d90c8b0d5e88d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package kz.gov.pki.kalkan.pkix.checker; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.math.BigInteger; | |
import java.net.HttpURLConnection; | |
import java.net.URL; | |
import java.net.URLEncoder; | |
import java.security.cert.CertPathBuilder; | |
import java.security.cert.CertPathValidator; | |
import java.security.cert.CertPathValidatorException; | |
import java.security.cert.CertStore; | |
import java.security.cert.Certificate; | |
import java.security.cert.CollectionCertStoreParameters; | |
import java.security.cert.PKIXBuilderParameters; | |
import java.security.cert.PKIXCertPathChecker; | |
import java.security.cert.TrustAnchor; | |
import java.security.cert.X509CertSelector; | |
import java.security.cert.X509Certificate; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.Date; | |
import java.util.HashSet; | |
import java.util.Hashtable; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import javax.security.auth.x500.X500Principal; | |
import kz.gov.pki.kalkan.asn1.ASN1InputStream; | |
import kz.gov.pki.kalkan.asn1.DERObject; | |
import kz.gov.pki.kalkan.asn1.DEROctetString; | |
import kz.gov.pki.kalkan.asn1.ocsp.OCSPObjectIdentifiers; | |
import kz.gov.pki.kalkan.asn1.ocsp.ResponderID; | |
import kz.gov.pki.kalkan.asn1.x509.AccessDescription; | |
import kz.gov.pki.kalkan.asn1.x509.AuthorityInformationAccess; | |
import kz.gov.pki.kalkan.asn1.x509.KeyPurposeId; | |
import kz.gov.pki.kalkan.asn1.x509.X509Extension; | |
import kz.gov.pki.kalkan.asn1.x509.X509Extensions; | |
import kz.gov.pki.kalkan.asn1.x509.X509Name; | |
import kz.gov.pki.kalkan.asn1.x509.X509ObjectIdentifiers; | |
import kz.gov.pki.kalkan.exception.KalkanException; | |
import kz.gov.pki.kalkan.exception.OCSPCode; | |
import kz.gov.pki.kalkan.ocsp.BasicOCSPResp; | |
import kz.gov.pki.kalkan.ocsp.CertificateID; | |
import kz.gov.pki.kalkan.ocsp.OCSPException; | |
import kz.gov.pki.kalkan.ocsp.OCSPReq; | |
import kz.gov.pki.kalkan.ocsp.OCSPReqGenerator; | |
import kz.gov.pki.kalkan.ocsp.OCSPResp; | |
import kz.gov.pki.kalkan.ocsp.OCSPRespStatus; | |
import kz.gov.pki.kalkan.ocsp.RespID; | |
import kz.gov.pki.kalkan.ocsp.RevokedStatus; | |
import kz.gov.pki.kalkan.ocsp.SingleResp; | |
import kz.gov.pki.kalkan.ocsp.UnknownStatus; | |
import kz.gov.pki.kalkan.util.encoders.Base64; | |
/** | |
* | |
* Класс реализует stateless {@link PKIXCertPathChecker} для проверки статуса сертификата по OCSP. | |
* Рекомендуется использовать дополнительно к {@link CertPathBuilder} или {@link CertPathValidator}, | |
* используя {@link PKIXBuilderParameters}. Можно обращаться напрямую для получения {@link BasicOCSPResp} | |
* для конкретного сертификата. | |
* Адрес сервиса берется из расширений сертификата, либо из системного окружения, | |
* если указано свойство {@link KNCAOCSPChecker#OCSP_RESPONDER_URL_PROP}. | |
* Проверка проводится только для конечного сертификата. | |
* | |
*/ | |
public class KNCAOCSPChecker extends PKIXCertPathChecker { | |
public static final String OCSP_ALLOWED_PERIOD_PROP = "knca.ocsp.allowedperiod"; | |
public static final String OCSP_RESPONDER_URL_PROP = "knca.ocspresponderURL"; | |
private static final int TIMEOUT = 5000; | |
public static final String REV_REASON = "reason"; | |
public static final String REV_TIME = "time"; | |
private static int allowedPeriod; | |
private Map<X500Principal, X509Certificate> caCertsMap; | |
private String httpMethod; | |
private byte[] nonce; | |
private String hashAlgOid; | |
private String provider; | |
private BasicOCSPResp basicOCSPResponse; | |
static { | |
Integer allowedPeriodPropValue = 300000; | |
try { | |
allowedPeriodPropValue = Integer.valueOf(System.getProperty(OCSP_ALLOWED_PERIOD_PROP, allowedPeriodPropValue.toString())); | |
} catch (NumberFormatException nfe) { | |
} | |
allowedPeriod = allowedPeriodPropValue >= 3600000 ? 3600000 : allowedPeriodPropValue; | |
} | |
/** | |
* Конструктор, с оптимальными параметрами по умолчанию | |
* @param caCertsMap | |
*/ | |
public KNCAOCSPChecker(Map<X500Principal, X509Certificate> caCertsMap) { | |
this(caCertsMap, CertificateID.HASH_SHA1, "GET", null, "KALKAN"); | |
} | |
/** | |
* Основной конструктор | |
* | |
* @param caCertsMap таблица корневых сертификатов {@link X509Certificate#getSubjectX500Principal()} = {@link X509Certificate} | |
* @param hashAlgOid OID алгоритма хэширования для {@link CertificateID} | |
* @param httpMethod тип метода HTTP ("GET", "POST") | |
* @param nonce опциональное случайное значение | |
* @param provider провайдер | |
*/ | |
public KNCAOCSPChecker(Map<X500Principal, X509Certificate> caCertsMap, String hashAlgOid, String httpMethod, | |
byte[] nonce, String provider) { | |
this.caCertsMap = caCertsMap == null ? Collections.<X500Principal, X509Certificate> emptyMap() : caCertsMap; | |
this.httpMethod = httpMethod; | |
this.nonce = nonce; | |
this.hashAlgOid = hashAlgOid == null ? CertificateID.HASH_SHA1 : hashAlgOid; | |
this.provider = provider == null ? "KALKAN" : provider; | |
} | |
public void init(boolean forward) throws CertPathValidatorException { | |
if (forward) { | |
throw new CertPathValidatorException("Forward checking not supported"); | |
} | |
} | |
public boolean isForwardCheckingSupported() { | |
return false; | |
} | |
public Set<String> getSupportedExtensions() { | |
return Collections.<String> emptySet(); | |
} | |
/** | |
* Может возвращать {@link KalkanException} с кодами {@link OCSPCode}. | |
* Если {@link SingleResp} не содержит {@link SingleResp#getNextUpdate()}, | |
* то ответ будет считаться действительным в течение | |
* (текущее время + {@link KNCAOCSPChecker#allowedPeriod} ms). С помощью | |
* {@link KNCAOCSPChecker#OCSP_ALLOWED_PERIOD_PROP} можно указать значение периода, | |
* максимальное значение - 1 час. По умолчанию - 5 минут. Значение в миллисекундах. | |
*/ | |
public void check(Certificate cert, Collection<String> unresolvedCritExts) throws CertPathValidatorException { | |
X509Certificate targetCert = (X509Certificate) cert; | |
if (targetCert.getBasicConstraints() >= 0) | |
return; | |
String ocspUrlStr = System.getProperty(OCSP_RESPONDER_URL_PROP); | |
try { | |
X509Certificate issuerCert = caCertsMap.get(targetCert.getIssuerX500Principal()); | |
if (issuerCert == null) { | |
throw new KalkanException(OCSPCode.TARGET_CERT_ISSUER_NOT_FOUND); | |
} | |
if (ocspUrlStr == null) { | |
ocspUrlStr = getOCSPAccessLocation(targetCert); | |
} | |
if (ocspUrlStr == null) { | |
throw new KalkanException(OCSPCode.OCSP_URL_NOT_DEFINED); | |
} | |
OCSPReq ocspReq = generateOCSPRequest(targetCert.getSerialNumber(), issuerCert); | |
byte[] ocspReqBytes = ocspReq.getEncoded(); | |
OCSPResp ocspResp = sendOCSPRequest(ocspUrlStr, ocspReqBytes); | |
if (ocspResp.getStatus() != OCSPRespStatus.SUCCESSFUL) { | |
throw new KalkanException(OCSPCode.OCSP_RESP_NOT_SUCCESSFUL).set("status", ocspResp.getStatus()); | |
} | |
BasicOCSPResp basicOCSPResp = (BasicOCSPResp) ocspResp.getResponseObject(); | |
basicOCSPResponse = basicOCSPResp; | |
if (nonce != null) { | |
byte[] respNonceExt = basicOCSPResp.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nonce.getId()); | |
ASN1InputStream asn1In = new ASN1InputStream(respNonceExt); | |
DERObject derObj = asn1In.readObject(); | |
asn1In.close(); | |
byte[] extV = DEROctetString.getInstance(derObj).getOctets(); | |
asn1In = new ASN1InputStream(extV); | |
derObj = asn1In.readObject(); | |
asn1In.close(); | |
if (!Arrays.equals(nonce, DEROctetString.getInstance(derObj).getOctets())) { | |
throw new KalkanException(OCSPCode.NONCES_NOT_EQUAL); | |
} | |
} | |
if (basicOCSPResp.getCerts(provider).length == 0) { | |
throw new KalkanException(OCSPCode.OCSP_EMPTY_CERT_LIST); | |
} | |
X509Certificate ocspCert = null; | |
for (int i = 0; i < basicOCSPResp.getCerts(provider).length; i++) { | |
X509Certificate respCert = basicOCSPResp.getCerts(provider)[i]; | |
RespID responderID = new RespID(new ResponderID(new X509Name(respCert.getSubjectDN().getName()))); | |
if (responderID.equals(basicOCSPResp.getResponderId())) { | |
ocspCert = respCert; | |
break; | |
} | |
} | |
if (ocspCert == null) { | |
throw new KalkanException(OCSPCode.OCSP_CERT_NOT_PRESENT); | |
} | |
checkOCSPCert(ocspCert); | |
if (!basicOCSPResp.verify(ocspCert.getPublicKey(), provider)) { | |
throw new KalkanException(OCSPCode.OCSP_RESP_NOT_VERIFIED); | |
} | |
if (basicOCSPResp.getResponses().length == 0) { | |
throw new KalkanException(OCSPCode.OCSP_SINGLERESP_NOT_PRESENT); | |
} | |
SingleResp singleResp = basicOCSPResp.getResponses()[0]; | |
Date thisUpdate = singleResp.getThisUpdate(); | |
if (thisUpdate == null) { | |
throw new KalkanException(OCSPCode.THIS_UPDATE_NOT_SATISFIED); | |
} | |
Date nowDate = new Date(); | |
if (nowDate.compareTo(thisUpdate) <= 0) { | |
throw new KalkanException(OCSPCode.THIS_UPDATE_NOT_SATISFIED); | |
} | |
Date nextUpdate = singleResp.getNextUpdate(); | |
if (nextUpdate != null) { | |
if (nowDate.compareTo(nextUpdate) >= 0) { | |
throw new KalkanException(OCSPCode.NEXT_UPDATE_NOT_SATISFIED); | |
} | |
} else { | |
Date allowedDate = new Date(thisUpdate.getTime() + allowedPeriod); | |
if (nowDate.compareTo(allowedDate) >= 0) { | |
throw new KalkanException(OCSPCode.ALLOWED_PERIOD_NOT_SATISFIED); | |
} | |
} | |
if (!singleResp.getCertID().equals(ocspReq.getRequestList()[0].getCertID())) { | |
throw new KalkanException(OCSPCode.CERTID_NOT_EQUAL); | |
} | |
Object status = singleResp.getCertStatus(); | |
if (status != null) { | |
if (status instanceof RevokedStatus) { | |
RevokedStatus rev = (RevokedStatus) status; | |
int reason = rev.hasRevocationReason() ? rev.getRevocationReason() : 0; | |
throw new KalkanException(OCSPCode.STATUS_REVOKED).set(REV_TIME, rev.getRevocationTime()) | |
.set(REV_REASON, reason); | |
} | |
if (status instanceof UnknownStatus) { | |
throw new KalkanException(OCSPCode.STATUS_UNKNOWN); | |
} | |
} | |
} catch (Exception e) { | |
throw new CertPathValidatorException(e); | |
} | |
} | |
private void checkOCSPCert(X509Certificate ocspCert) throws KalkanException { | |
X509Certificate trustedCert = caCertsMap.get(ocspCert.getIssuerX500Principal()); | |
if (trustedCert == null) { | |
throw new KalkanException(OCSPCode.OCSP_CERT_ISSUER_NOT_FOUND); | |
} | |
for (int i = 0; i < caCertsMap.size(); i++) { | |
if (trustedCert.getIssuerX500Principal().equals(trustedCert.getSubjectX500Principal())) { | |
break; | |
} | |
X509Certificate topTrustedCert = caCertsMap.get(trustedCert.getIssuerX500Principal()); | |
if (topTrustedCert != null) { | |
trustedCert = topTrustedCert; | |
} else { | |
break; | |
} | |
} | |
Set<TrustAnchor> trustedAnchors = new HashSet<TrustAnchor>(); | |
trustedAnchors.add(new TrustAnchor(trustedCert, null)); | |
List<Certificate> chainList = new ArrayList<Certificate>(caCertsMap.values()); | |
chainList.add(ocspCert); | |
CollectionCertStoreParameters certStoreParams = new CollectionCertStoreParameters(chainList); | |
try { | |
CertStore certStore = CertStore.getInstance("Collection", certStoreParams, provider); | |
X509CertSelector selector = new X509CertSelector(); | |
selector.setCertificate(ocspCert); | |
Set<String> extKeyUsageSet = new HashSet<String>(); | |
extKeyUsageSet.add(KeyPurposeId.id_kp_OCSPSigning.getId()); | |
selector.setExtendedKeyUsage(extKeyUsageSet); | |
CertPathBuilder certPathBuilder = CertPathBuilder.getInstance("PKIX", provider); | |
PKIXBuilderParameters builderParams = new PKIXBuilderParameters(trustedAnchors, selector); | |
builderParams.setSigProvider(provider); | |
builderParams.addCertStore(certStore); | |
builderParams.setRevocationEnabled(false); | |
certPathBuilder.build(builderParams); | |
} catch (Exception e) { | |
throw new KalkanException(e, OCSPCode.OCSP_CERT_NOT_VALIDATED); | |
} | |
if (ocspCert.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) == null) { | |
throw new KalkanException(OCSPCode.NOCHECK_EXT_NOT_FOUND); | |
} | |
} | |
private OCSPResp sendOCSPRequest(String ocspUrlStr, byte[] ocspReqBytes) throws IOException, OCSPException { | |
HttpURLConnection con = null; | |
URL ocspUrl; | |
InputStream in = null; | |
OutputStream os = null; | |
OCSPResp ocspResp = null; | |
try { | |
if ("POST".equals(httpMethod)) { | |
ocspUrl = new URL(ocspUrlStr); | |
con = (HttpURLConnection) ocspUrl.openConnection(); | |
con.setConnectTimeout(TIMEOUT); | |
con.setReadTimeout(TIMEOUT); | |
con.setDoOutput(true); | |
con.setRequestMethod("POST"); | |
con.setRequestProperty("Content-Type", "application/ocsp-request"); | |
os = con.getOutputStream(); | |
os.write(ocspReqBytes); | |
} else if (ocspUrlStr.endsWith("/")) { | |
ocspUrl = new URL(ocspUrlStr + URLEncoder.encode(Base64.encodeStr(ocspReqBytes), "UTF-8")); | |
con = (HttpURLConnection) ocspUrl.openConnection(); | |
} else { | |
ocspUrl = new URL(ocspUrlStr + "/" + URLEncoder.encode(Base64.encodeStr(ocspReqBytes), "UTF-8")); | |
con = (HttpURLConnection) ocspUrl.openConnection(); | |
} | |
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { | |
throw new OCSPException("HTTP error code: " + con.getResponseCode()); | |
} | |
if (!"application/ocsp-response".equals(con.getContentType())) { | |
throw new OCSPException("Wrong content-type: " + con.getContentType()); | |
} | |
in = con.getInputStream(); | |
ocspResp = new OCSPResp(in); | |
} finally { | |
if (in != null) { | |
try { | |
in.close(); | |
} catch (IOException e) { | |
System.err.println("[KNCACertPathChecker] Error closing instream: " + e.getMessage()); | |
} | |
} | |
if (os != null) { | |
try { | |
os.close(); | |
} catch (IOException e) { | |
System.err.println("[KNCACertPathChecker] Error closing outstream: " + e.getMessage()); | |
} | |
} | |
if (con != null) { | |
con.disconnect(); | |
} | |
} | |
return ocspResp; | |
} | |
private String getOCSPAccessLocation(X509Certificate targetCert) throws IOException, KalkanException { | |
String ret = null; | |
byte[] authInfoAccessExt = targetCert.getExtensionValue(X509Extensions.AuthorityInfoAccess.getId()); | |
if (authInfoAccessExt == null) { | |
throw new KalkanException(OCSPCode.AUTH_INFO_ACCESS_NOT_FOUND); | |
} | |
ASN1InputStream asn1In = new ASN1InputStream(authInfoAccessExt); | |
DEROctetString octetString = (DEROctetString) asn1In.readObject(); | |
asn1In.close(); | |
ASN1InputStream seqIn = new ASN1InputStream(octetString.getOctets()); | |
DERObject derObj = seqIn.readObject(); | |
seqIn.close(); | |
AuthorityInformationAccess authInfoAccess = AuthorityInformationAccess.getInstance(derObj); | |
AccessDescription[] accessDescriptions = authInfoAccess.getAccessDescriptions(); | |
for (int i = 0; i < accessDescriptions.length; i++) { | |
if (accessDescriptions[i].getAccessMethod().equals(X509ObjectIdentifiers.ocspAccessMethod)) { | |
ret = accessDescriptions[i].getAccessLocation().getName().toString(); | |
} | |
} | |
return ret; | |
} | |
private OCSPReq generateOCSPRequest(BigInteger serialNumber, X509Certificate issuerCert) throws OCSPException { | |
OCSPReqGenerator gen = new OCSPReqGenerator(); | |
CertificateID certId = new CertificateID(hashAlgOid, issuerCert, serialNumber, provider); | |
gen.addRequest(certId); | |
if (nonce != null) { | |
Hashtable exts = new Hashtable(); | |
X509Extension nonceExt = new X509Extension(false, new DEROctetString(new DEROctetString(nonce))); | |
exts.put(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, nonceExt); | |
gen.setRequestExtensions(new X509Extensions(exts)); | |
} | |
OCSPReq req = gen.generate(); | |
return req; | |
} | |
/** | |
* Возвращает ответ только при непосредственном вызове {@link KNCAOCSPChecker#check(Certificate)} | |
* @return {@link BasicOCSPResp} | |
*/ | |
public BasicOCSPResp getBasicOCSPResponse() { | |
return basicOCSPResponse; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment