Skip to content

Commit

Permalink
add OCSP support (#124)
Browse files Browse the repository at this point in the history
* Basic structure of everything. Lots more to do.

* Move toward more BC-centric processing

TODO:
- Replace manual construction of Request in #sign with OCSPReqBuilder
- Fix verification

* Verification of BasicResponses working

* There seems to be an issue with decoding objects using ASN1.decode
  and then trying to reconstitute decoded objects by calling to_der on them.
  Some data seems to be left off. Not covering it here because of scope creep.
lampad authored and kares committed Mar 9, 2017
1 parent 2e70c2b commit 96955ce
Showing 11 changed files with 2,554 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/main/java/org/jruby/ext/openssl/ASN1.java
Original file line number Diff line number Diff line change
@@ -326,6 +326,7 @@ private static void defaultObjects(final Ruby runtime) {
addObject(runtime, 184, "AES-256-CBC", "aes-256-cbc","2.16.840.1.101.3.4.1.42");
addObject(runtime, 185, "AES-256-OFB", "aes-256-ofb","2.16.840.1.101.3.4.1.43");
addObject(runtime, 186, "AES-256-CFB", "aes-256-cfb","2.16.840.1.101.3.4.1.44");
addObject(runtime, 672, "SHA256", "sha256", "2.16.840.1.101.3.4.2.1");

addObject(runtime, 660, "street", "streetAddress", "2.5.4.9");
addObject(runtime, 391, "DC", "domainComponent", "0.9.2342.19200300.100.1.25");
215 changes: 215 additions & 0 deletions src/main/java/org/jruby/ext/openssl/OCSP.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* The contents of this file are subject to the Common Public License Version 1.0
* (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.eclipse.org/legal/cpl-v10.html
*
* 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 APARTICULAR 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.
*
* Copyright (C) 2017 Donovan Lampa <donovan.lampa@gmail.com>
* Copyright (C) 2009-2017 The JRuby Team
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
*
*
* JRuby-OpenSSL includes software by The Legion of the Bouncy Castle Inc.
* Please, visit (http://bouncycastle.org/license.html) for licensing details.
*/
package org.jruby.ext.openssl;

import java.security.Security;
import java.util.HashMap;
import java.util.Map;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.builtin.IRubyObject;

/**
* OCSP
*
* @author lampad
*/
public class OCSP {

//Response has valid confirmations
private static final String _RESPONSE_STATUS_SUCCESSFUL_STR = "RESPONSE_STATUS_SUCCESSFUL";
private static final int _RESPONSE_STATUS_SUCCESSFUL = 0;
//Illegal confirmation request
private static final String _RESPONSE_STATUS_MALFORMEDREQUEST_STR = "RESPONSE_STATUS_MALFORMEDREQUEST";
private static final int _RESPONSE_STATUS_MALFORMEDREQUEST = 1;
//Internal error in issuer
private static final String _RESPONSE_STATUS_INTERNALERROR_STR = "RESPONSE_STATUS_INTERNALERROR";
private static final int _RESPONSE_STATUS_INTERNALERROR = 2;
//Try again later
private static final String _RESPONSE_STATUS_TRYLATER_STR = "RESPONSE_STATUS_TRYLATER";
private static final int _RESPONSE_STATUS_TRYLATER = 3;
//You must sign the request and resubmit
private static final String _RESPONSE_STATUS_SIGREQUIRED_STR = "RESPONSE_STATUS_SIGREQUIRED";
private static final int _RESPONSE_STATUS_SIGREQUIRED = 5;
//Your request is unauthorized.
private static final String _RESPONSE_STATUS_UNAUTHORIZED_STR = "RESPONSE_STATUS_UNAUTHORIZED";
private static final int _RESPONSE_STATUS_UNAUTHORIZED = 6;

private static final Map<Integer, String> responseMap;

//The certificate was revoked for an unknown reason
private static final int _REVOKED_STATUS_NOSTATUS = -1;
//The certificate was revoked for an unspecified reason
private static final int _REVOKED_STATUS_UNSPECIFIED = 0;
//The certificate was revoked due to a key compromise
private static final int _REVOKED_STATUS_KEYCOMPROMISE = 1;
//This CA certificate was revoked due to a key compromise
private static final int _REVOKED_STATUS_CACOMPROMISE = 2;
//The certificate subject's name or other information changed
private static final int _REVOKED_STATUS_AFFILIATIONCHANGED = 3;
//The certificate was superseded by a new certificate
private static final int _REVOKED_STATUS_SUPERSEDED = 4;
//The certificate is no longer needed
private static final int _REVOKED_STATUS_CESSATIONOFOPERATION = 5;
//The certificate is on hold
private static final int _REVOKED_STATUS_CERTIFICATEHOLD = 6;
//The certificate was previously on hold and should now be removed from the CRL
private static final int _REVOKED_STATUS_REMOVEFROMCRL = 8;

//Do not include certificates in the response
private static final int _NOCERTS = 0x1;
//Do not search certificates contained in the response for a signer
private static final int _NOINTERN = 0x2;
//Do not check the signature on the response
private static final int _NOSIGS = 0x4;
//Do not verify the certificate chain on the response
private static final int _NOCHAIN = 0x8;
//Do not verify the response at all
private static final int _NOVERIFY = 0x10;
//Do not check trust
private static final int _NOEXPLICIT = 0x20;
//(This flag is not used by OpenSSL 1.0.1g)
private static final int _NOCASIGN = 0x40;
//(This flag is not used by OpenSSL 1.0.1g)
private static final int _NODELEGATED = 0x80;
//Do not make additional signing certificate checks
private static final int _NOCHECKS = 0x100;
//Do not verify additional certificates
private static final int _TRUSTOTHER = 0x200;
//Identify the response by signing the certificate key ID
private static final int _RESPID_KEY = 0x400;
//Do not include producedAt time in response
private static final int _NOTIME = 0x800;

/*
* Indicates the certificate is not revoked but does not necessarily mean
* the certificate was issued or that this response is within the
* certificate's validity interval
*/
private static final int _V_CERTSTATUS_GOOD = 0;
/* Indicates the certificate has been revoked either permanently or
* temporarily (on hold).
*/
private static final int _V_CERTSTATUS_REVOKED = 1;
/* Indicates the responder does not know about the certificate being
* requested.
*/
private static final int _V_CERTSTATUS_UNKNOWN = 2;

//The responder ID is based on the key name.
private static final int _V_RESPID_NAME = 0;
//The responder ID is based on the public key.
private static final int _V_RESPID_KEY =1;

static {
Map<Integer, String> resMap = new HashMap<Integer, String>();
resMap.put(_RESPONSE_STATUS_SUCCESSFUL, _RESPONSE_STATUS_SUCCESSFUL_STR);
resMap.put(_RESPONSE_STATUS_MALFORMEDREQUEST, _RESPONSE_STATUS_MALFORMEDREQUEST_STR);
resMap.put(_RESPONSE_STATUS_INTERNALERROR, _RESPONSE_STATUS_INTERNALERROR_STR);
resMap.put(_RESPONSE_STATUS_TRYLATER, _RESPONSE_STATUS_TRYLATER_STR);
resMap.put(_RESPONSE_STATUS_SIGREQUIRED, _RESPONSE_STATUS_SIGREQUIRED_STR);
resMap.put(_RESPONSE_STATUS_UNAUTHORIZED, _RESPONSE_STATUS_UNAUTHORIZED_STR);
responseMap = resMap;
}

public static void createOCSP(final Ruby runtime, final RubyModule OpenSSL) {
final RubyModule OCSP = OpenSSL.defineModuleUnder("OCSP");
final RubyClass OpenSSLError = OpenSSL.getClass("OpenSSLError");
Security.addProvider(new BouncyCastleProvider());
OCSP.defineClassUnder("OCSPError", OpenSSLError, OpenSSLError.getAllocator());

OCSPBasicResponse.createBasicResponse(runtime, OCSP);
OCSPCertificateId.createCertificateId(runtime, OCSP);
OCSPRequest.createRequest(runtime, OCSP);
OCSPResponse.createResponse(runtime, OCSP);
OCSPSingleResponse.createSingleResponse(runtime, OCSP);

//ResponseStatuses
OCSP.setConstant(_RESPONSE_STATUS_SUCCESSFUL_STR, runtime.newFixnum(_RESPONSE_STATUS_SUCCESSFUL));
OCSP.setConstant(_RESPONSE_STATUS_MALFORMEDREQUEST_STR, runtime.newFixnum(_RESPONSE_STATUS_MALFORMEDREQUEST));
OCSP.setConstant(_RESPONSE_STATUS_INTERNALERROR_STR, runtime.newFixnum(_RESPONSE_STATUS_INTERNALERROR));
OCSP.setConstant(_RESPONSE_STATUS_TRYLATER_STR, runtime.newFixnum(_RESPONSE_STATUS_TRYLATER));
OCSP.setConstant(_RESPONSE_STATUS_SIGREQUIRED_STR, runtime.newFixnum(_RESPONSE_STATUS_SIGREQUIRED));
OCSP.setConstant(_RESPONSE_STATUS_UNAUTHORIZED_STR, runtime.newFixnum(_RESPONSE_STATUS_UNAUTHORIZED));

//RevocationReasons
OCSP.setConstant("REVOKED_STATUS_NOSTATUS", runtime.newFixnum(_REVOKED_STATUS_NOSTATUS));
OCSP.setConstant("REVOKED_STATUS_UNSPECIFIED", runtime.newFixnum(_REVOKED_STATUS_UNSPECIFIED));
OCSP.setConstant("REVOKED_STATUS_KEYCOMPROMISE", runtime.newFixnum(_REVOKED_STATUS_KEYCOMPROMISE));
OCSP.setConstant("REVOKED_STATUS_CACOMPROMISE", runtime.newFixnum(_REVOKED_STATUS_CACOMPROMISE));
OCSP.setConstant("REVOKED_STATUS_AFFILIATIONCHANGED", runtime.newFixnum(_REVOKED_STATUS_AFFILIATIONCHANGED));
OCSP.setConstant("REVOKED_STATUS_SUPERSEDED", runtime.newFixnum(_REVOKED_STATUS_SUPERSEDED));
OCSP.setConstant("REVOKED_STATUS_CESSATIONOFOPERATION", runtime.newFixnum(_REVOKED_STATUS_CESSATIONOFOPERATION));
OCSP.setConstant("REVOKED_STATUS_CERTIFICATEHOLD", runtime.newFixnum(_REVOKED_STATUS_CERTIFICATEHOLD));
OCSP.setConstant("REVOKED_STATUS_REMOVEFROMCRL", runtime.newFixnum(_REVOKED_STATUS_REMOVEFROMCRL));

OCSP.setConstant("NOCERTS", runtime.newFixnum(_NOCERTS));
OCSP.setConstant("NOINTERN", runtime.newFixnum(_NOINTERN));
OCSP.setConstant("NOSIGS", runtime.newFixnum(_NOSIGS));
OCSP.setConstant("NOCHAIN", runtime.newFixnum(_NOCHAIN));
OCSP.setConstant("NOVERIFY", runtime.newFixnum(_NOVERIFY));
OCSP.setConstant("NOEXPLICIT", runtime.newFixnum(_NOEXPLICIT));
OCSP.setConstant("NOCASIGN", runtime.newFixnum(_NOCASIGN));
OCSP.setConstant("NODELEGATED", runtime.newFixnum(_NODELEGATED));
OCSP.setConstant("NOCHECKS", runtime.newFixnum(_NOCHECKS));
OCSP.setConstant("TRUSTOTHER", runtime.newFixnum(_TRUSTOTHER));
OCSP.setConstant("RESPID_KEY", runtime.newFixnum(_RESPID_KEY));
OCSP.setConstant("NOTIME", runtime.newFixnum(_NOTIME));

OCSP.setConstant("V_CERTSTATUS_GOOD", runtime.newFixnum(_V_CERTSTATUS_GOOD));
OCSP.setConstant("V_CERTSTATUS_REVOKED", runtime.newFixnum(_V_CERTSTATUS_REVOKED));
OCSP.setConstant("V_CERTSTATUS_UNKNOWN", runtime.newFixnum(_V_CERTSTATUS_UNKNOWN));

OCSP.setConstant("V_RESPID_NAME", runtime.newFixnum(_V_RESPID_NAME));
OCSP.setConstant("V_RESPID_KEY", runtime.newFixnum(_V_RESPID_KEY));
}

public static String getResponseStringForValue(IRubyObject fixnum) {
RubyFixnum rubyFixnum = (RubyFixnum) fixnum;
return responseMap.get((int)rubyFixnum.getLongValue());
}

public static RaiseException newOCSPError(Ruby runtime, Exception ex) {
return Utils.newError(runtime, _OCSP(runtime).getClass("OCSPError"), ex);
}

static RubyModule _OCSP(final Ruby runtime) {
return (RubyModule) runtime.getModule("OpenSSL").getConstant("OCSP");
}

}
710 changes: 710 additions & 0 deletions src/main/java/org/jruby/ext/openssl/OCSPBasicResponse.java

Large diffs are not rendered by default.

321 changes: 321 additions & 0 deletions src/main/java/org/jruby/ext/openssl/OCSPCertificateId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
/*
* The contents of this file are subject to the Common Public License Version 1.0
* (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.eclipse.org/legal/cpl-v10.html
*
* 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 APARTICULAR 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.
*
* Copyright (C) 2017 Donovan Lampa <donovan.lampa@gmail.com>
* Copyright (C) 2009-2017 The JRuby Team
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
*
*
* JRuby-OpenSSL includes software by The Legion of the Bouncy Castle Inc.
* Please, visit (http://bouncycastle.org/license.html) for licensing details.
*/
package org.jruby.ext.openssl;

import java.io.IOException;
import java.math.BigInteger;

import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ocsp.CertID;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.jruby.Ruby;
import org.jruby.RubyBignum;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

import static org.jruby.ext.openssl.OCSP._OCSP;
import static org.jruby.ext.openssl.Digest._Digest;

/**
* An OpenSSL::OCSP::CertificateId identifies a certificate to the
* CA so that a status check can be performed.
*
* @author lampad
*/
public class OCSPCertificateId extends RubyObject {
private static final long serialVersionUID = 6324454052172773918L;

private static ObjectAllocator CERTIFICATEID_ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new OCSPCertificateId(runtime, klass);
}
};

public static void createCertificateId(final Ruby runtime, final RubyModule _OCSP) {
RubyClass _certificateId = _OCSP.defineClassUnder("CertificateId", runtime.getObject(), CERTIFICATEID_ALLOCATOR);
_certificateId.defineAnnotatedMethods(OCSPCertificateId.class);
}

private CertID bcCertId;
private X509Cert originalIssuer;

public OCSPCertificateId(Ruby runtime, RubyClass metaClass) {
super(runtime, metaClass);
}

public OCSPCertificateId(Ruby runtime) {
this(runtime, (RubyClass) _OCSP(runtime).getConstantAt("CertificateId"));
}

@JRubyMethod(name = "initialize", visibility = Visibility.PRIVATE)
public IRubyObject initialize(final ThreadContext context, IRubyObject subject, IRubyObject issuer, IRubyObject digest) {
if (digest == null || digest.isNil()) {
return initialize(context, subject, issuer);
}

X509Cert subjectCert = (X509Cert) subject;
originalIssuer = (X509Cert) issuer;
BigInteger serial = subjectCert.getSerial();

return initializeImpl(context, serial, originalIssuer, digest);
}

@JRubyMethod(name = "initialize", visibility = Visibility.PRIVATE)
public IRubyObject initialize(final ThreadContext context, IRubyObject subject, IRubyObject issuer) {
Ruby runtime = context.getRuntime();

X509Cert subjectCert = (X509Cert) subject;
originalIssuer = (X509Cert) issuer;
BigInteger serial = subjectCert.getSerial();

Digest digestInstance = new Digest(runtime, _Digest(runtime));
IRubyObject digest = digestInstance.initialize(context, new IRubyObject[] { RubyString.newString(runtime, "SHA1") });

return initializeImpl(context, serial, originalIssuer, digest);
}

@JRubyMethod(name = "initialize", visibility = Visibility.PRIVATE)
public IRubyObject initialize(final ThreadContext context, IRubyObject der) {
Ruby runtime = context.getRuntime();

RubyString derStr = StringHelper.readPossibleDERInput(context, der);
try {
return initializeImpl(derStr.getBytes());
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}

private IRubyObject initializeImpl(final ThreadContext context, BigInteger serial,
IRubyObject issuerCert, IRubyObject digest) {
Ruby runtime = context.getRuntime();

Digest rubyDigest = (Digest) digest;
ASN1ObjectIdentifier oid = ASN1.sym2Oid(runtime, rubyDigest.getName().toLowerCase());
AlgorithmIdentifier bcAlgId = new AlgorithmIdentifier(oid);
BcDigestCalculatorProvider calculatorProvider = new BcDigestCalculatorProvider();
DigestCalculator calc;
try {
calc = calculatorProvider.get(bcAlgId);
}
catch (OperatorCreationException e) {
throw newOCSPError(runtime, e);
}

X509Cert rubyCert = (X509Cert) issuerCert;

try {
this.bcCertId = new CertificateID(calc, new X509CertificateHolder(rubyCert.getAuxCert().getEncoded()), serial).toASN1Primitive();
}
catch (Exception e) {
throw newOCSPError(runtime, e);
}

return this;
}

private IRubyObject initializeImpl(byte[] derByteStream) throws IOException {
this.bcCertId = CertID.getInstance(derByteStream);

return this;
}

@JRubyMethod(name = "serial")
public IRubyObject serial() {
return RubyBignum.newBignum(getRuntime(), bcCertId.getSerialNumber().getValue());
}

@JRubyMethod(name = "issuer_name_hash")
public IRubyObject issuer_name_hash() {
Ruby runtime = getRuntime();
String oidSym = ASN1.oid2Sym(runtime, getBCCertificateID().getHashAlgOID());
RubyString digestName = RubyString.newString(runtime, oidSym);

// For whatever reason, the MRI Ruby tests appear to suggest that they compute the hexdigest hash
// of the issuer name over the original name instead of the hash computed in the created CertID.
// I'm not sure how it's supposed to work with a passed in DER string since presumably the hash
// is already computed and can't be reversed to get to the original name and thus we just compute
// a hash of a hash if we don't have the original issuer around.
if (originalIssuer == null) {
try {
return Digest.hexdigest(runtime.getCurrentContext(), this, digestName,
RubyString.newString(runtime, bcCertId.getIssuerNameHash().getEncoded("DER")));
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}
else {
return Digest.hexdigest(runtime.getCurrentContext(), this, digestName,
originalIssuer.getSubject().to_der(runtime.getCurrentContext()));
}
}

// For whatever reason, the MRI Ruby tests appear to suggest that they compute the hexdigest hash
// of the issuer key over the original key instead of the hash computed in the created CertID.
// I'm not sure how it's supposed to work with a passed in DER string since presumably the hash
// is already computed and can't be reversed to get to the original key, so we just compute
// a hash of a hash if we don't have the original issuer around.
@JRubyMethod(name = "issuer_key_hash")
public IRubyObject issuer_key_hash() {
Ruby runtime = getRuntime();
String oidSym = ASN1.oid2Sym(runtime, getBCCertificateID().getHashAlgOID());
RubyString digestName = RubyString.newString(runtime, oidSym);

if (originalIssuer == null) {
try {
return Digest.hexdigest(runtime.getCurrentContext(), this, RubyString.newString(runtime, oidSym),
RubyString.newString(runtime, bcCertId.getIssuerKeyHash().getEncoded("DER")));
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}
else {
PKey key = (PKey)originalIssuer.public_key(runtime.getCurrentContext());
return Digest.hexdigest(runtime.getCurrentContext(), this, digestName, key.to_der());
}
}

@JRubyMethod(name = "hash_algorithm")
public IRubyObject hash_algorithm() {
Ruby runtime = getRuntime();
ASN1ObjectIdentifier oid = bcCertId.getHashAlgorithm().getAlgorithm();
Integer nid = ASN1.oid2nid(runtime, oid);
String ln = ASN1.nid2ln(runtime, nid);

return RubyString.newString(runtime, ln);
}

@JRubyMethod(name = "cmp")
public IRubyObject cmp(IRubyObject other) {
Ruby runtime = getRuntime();
RubyFixnum ret = (RubyFixnum) this.cmp_issuer(other);
if (!ret.eql(RubyFixnum.zero(runtime))) return ret;
OCSPCertificateId that = (OCSPCertificateId) other;
return RubyFixnum.newFixnum(
runtime,
this.getCertID().getSerialNumber().getValue().compareTo(
that.getCertID().getSerialNumber().getValue()
)
);
}

@JRubyMethod(name = "cmp_issuer")
public IRubyObject cmp_issuer(IRubyObject other) {
Ruby runtime = getRuntime();
if ( equals(other) ) {
return RubyFixnum.zero(runtime);
}
if (other instanceof OCSPCertificateId) {
OCSPCertificateId that = (OCSPCertificateId) other;
CertID thisCert = this.getCertID();
CertID thatCert = that.getCertID();
int ret = thisCert.getHashAlgorithm().getAlgorithm().toString().compareTo(
thatCert.getHashAlgorithm().getAlgorithm().toString());
if (ret != 0) return RubyFixnum.newFixnum(runtime, ret);
ret = thisCert.getIssuerNameHash().toString().compareTo(
thatCert.getIssuerNameHash().toString());
if (ret != 0) return RubyFixnum.newFixnum(runtime, ret);
return RubyFixnum.newFixnum(runtime,
thisCert.getIssuerKeyHash().toString().compareTo(
thatCert.getIssuerKeyHash().toString()));
}
else {
return runtime.getCurrentContext().nil;
}
}

@JRubyMethod(name = "to_der")
public IRubyObject to_der() {
Ruby runtime = getRuntime();
try {
return StringHelper.newString(runtime, bcCertId.getEncoded(ASN1Encoding.DER));
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}

@Override
@JRubyMethod(visibility = Visibility.PRIVATE)
public IRubyObject initialize_copy(IRubyObject obj) {
if ( this == obj ) return this;

checkFrozen();
this.bcCertId = ((OCSPCertificateId)obj).getCertID();
return this;
}

@Override
public boolean equals(Object other) {
if ( this == other ) return true;
if ( other instanceof OCSPCertificateId ) {
OCSPCertificateId that = (OCSPCertificateId) other;
return this.getCertID().equals(that.getCertID());
}
else {
return false;
}
}

public CertID getCertID() {
return bcCertId;
}

public CertificateID getBCCertificateID() {
if (bcCertId == null) return null;
return new CertificateID(bcCertId);
}

private static RaiseException newOCSPError(Ruby runtime, Exception e) {
return Utils.newError(runtime, _OCSP(runtime).getClass("OCSPError"), e);
}

}
480 changes: 480 additions & 0 deletions src/main/java/org/jruby/ext/openssl/OCSPRequest.java

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions src/main/java/org/jruby/ext/openssl/OCSPResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* The contents of this file are subject to the Common Public License Version 1.0
* (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.eclipse.org/legal/cpl-v10.html
*
* 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 APARTICULAR 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.
*
* Copyright (C) 2017 Donovan Lampa <donovan.lampa@gmail.com>
* Copyright (C) 2009-2017 The JRuby Team
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
*
*
* JRuby-OpenSSL includes software by The Legion of the Bouncy Castle Inc.
* Please, visit (http://bouncycastle.org/license.html) for licensing details.
*/
package org.jruby.ext.openssl;

import static org.jruby.ext.openssl.OCSP._OCSP;

import java.io.IOException;

import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

/*
* An OpenSSL::OCSP::Response contains the status of a certificate check which
* is created from an OpenSSL::OCSP::Request.
*
* @author lampad
*/
public class OCSPResponse extends RubyObject {
private static final long serialVersionUID = 5763247988029815198L;

private static ObjectAllocator RESPONSE_ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new OCSPResponse(runtime, klass);
}
};

public OCSPResponse(Ruby runtime, RubyClass metaClass) {
super(runtime, metaClass);
}

public OCSPResponse(Ruby runtime) {
this(runtime, (RubyClass) _OCSP(runtime).getConstantAt("Response"));
}

public static void createResponse(final Ruby runtime, final RubyModule _OCSP) {
RubyClass _request = _OCSP.defineClassUnder("Response", runtime.getObject(), RESPONSE_ALLOCATOR);
_request.defineAnnotatedMethods(OCSPResponse.class);
}

private org.bouncycastle.asn1.ocsp.OCSPResponse bcResp;

@JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE)
public IRubyObject initialize(final ThreadContext context, IRubyObject args[]) {
Ruby runtime = context.getRuntime();

if ( Arity.checkArgumentCount(runtime, args, 0, 1) == 0 ) return this;

RubyString derString = (RubyString) args[0];
try {
bcResp = org.bouncycastle.asn1.ocsp.OCSPResponse.getInstance(ASN1TaggedObject.fromByteArray(derString.getBytes()));
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}

return this;
}

@JRubyMethod(name = "create", meta = true)
public static IRubyObject create(final ThreadContext context, final IRubyObject self, IRubyObject status) {
Ruby runtime = context.runtime;
OCSPRespBuilder builder = new OCSPRespBuilder();
OCSPResp tmpResp;
OCSPResponse ret = new OCSPResponse(runtime);
try {
tmpResp = builder.build(RubyFixnum.fix2int((RubyFixnum)status), null);
ret.initialize(context, new IRubyObject[] { RubyString.newString(runtime, tmpResp.getEncoded())});
}
catch (Exception e) {
throw newOCSPError(runtime, e);
}

return ret;
}

@JRubyMethod(name = "create", meta = true)
public static IRubyObject create(final ThreadContext context, final IRubyObject self, IRubyObject status, IRubyObject basicResponse) {
Ruby runtime = context.runtime;
if (basicResponse == null || basicResponse.isNil()) {
return create(context, self, status);
}
else {
OCSPResponse ret = new OCSPResponse(runtime);
OCSPBasicResponse rubyBasicResp = (OCSPBasicResponse) basicResponse;
OCSPRespBuilder builder = new OCSPRespBuilder();
try {
OCSPResp tmpResp = builder.build(RubyFixnum.fix2int((RubyFixnum)status), new BasicOCSPResp(rubyBasicResp.getASN1BCOCSPResp()));
ret.initialize(context, new IRubyObject[] { RubyString.newString(runtime, tmpResp.getEncoded())});
}
catch (Exception e) {
throw newOCSPError(runtime, e);
}

return ret;
}
}

@Override
@JRubyMethod(name = "initialize_copy", visibility = Visibility.PRIVATE)
public IRubyObject initialize_copy(IRubyObject obj) {
if ( this == obj ) return this;

checkFrozen();
this.bcResp = ((OCSPResponse)obj).getBCResp();
return this;
}

@JRubyMethod(name = "basic")
public IRubyObject basic() {
Ruby runtime = getRuntime();
ThreadContext context = runtime.getCurrentContext();
if (bcResp == null || bcResp.getResponseBytes() == null || bcResp.getResponseBytes().getResponse() == null) {
return getRuntime().getCurrentContext().nil;
}
else {
OCSPBasicResponse ret = new OCSPBasicResponse(runtime);
return ret.initialize(context, RubyString.newString(runtime, bcResp.getResponseBytes().getResponse().getOctets()));
}
}

@JRubyMethod(name = "status")
public IRubyObject status() {
return RubyFixnum.newFixnum(getRuntime(), bcResp.getResponseStatus().getValue().longValue());
}

@JRubyMethod(name = "status_string")
public IRubyObject status_string() {
String statusStr = OCSP.getResponseStringForValue(status());
return RubyString.newString(getRuntime(), statusStr);
}

@JRubyMethod(name = "to_der")
public IRubyObject to_der() {
Ruby runtime = getRuntime();
try {
return RubyString.newString(runtime, bcResp.getEncoded());
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}

public org.bouncycastle.asn1.ocsp.OCSPResponse getBCResp() {
return bcResp;
}

private static RaiseException newOCSPError(Ruby runtime, Exception e) {
return Utils.newError(runtime, _OCSP(runtime).getClass("OCSPError"), e);
}

}
319 changes: 319 additions & 0 deletions src/main/java/org/jruby/ext/openssl/OCSPSingleResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
/*
* The contents of this file are subject to the Common Public License Version 1.0
* (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.eclipse.org/legal/cpl-v10.html
*
* 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 APARTICULAR 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.
*
* Copyright (C) 2017 Donovan Lampa <donovan.lampa@gmail.com>
* Copyright (C) 2009-2017 The JRuby Team
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
*
*
* JRuby-OpenSSL includes software by The Legion of the Bouncy Castle Inc.
* Please, visit (http://bouncycastle.org/license.html) for licensing details.
*/
package org.jruby.ext.openssl;

import static org.jruby.ext.openssl.OCSP._OCSP;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1GeneralizedTime;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.ocsp.CertID;
import org.bouncycastle.asn1.ocsp.RevokedInfo;
import org.bouncycastle.asn1.ocsp.SingleResponse;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubyTime;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

/*
* An OpenSSL::OCSP::SingleResponse represents an OCSP SingleResponse structure,
* which contains the basic information of the status of the certificate.
*
* @author lampad
*/
public class OCSPSingleResponse extends RubyObject {
private static final long serialVersionUID = 7947277768033100227L;

private static ObjectAllocator SINGLERESPONSE_ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new OCSPSingleResponse(runtime, klass);
}
};

public static void createSingleResponse(final Ruby runtime, final RubyModule _OCSP) {
RubyClass _request = _OCSP.defineClassUnder("SingleResponse", runtime.getObject(), SINGLERESPONSE_ALLOCATOR);
_request.defineAnnotatedMethods(OCSPSingleResponse.class);
}

public OCSPSingleResponse(Ruby runtime, RubyClass metaClass) {
super(runtime, metaClass);
}

public OCSPSingleResponse(Ruby runtime) {
this(runtime, (RubyClass) _OCSP(runtime).getConstantAt("SingleResponse"));
}

private SingleResponse bcSingleResponse;

@JRubyMethod(name = "initialize", visibility = Visibility.PRIVATE)
public IRubyObject initialize(final ThreadContext context, IRubyObject derStr) {
Ruby runtime = context.getRuntime();
RubyString rubyDerStr = (RubyString) derStr;
try {
bcSingleResponse = SingleResponse.getInstance(DERTaggedObject.fromByteArray(rubyDerStr.getBytes()));
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}

return this;
}

@JRubyMethod(name = "cert_status")
public IRubyObject cert_status() {
return RubyFixnum.newFixnum(getRuntime(), bcSingleResponse.getCertStatus().getTagNo());
}

@JRubyMethod(name = "certid")
public IRubyObject certid() {
Ruby runtime = getRuntime();
ThreadContext context = runtime.getCurrentContext();
CertID bcCertId = bcSingleResponse.getCertID();
OCSPCertificateId rubyCertId = new OCSPCertificateId(runtime);
try {
rubyCertId.initialize(context, RubyString.newString(runtime, bcCertId.getEncoded()));
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}

return rubyCertId;
}

@JRubyMethod(name = "check_validity", rest = true)
public IRubyObject check_validity(IRubyObject[] args) {
Ruby runtime = getRuntime();
int nsec, maxsec;
Date thisUpdate, nextUpdate;

if ( Arity.checkArgumentCount(runtime, args, 0, 2) == 0 ) {
nsec = 0;
maxsec = -1;
}
else if ( Arity.checkArgumentCount(runtime, args, 0, 2) == 1 ) {
RubyFixnum rNsec = (RubyFixnum) args[0];
nsec = (int)rNsec.getLongValue();
maxsec = -1;
}
else {
RubyFixnum rNsec = (RubyFixnum) args[0];
RubyFixnum rMaxsec = (RubyFixnum) args[1];
nsec = (int)rNsec.getLongValue();
maxsec = (int)rMaxsec.getLongValue();
}

try {
ASN1GeneralizedTime bcThisUpdate = bcSingleResponse.getThisUpdate();
if (bcThisUpdate == null) {
thisUpdate = null;
}
else {
thisUpdate = bcThisUpdate.getDate();
}
ASN1GeneralizedTime bcNextUpdate = bcSingleResponse.getNextUpdate();
if (bcNextUpdate == null) {
nextUpdate = null;
}
else {
nextUpdate = bcNextUpdate.getDate();
}
}
catch (ParseException e) {
throw newOCSPError(runtime, e);
}

return RubyBoolean.newBoolean(runtime, checkValidityImpl(thisUpdate, nextUpdate, nsec, maxsec));
}

@JRubyMethod(name = "extensions")
public IRubyObject extensions() {
Ruby runtime = getRuntime();
Extensions exts = bcSingleResponse.getSingleExtensions();
if (exts == null) return RubyArray.newEmptyArray(runtime);
List<X509Extension> retExts = new ArrayList<X509Extension>();
List<ASN1ObjectIdentifier> extOids = Arrays.asList(exts.getExtensionOIDs());
for (ASN1ObjectIdentifier extOid : extOids) {
Extension ext = exts.getExtension(extOid);
ASN1Encodable extAsn1 = ext.getParsedValue();
X509Extension retExt = X509Extension.newExtension(runtime, extOid, extAsn1, ext.isCritical());
retExts.add(retExt);
}

return RubyArray.newArray(runtime, retExts);
}

@JRubyMethod(name = "next_update")
public IRubyObject next_update() {
Ruby runtime = getRuntime();
if (bcSingleResponse.getNextUpdate() == null) return runtime.getCurrentContext().nil;
Date nextUpdate;
try {
nextUpdate = bcSingleResponse.getNextUpdate().getDate();
}
catch (ParseException e) {
throw newOCSPError(runtime, e);
}

if (nextUpdate == null) {
return runtime.getCurrentContext().nil;
}

return RubyTime.newTime(runtime, nextUpdate.getTime());
}

@JRubyMethod(name = "this_update")
public IRubyObject this_update() {
Ruby runtime = getRuntime();
if (bcSingleResponse.getThisUpdate() == null) return runtime.getCurrentContext().nil;
Date thisUpdate;
try {
thisUpdate = bcSingleResponse.getThisUpdate().getDate();
}
catch (ParseException e) {
throw newOCSPError(runtime, e);
}

return RubyTime.newTime(runtime, thisUpdate.getTime());
}

@JRubyMethod(name = "revocation_reason")
public IRubyObject revocation_reason() {
Ruby runtime = getRuntime();
RubyFixnum revoked = (RubyFixnum) _OCSP(runtime).getConstant("V_CERTSTATUS_REVOKED");
if (bcSingleResponse.getCertStatus().getTagNo() == (int)revoked.getLongValue()) {
try {
RevokedInfo revokedInfo = RevokedInfo.getInstance(
DERTaggedObject.fromByteArray(bcSingleResponse.getCertStatus().getStatus().toASN1Primitive().getEncoded())
);
return RubyFixnum.newFixnum(runtime, revokedInfo.getRevocationReason().getValue().intValue());
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}
return runtime.getCurrentContext().nil;
}

@JRubyMethod(name = "revocation_time")
public IRubyObject revocation_time() {
Ruby runtime = getRuntime();
RubyFixnum revoked = (RubyFixnum) _OCSP(runtime).getConstant("V_CERTSTATUS_REVOKED");
if (bcSingleResponse.getCertStatus().getTagNo() == (int)revoked.getLongValue()) {
try {
RevokedInfo revokedInfo = RevokedInfo.getInstance(
DERTaggedObject.fromByteArray(bcSingleResponse.getCertStatus().getStatus().toASN1Primitive().getEncoded())
);
return RubyTime.newTime(runtime, revokedInfo.getRevocationTime().getDate().getTime());
}
catch (Exception e) {
throw newOCSPError(runtime, e);
}
}
return runtime.getCurrentContext().nil;
}

@JRubyMethod(name = "to_der")
public IRubyObject to_der() {
Ruby runtime = getRuntime();
try {
return RubyString.newString(runtime, bcSingleResponse.getEncoded());
}
catch (IOException e) {
throw newOCSPError(runtime, e);
}
}

public SingleResponse getBCSingleResp() {
return bcSingleResponse;
}

// see OCSP_check_validity in ocsp_cl.c
private boolean checkValidityImpl(Date thisUpdate, Date nextUpdate, int nsec, int maxsec) {
boolean ret = true;
Date currentTime = new Date();
Date tempTime = new Date();

tempTime.setTime(currentTime.getTime() + (nsec*1000));
if (thisUpdate.compareTo(tempTime) > 0) {
ret = false;
}

if (maxsec >= 0) {
tempTime.setTime(currentTime.getTime() - (maxsec*1000));
if (thisUpdate.compareTo(tempTime) < 0) {
ret = false;
}
}

if (nextUpdate == null) {
return ret;
}

tempTime.setTime(currentTime.getTime() - (nsec*1000));
if (nextUpdate.compareTo(tempTime) < 0) {
ret = false;
}

if (nextUpdate.compareTo(thisUpdate) < 0) {
ret = false;
}

return ret;
}

private static RaiseException newOCSPError(Ruby runtime, Exception e) {
return Utils.newError(runtime, _OCSP(runtime).getClass("OCSPError"), e);
}
}
1 change: 1 addition & 0 deletions src/main/java/org/jruby/ext/openssl/OpenSSL.java
Original file line number Diff line number Diff line change
@@ -84,6 +84,7 @@ public static void createOpenSSL(final Ruby runtime) {
SSL.createSSL(runtime, _OpenSSL);
PKCS7.createPKCS7(runtime, _OpenSSL);
PKCS5.createPKCS5(runtime, _OpenSSL);
OCSP.createOCSP(runtime, _OpenSSL);

runtime.getLoadService().require("jopenssl/version");

13 changes: 7 additions & 6 deletions src/main/java/org/jruby/ext/openssl/X509StoreContext.java
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
@@ -255,20 +256,20 @@ public IRubyObject cleanup(final ThreadContext context) {

@JRubyMethod(name = "flags=")
public IRubyObject set_flags(final ThreadContext context, final IRubyObject arg) {
warn(context, "WARNING: unimplemented method called: StoreContext#flags=");
return context.runtime.getNil();
storeContext.setFlags(RubyFixnum.fix2long((RubyFixnum)arg));
return arg;
}

@JRubyMethod(name = "purpose=")
public IRubyObject set_purpose(final ThreadContext context, final IRubyObject arg) {
warn(context, "WARNING: unimplemented method called: StoreContext#purpose=");
return context.runtime.getNil();
storeContext.setPurpose(RubyFixnum.fix2int((RubyFixnum)arg));
return arg;
}

@JRubyMethod(name = "trust=")
public IRubyObject set_trust(final ThreadContext context, final IRubyObject arg) {
warn(context, "WARNING: unimplemented method called: StoreContext#trust=");
return context.runtime.getNil();
storeContext.setTrust(RubyFixnum.fix2int((RubyFixnum)arg));
return arg;
}

@JRubyMethod(name = "time=")
10 changes: 10 additions & 0 deletions src/main/java/org/jruby/ext/openssl/x509store/X509Utils.java
Original file line number Diff line number Diff line change
@@ -602,5 +602,15 @@ else if (maybeCertFile != null && new File(maybeCertFile).exists()) {

public static final int EXFLAG_INVALID_POLICY=0x400;

public static final int XKU_SSL_SERVER=0x1;
public static final int XKU_SSL_CLIENT=0x2;
public static final int XKU_SMIME=0x4;
public static final int XKU_CODE_SIGN=0x8;
public static final int XKU_SGC=0x8;
public static final int XKU_OCSP_SIGN=0x20;
public static final int XKU_TIMESTAMP=0x40;
public static final int XKU_DVCS=0x80;
public static final int XKU_ANYEKU=0x100;

public static final int POLICY_FLAG_ANY_POLICY = 0x2;
}// X509
294 changes: 294 additions & 0 deletions src/test/ruby/ssl/test_ocsp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
# coding: US-ASCII
require File.expand_path('test_helper', File.dirname(__FILE__))

class TestOCSP < TestCase
include SSLTestHelper

def setup
super
# @ca_cert
# |
# @cert
# |----------|
# @cert2 @ocsp_cert
now = Time.now

ca_subj = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=TestCA")
@ca_key = OpenSSL::PKey::RSA.new TEST_KEY_RSA1024
ca_exts = [
["basicConstraints", "CA:TRUE", true],
["keyUsage", "cRLSign,keyCertSign", true],
]
@ca_cert = issue_cert(ca_subj, @ca_key, 1, now, now+1800, ca_exts, nil, nil, OpenSSL::Digest::SHA1.new)

cert_subj = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=TestCA2")
@cert_key = OpenSSL::PKey::RSA.new TEST_KEY_RSA1024
cert_exts = [
["basicConstraints", "CA:TRUE", true],
["keyUsage", "cRLSign,keyCertSign", true],
]
@cert = issue_cert(cert_subj, @cert_key, 5, now, now+1800, cert_exts, @ca_cert, @ca_key, OpenSSL::Digest::SHA1.new)

cert2_subj = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=TestCert")
@cert2_key = OpenSSL::PKey::RSA.new TEST_KEY_RSA1024
cert2_exts = []
@cert2 = issue_cert(cert2_subj, @cert2_key, 10, now, now+1800, cert2_exts, @cert, @cert_key, OpenSSL::Digest::SHA1.new)

ocsp_subj = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=TestCAOCSP")
@ocsp_key = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
ocsp_exts = [
["extendedKeyUsage", "OCSPSigning", true],
]
@ocsp_cert = issue_cert(ocsp_subj, @ocsp_key, 100, now, now+1800, ocsp_exts, @cert, @cert_key, OpenSSL::Digest::SHA1.new)
end

def test_new_certificate_id
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
assert_kind_of OpenSSL::OCSP::CertificateId, cid
assert_equal @cert.serial, cid.serial
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA256.new)
assert_kind_of OpenSSL::OCSP::CertificateId, cid
assert_equal @cert.serial, cid.serial
end

def test_certificate_id_issuer_name_hash
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
assert_equal OpenSSL::Digest::SHA1.hexdigest(@cert.issuer.to_der), cid.issuer_name_hash
assert_equal "d91f736ac4dc3242f0fb9b77a3149bd83c5c43d0", cid.issuer_name_hash
end

def test_certificate_id_issuer_key_hash
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
assert_equal OpenSSL::Digest::SHA1.hexdigest(OpenSSL::ASN1.decode(@ca_cert.to_der).value[0].value[6].value[1].value), cid.issuer_key_hash
assert_equal "d1fef9fbf8ae1bc160cbfa03e2596dd873089213", cid.issuer_key_hash
end

def test_certificate_id_hash_algorithm
cid_sha1 = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
cid_sha256 = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA256.new)
assert_equal "sha1", cid_sha1.hash_algorithm
assert_equal "sha256", cid_sha256.hash_algorithm
end

def test_certificate_id_der
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
der = cid.to_der
asn1 = OpenSSL::ASN1.decode(der)
# hash algorithm defaults to SHA-1
assert_equal OpenSSL::ASN1.ObjectId("SHA1").to_der, asn1.value[0].value[0].to_der
assert_equal [cid.issuer_name_hash].pack("H*"), asn1.value[1].value
assert_equal [cid.issuer_key_hash].pack("H*"), asn1.value[2].value
assert_equal @cert.serial, asn1.value[3].value
assert_equal der, OpenSSL::OCSP::CertificateId.new(der).to_der
end

def test_certificate_id_dup
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
other = cid.dup.to_der
assert_equal cid.to_der, other
end

def test_request_der
request = OpenSSL::OCSP::Request.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
request.add_certid(cid)
request.sign(@cert, @cert_key, [@ca_cert], 0)
asn1 = OpenSSL::ASN1.decode(request.to_der)
# TODO: ASN1#to_der seems to be missing some data...
# assert_equal cid.to_der, asn1.value[0].value.find { |a| a.tag_class == :UNIVERSAL }.value[0].value[0].to_der
assert_equal OpenSSL::ASN1.ObjectId("sha1WithRSAEncryption").to_der, asn1.value[1].value[0].value[0].value[0].to_der
# assert_equal @cert.to_der, asn1.value[1].value[0].value[2].value[0].value[0].to_der
# assert_equal @ca_cert.to_der, asn1.value[1].value[0].value[2].value[0].value[1].to_der
# assert_equal asn1.to_der, OpenSSL::OCSP::Request.new(asn1.to_der).to_der
end

def test_request_sign_verify
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
store = OpenSSL::X509::Store.new.add_cert(@ca_cert)

# with signer cert
req = OpenSSL::OCSP::Request.new.add_certid(cid)
req.sign(@cert, @cert_key, [])
assert_equal true, req.verify([], store)

# without signer cert
req = OpenSSL::OCSP::Request.new.add_certid(cid)
req.sign(@cert, @cert_key, nil)
assert_equal false, req.verify([@cert2], store)
assert_equal false, req.verify([], store) # no signer
assert_equal false, req.verify([], store, OpenSSL::OCSP::NOVERIFY)

assert_equal true, req.verify([@cert], store, OpenSSL::OCSP::NOINTERN)
ret = req.verify([@cert], store)
if ret || OpenSSL::OPENSSL_VERSION =~ /OpenSSL/ && OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10002000
assert_equal true, ret
else
# RT2560; OCSP_request_verify() does not find signer cert from 'certs' when
# OCSP_NOINTERN is not specified.
# fixed by OpenSSL 1.0.1j, 1.0.2 and LibreSSL 2.4.2
pend "RT2560: ocsp_req_find_signer"
end
end

def test_request_nonce
req0 = OpenSSL::OCSP::Request.new
req1 = OpenSSL::OCSP::Request.new.add_nonce("NONCE")
req2 = OpenSSL::OCSP::Request.new.add_nonce("ABCDE")
bres = OpenSSL::OCSP::BasicResponse.new
assert_equal 2, req0.check_nonce(bres)
bres.copy_nonce(req1)
assert_equal 3, req0.check_nonce(bres)
assert_equal 1, req1.check_nonce(bres)
bres.add_nonce("NONCE")
assert_equal 1, req1.check_nonce(bres)
assert_equal 0, req2.check_nonce(bres)
end

def test_request_dup
request = OpenSSL::OCSP::Request.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
request.add_certid(cid)
assert_equal request.to_der, request.dup.to_der
end

def test_basic_response_der
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, [])
bres.add_nonce("NONCE")
bres.sign(@ocsp_cert, @ocsp_key, [@ca_cert], 0)
der = bres.to_der
asn1 = OpenSSL::ASN1.decode(der)
assert_equal OpenSSL::ASN1.Sequence([@ocsp_cert, @ca_cert]).to_der, asn1.value[3].value[0].to_der
assert_equal der, OpenSSL::OCSP::BasicResponse.new(der).to_der
rescue TypeError
if /GENERALIZEDTIME/ =~ $!.message
pend "OCSP_basic_sign() is broken"
else
raise
end
end

def test_basic_response_sign_verify
store = OpenSSL::X509::Store.new.add_cert(@ca_cert)

# signed by CA
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA256.new)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, nil, -400, -300, 500, [])
bres.sign(@ca_cert, @ca_key, nil, 0, OpenSSL::Digest::SHA256.new)
assert_equal false, bres.verify([], store) # signer not found
assert_equal true, bres.verify([@ca_cert], store)
bres.sign(@ca_cert, @ca_key, [], 0, OpenSSL::Digest::SHA256.new)
assert_equal true, bres.verify([], store)

# signed by OCSP signer
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert2, @cert)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, nil, -400, -300, 500, [])
bres.sign(@ocsp_cert, @ocsp_key, [@cert])
assert_equal true, bres.verify([], store)
assert_equal false, bres.verify([], store, OpenSSL::OCSP::NOCHAIN)
# OpenSSL had a bug on this; test that our workaround works
bres.sign(@ocsp_cert, @ocsp_key, [])
assert_equal true, bres.verify([@cert], store)
end

def test_basic_response_dup
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, [])
bres.sign(@ocsp_cert, @ocsp_key, [@ca_cert], 0)
assert_equal bres.to_der, bres.dup.to_der
end

def test_basic_response_response_operations
bres = OpenSSL::OCSP::BasicResponse.new
now = Time.at(Time.now.to_i)
cid1 = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
cid2 = OpenSSL::OCSP::CertificateId.new(@ocsp_cert, @ca_cert, OpenSSL::Digest::SHA1.new)
cid3 = OpenSSL::OCSP::CertificateId.new(@ca_cert, @ca_cert, OpenSSL::Digest::SHA1.new)
bres.add_status(cid1, OpenSSL::OCSP::V_CERTSTATUS_REVOKED, OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED, now - 400, -300, nil, nil)
bres.add_status(cid2, OpenSSL::OCSP::V_CERTSTATUS_GOOD, nil, nil, -300, 500, [])

assert_equal 2, bres.responses.size
single = bres.responses.first
assert_equal cid1.to_der, single.certid.to_der
assert_equal OpenSSL::OCSP::V_CERTSTATUS_REVOKED, single.cert_status
assert_equal OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED, single.revocation_reason
assert_equal now - 400, single.revocation_time
assert_in_delta (now - 301), single.this_update, 1
assert_equal nil, single.next_update
assert_equal [], single.extensions

assert_equal cid2.to_der, bres.find_response(cid2).certid.to_der
assert_equal nil, bres.find_response(cid3)
end

def test_single_response_der
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, nil, nil, -300, 500, nil)
single = bres.responses[0]
der = single.to_der
asn1 = OpenSSL::ASN1.decode(der)
assert_equal :CONTEXT_SPECIFIC, asn1.value[1].tag_class
assert_equal 0, asn1.value[1].tag # good
assert_equal der, OpenSSL::OCSP::SingleResponse.new(der).to_der
end

def test_single_response_check_validity
bres = OpenSSL::OCSP::BasicResponse.new
cid1 = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
cid2 = OpenSSL::OCSP::CertificateId.new(@ocsp_cert, @ca_cert, OpenSSL::Digest::SHA1.new)
bres.add_status(cid1, OpenSSL::OCSP::V_CERTSTATUS_REVOKED, OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED, -400, -300, -50, [])
bres.add_status(cid2, OpenSSL::OCSP::V_CERTSTATUS_REVOKED, OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED, -400, -300, nil, [])
bres.add_status(cid2, OpenSSL::OCSP::V_CERTSTATUS_GOOD, nil, nil, Time.now + 100, nil, nil)

if bres.responses[2].check_validity # thisUpdate is in future; must fail
# LibreSSL bug; skip for now
pend "OCSP_check_validity() is broken"
end

single1 = bres.responses[0]
assert_equal false, single1.check_validity
assert_equal false, single1.check_validity(30)
assert_equal true, single1.check_validity(60)
single2 = bres.responses[1]
assert_equal true, single2.check_validity
assert_equal true, single2.check_validity(0, 500)
assert_equal false, single2.check_validity(0, 200)
end

def test_response
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, [])
bres.sign(@ocsp_cert, @ocsp_key, [])
res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres)

assert_equal bres.to_der, res.basic.to_der
assert_equal OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, res.status
end

def test_response_der
bres = OpenSSL::OCSP::BasicResponse.new
cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert, OpenSSL::Digest::SHA1.new)
bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, [])
bres.sign(@ocsp_cert, @ocsp_key, [@ca_cert], 0)
res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres)
der = res.to_der
asn1 = OpenSSL::ASN1.decode(der)
assert_equal OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, asn1.value[0].value
assert_equal OpenSSL::ASN1.ObjectId("basicOCSPResponse").to_der, asn1.value[1].value[0].value[0].to_der
assert_equal bres.to_der, asn1.value[1].value[0].value[1].value
assert_equal der, OpenSSL::OCSP::Response.new(der).to_der
end

def test_response_dup
bres = OpenSSL::OCSP::BasicResponse.new
bres.sign(@ocsp_cert, @ocsp_key, [@ca_cert], 0)
res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres)
assert_equal res.to_der, res.dup.to_der
end
end

0 comments on commit 96955ce

Please sign in to comment.