Skip to content

Instantly share code, notes, and snippets.

@as1an
Last active June 3, 2020 07:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save as1an/c84b13b0afc740dde14d90c8b0d5e88d to your computer and use it in GitHub Desktop.
Save as1an/c84b13b0afc740dde14d90c8b0d5e88d to your computer and use it in GitHub Desktop.
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