/* Tibisay Movil Copyright (C) 2013 Antonio Araujo (aaraujo@cenditel.gob.ve), Jose Ruiz (jruiz@cenditel.gob.ve), Fundacion Centro Nacional de Desarrollo e Investigacion en Tecnologias Libres - CENDITEL. La Fundación CENDITEL concede permiso para usar, copiar, distribuir y/o modificar este programa, reconociendo el derecho que la humanidad posee al libre acceso al conocimiento, bajo los términos de la licencia de software GPL versión 2.0 de la Free Software Foundation. Este programa se distribuye con la esperanza de que sea util, pero SIN NINGUNA GARANTIA; tampoco las implicitas garantias de MERCANTILIDAD o ADECUACION A UN PROPOSITO PARTICULAR. Para mayor información sobre los términos de la licencia ver el archivo llamado "gpl-2.0.txt" en ingles. */ package ve.gob.cenditel.tibisaymovil; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.EOFException; 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 org.spongycastle.asn1.DEROctetString; import org.spongycastle.asn1.ocsp.OCSPObjectIdentifiers; import org.spongycastle.asn1.oiw.OIWObjectIdentifiers; import org.spongycastle.asn1.x509.AccessDescription; import org.spongycastle.asn1.x509.AlgorithmIdentifier; import org.spongycastle.asn1.x509.AuthorityInformationAccess; import org.spongycastle.asn1.x509.Extension; import org.spongycastle.asn1.x509.Extensions; import org.spongycastle.asn1.x509.X509Extension; import org.spongycastle.cert.CertException; import org.spongycastle.cert.X509CertificateHolder; import org.spongycastle.cert.jcajce.JcaX509CertificateConverter; import org.spongycastle.cert.jcajce.JcaX509CertificateHolder; import org.spongycastle.cert.ocsp.BasicOCSPResp; import org.spongycastle.cert.ocsp.CertificateID; import org.spongycastle.cert.ocsp.CertificateStatus; import org.spongycastle.cert.ocsp.OCSPException; import org.spongycastle.cert.ocsp.OCSPReq; import org.spongycastle.cert.ocsp.OCSPReqBuilder; import org.spongycastle.cert.ocsp.OCSPResp; import org.spongycastle.cert.ocsp.OCSPRespBuilder; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.operator.ContentVerifierProvider; import org.spongycastle.operator.DigestCalculator; import org.spongycastle.operator.OperatorCreationException; import org.spongycastle.operator.bc.BcDigestCalculatorProvider; import org.spongycastle.operator.jcajce.JcaContentVerifierProviderBuilder; /** * Utilities to deal with X509 certificates. * * @author José M. Prieto (jmprieto@emergya.com) */ public class CertificateUtils { static { Security.addProvider(new BouncyCastleProvider()); } /** * Default OCSP responder. * * @see #checkWithOCSP(X509Certificate, X509Certificate) */ private static final String DEFAULT_OCSP_RESPONDER = "http://publicador-psc.fii.gob.ve:2560/ocsp/"; /** * Validates the status of a certificate with an OCSP responder, according to RFC 2560. The Online Certificate * Status Protocol (OCSP) enables applications to determine the (revocation) state of an identified certificate. * Uses the responder specified in the corresponding certificate extension if any or a default one. * * @param certificate the certificate to be validated * @param issuerCertificate the certificate of the issuer authority * @param responderUrl the URL of the OCSP responder service to be used * @return true if the certificate is known by the responder and good, false if it is unknown or revoked * @throws CertificateException on errors when dealing with certificates * @throws IOException on I/O errors * @throws OCSPException on OCSP protocol errors */ public static ValidationResult checkWithOCSP(X509Certificate certificate, X509Certificate issuerCertificate) throws CertificateException, IOException, OCSPException { String responderUrl = DEFAULT_OCSP_RESPONDER; X509CertificateHolder holder = new JcaX509CertificateHolder(certificate); Extension extension = holder.getExtension(X509Extension.authorityInfoAccess); if (extension != null) { AuthorityInformationAccess authority = AuthorityInformationAccess.getInstance(extension.getParsedValue()); AccessDescription[] descriptions = authority.getAccessDescriptions(); for (AccessDescription description : descriptions) { if (AccessDescription.id_ad_ocsp.equals(description.getAccessMethod())) { responderUrl = description.getAccessLocation().getName().toString(); } } } return checkWithOCSP(certificate, issuerCertificate, responderUrl); } public static String getSubject(X509Certificate cert) { return cert.getSubjectDN().getName(); } public static String getSerial(X509Certificate cert) { return cert.getSerialNumber().toString(); } public static String getIssuer(X509Certificate cert) { return cert.getIssuerDN().getName(); } /** * Validates the status of a certificate with the specified OCSP responder, according to RFC 2560. * * @param certificate the certificate to be validated * @param issuerCertificate the certificate of the issuer authority * @param responderUrl the URL of the OCSP responder service to be used * @return true if the certificate is known by the responder and good, false if it is unknown or revoked * @throws CertificateException on errors when dealing with certificates * @throws IOException on I/O errors * @throws OCSPException on OCSP protocol errors */ static ValidationResult checkWithOCSP(X509Certificate certificate, X509Certificate issuerCertificate, String responderUrl) throws CertificateException, IOException, OCSPException { try { OCSPReq request = buildOCSPRequest(certificate, issuerCertificate); URL url = new URL(responderUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("Content-Type", "application/ocsp-request"); connection.setRequestProperty("Accept", "application/ocsp-response"); connection.setDoOutput(true); DataOutputStream output = new DataOutputStream(new BufferedOutputStream(connection.getOutputStream())); output.write(request.getEncoded()); output.flush(); output.close(); int statusCode = connection.getResponseCode(); if (statusCode == 301 || statusCode == 302 || statusCode == 307) { return checkWithOCSP(certificate, issuerCertificate, connection.getHeaderField("Location")); } else if (statusCode < 200 || statusCode >= 300) { throw new IOException(); } return isOCSPResponseValid(new OCSPResp( getBytesFromStream((InputStream) connection.getContent(), connection.getContentLength())), issuerCertificate); } catch (OperatorCreationException e) { //throw new RuntimeException(e); throw new CertificateException(e); } catch (CertException e) { //throw new RuntimeException(e); throw new CertificateException(e); } } /** * Checks a response received from the OCSP service. * * @param response the OCSP service response * @param issuerCertificate X509Certificate of the certificate being checked * @return true if the certificate is known by the responder and good, false if it is unknown or revoked * @throws CertificateException on errors when dealing with certificates * @throws IOException on I/O errors * @throws OCSPException on OCSP protocol errors * @throws OperatorCreationException on errors from the crypto library (hopefully never) * @throws CertException on certificate handling errors */ private static ValidationResult isOCSPResponseValid(OCSPResp response, X509Certificate issuerCertificate) throws IOException, OCSPException, OperatorCreationException, CertificateException, CertException { if (response.getStatus() != OCSPRespBuilder.SUCCESSFUL) { throw new IOException(); } BasicOCSPResp validations = (BasicOCSPResp) response.getResponseObject(); assert validations.getResponses().length == 1; CertificateStatus status = validations.getResponses()[0].getCertStatus(); X509CertificateHolder signer = validations.getCerts()[0]; ContentVerifierProvider verifier = new JcaContentVerifierProviderBuilder().build(signer); //return (status == null) && validations.isSignatureValid(verifier) && isValidSigner(signer, issuerCertificate); return new ValidationResult(status, validations.isSignatureValid(verifier), isValidSigner(signer, issuerCertificate)); } /** * Check if the OCSP response has been signed by a legitimate authority. From the RFC: * * All definitive response messages SHALL be digitally signed. The key * used to sign the response MUST belong to one of the following: * * -- the CA who issued the certificate in question * -- a Trusted Responder whose public key is trusted by the requester * -- a CA Designated Responder (Authorized Responder) who holds a * specially marked certificate issued directly by the CA, indicating * that the responder may issue OCSP responses for that CA * * @param signer * @param issuerCertificate * @return true if the signer is valid. false otherwise * @throws CertificateException on certificate handling errors * @throws CertException if the signature cannot be processed or is inappropriate * @throws OperatorCreationException on errors from the crypto library (hopefully never) */ private static boolean isValidSigner(X509CertificateHolder signer, X509Certificate issuerCertificate) throws CertificateException, CertException, OperatorCreationException { X509Certificate signerCertificate = new JcaX509CertificateConverter().getCertificate(signer); ContentVerifierProvider verifier = new JcaContentVerifierProviderBuilder().build(issuerCertificate); return signerCertificate.equals(issuerCertificate) || signer.isSignatureValid(verifier); } /** * Build a request to validate a certificate with an OCSP responder. * * @param certificate the certificate to be validated * @param issuerCertificate the certificate of the issuer authority * @return the request object * @throws CertificateException on errors when dealing with certificates * @throws OCSPException on OCSP protocol errors * @throws OperatorCreationException on errors from the crypto library (hopefully never) */ private static OCSPReq buildOCSPRequest(X509Certificate certificate, X509Certificate issuerCertificate) throws OperatorCreationException, CertificateEncodingException, OCSPException { DigestCalculator sha1; sha1 = new BcDigestCalculatorProvider().get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)); X509CertificateHolder issuer = new JcaX509CertificateHolder(issuerCertificate); CertificateID id = new CertificateID(sha1, issuer, certificate.getSerialNumber()); OCSPReqBuilder gen = new OCSPReqBuilder(); BigInteger nonce = BigInteger.valueOf(System.currentTimeMillis()); Extension extension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, new DEROctetString( nonce.toByteArray())); gen.addRequest(id); gen.setRequestExtensions(new Extensions(new Extension[] { extension })); OCSPReq request = gen.build(); return request; } /** * Read data from a stream. * * @param stream the stream to be read * @param length number of bytes to read * @return the data read * @throws EOFException if the file has less than length bytes available to read * @throws IOException on I/O errors */ private static byte[] getBytesFromStream(InputStream stream, int length) throws IOException { byte[] bytes = new byte[length]; int numRead = 0; for (int offset = 0; offset < length; offset += (numRead = stream.read(bytes, offset, length - offset))) { if (numRead < 0) { throw new EOFException(); } } stream.close(); return bytes; } }