/*
* The MIT License
*
* Copyright 2014 Rui Martinho (rmartinho@gmail.com), António Braz (antoniocbraz@gmail.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.poreid.verify.ocsp;
/**
*
* @author POReID
*/
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
import java.util.Optional;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.CertException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.RevokedStatus;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.x509.extension.X509ExtensionUtil;
public class OCSPClient {
private static byte[] sentNonce;
private final X509Certificate issuer;
private final X509Certificate certificate;
private URL url;
private RevokedStatus revokedStatus = null;
public OCSPClient(X509Certificate issuer, X509Certificate certificate) {
this.issuer = issuer;
this.certificate = certificate;
this.url = getOcspUrlFromCertificate(certificate);
}
private OCSPReq generateOCSPRequest(X509Certificate issuerCert, BigInteger serialNumber) throws CertificateEncodingException, OperatorCreationException, OCSPException, IOException {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
OCSPReqBuilder gen = new OCSPReqBuilder();
gen.addRequest(new JcaCertificateID(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build().get(CertificateID.HASH_SHA1), issuerCert, serialNumber));
BigInteger nonce = BigInteger.valueOf(System.currentTimeMillis());
Extension ext = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, true, new DEROctetString(nonce.toByteArray()));
gen.setRequestExtensions(new Extensions(new Extension[]{ext}));
sentNonce = ext.getExtnId().getEncoded();
return gen.build();
}
/* não é obrigatório que tenha, mas os certificados do CC têm */
private URL getOcspUrlFromCertificate(X509Certificate certificate) {
byte[] octetBytes = certificate.getExtensionValue(org.bouncycastle.asn1.x509.Extension.authorityInfoAccess.getId());
if (null != octetBytes) {
try {
byte[] encoded = X509ExtensionUtil.fromExtensionValue(octetBytes).getEncoded();
ASN1Sequence seq = ASN1Sequence.getInstance(ASN1Primitive.fromByteArray(encoded));
AuthorityInformationAccess access = AuthorityInformationAccess.getInstance(seq);
for (AccessDescription accessDescription : access.getAccessDescriptions()){
if (accessDescription.getAccessMethod().equals(AccessDescription.id_ad_ocsp)){
url = new URL(accessDescription.getAccessLocation().getName().toString());
break;
}
}
} catch (IOException ignore) {
}
}
return url;
}
public Optional<RevokedStatus> getRevokedStatus(){
return Optional.ofNullable(revokedStatus);
}
public CertStatus getCertificateStatus() throws OCSPValidationException {
try {
if (null == url) {
throw new OCSPValidationException("Certificado não tem validação por OCSP");
}
byte[] encodedOcspRequest = generateOCSPRequest(issuer, certificate.getSerialNumber()).getEncoded();
HttpURLConnection httpConnection;
httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestProperty("Content-Type", "application/ocsp-request");
httpConnection.setRequestProperty("Accept", "application/ocsp-response");
httpConnection.setDoOutput(true);
try (DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(httpConnection.getOutputStream()))) {
dataOut.write(encodedOcspRequest);
dataOut.flush();
}
InputStream in = (InputStream) httpConnection.getContent();
if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new OCSPValidationException("Código HTTP recebido != 200 ["+httpConnection.getResponseCode()+"]");
}
OCSPResp ocspResponse = new OCSPResp(in);
BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject();
byte[] receivedNonce = basicResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce).getExtnId().getEncoded();
if (!Arrays.equals(receivedNonce, sentNonce)) {
throw new OCSPValidationException("Nonce na resposta ocsp não coincide com nonce do pedido ocsp");
}
X509CertificateHolder certHolder = basicResponse.getCerts()[0];
if (!basicResponse.isSignatureValid(new JcaContentVerifierProviderBuilder().setProvider("BC").build(issuer))){
if (!certHolder.isValidOn(Date.from(Instant.now()))){
throw new OCSPValidationException("Certificado não é válido na data atual");
}
// Certificado tem de ter uma Key Purpose ID for authorized responders
if (!ExtendedKeyUsage.fromExtensions(certHolder.getExtensions()).hasKeyPurposeId(KeyPurposeId.id_kp_OCSPSigning)){
throw new OCSPValidationException("Certificado não contém extensão necessária (id_kp_OCSPSigning)");
}
// Certificado tem de ser emitido pela mesma CA do certificado que estamos a verificar
if (!certHolder.isSignatureValid(new JcaContentVerifierProviderBuilder().setProvider("BC").build(issuer))){
throw new OCSPValidationException("Certificado não é assinado pelo mesmo issuer");
}
// Validar assinatura na resposta ocsp
if (!basicResponse.isSignatureValid(new JcaContentVerifierProviderBuilder().setProvider("BC").build(certHolder))){
throw new OCSPValidationException("Não foi possivel validar resposta ocsp");
}
} else {
if (!certHolder.isValidOn(Date.from(Instant.now()))){
throw new OCSPValidationException("Certificado não é válido na data atual");
}
}
// Politica de Certificados do SCEE
if (null == certHolder.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck).getExtnId()) {
throw new OCSPValidationException("Extensão id_pkix_ocsp_nocheck não encontrada no certificado (Politica de Certificados do SCEE)");
}
SingleResp[] responses = basicResponse.getResponses();
if (responses[0].getCertID().getSerialNumber().equals(certificate.getSerialNumber())) {
CertificateStatus status = responses[0].getCertStatus();
if (status == CertificateStatus.GOOD) {
return CertStatus.GOOD;
} else {
if (status instanceof RevokedStatus) {
revokedStatus = (RevokedStatus) status;
return CertStatus.REVOKED;
} else {
return CertStatus.UNKNOWN;
}
}
} else {
throw new OCSPValidationException("Número de série do certificado na resposta ocsp não coincide com número de série do certificado");
}
} catch (CertificateEncodingException | OperatorCreationException | OCSPException | IOException ex) {
throw new OCSPValidationException("Não foi possivel efetuar a validação através de OCSP (" + certificate.getSubjectX500Principal().getName() + ")", ex);
} catch (CertException | CertificateException ex) {
throw new OCSPValidationException("Não foi possivel efetuar a validação através de OCSP (" + certificate.getSubjectX500Principal().getName() + ")", ex);
}
}
public boolean checkOCSP() throws OCSPValidationException {
try {
return getCertificateStatus() == CertStatus.GOOD;
} catch (OCSPValidationException ex) {
return false;
}
}
}