/***** BEGIN LICENSE BLOCK ***** * Version: EPL 1.0/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Eclipse 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/epl-v10.html * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * Copyright (C) 2006, 2007 Ola Bini <ola@ologix.com> * * 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. ***** END LICENSE BLOCK *****/ package org.jruby.ext.openssl; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringWriter; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.security.GeneralSecurityException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.CRLException; import java.security.cert.CertificateFactory; import java.security.cert.X509CRLEntry; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.cert.X509CRLHolder; import org.bouncycastle.cert.X509v2CRLBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.joda.time.DateTime; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyClass; import org.jruby.RubyInteger; import org.jruby.RubyModule; import org.jruby.RubyNumeric; 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.ext.openssl.x509store.PEMInputOutput; import org.jruby.runtime.Arity; import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.Visibility; import org.jruby.runtime.builtin.Variable; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; import static org.jruby.ext.openssl.OpenSSL.*; import static org.jruby.ext.openssl.X509._X509; import static org.jruby.ext.openssl.X509Extension.newExtension; import static org.jruby.ext.openssl.StringHelper.appendGMTDateTime; import static org.jruby.ext.openssl.StringHelper.appendLowerHexValue; /** * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a> */ public class X509CRL extends RubyObject { private static final long serialVersionUID = -2463300006179688577L; private static ObjectAllocator X509CRL_ALLOCATOR = new ObjectAllocator() { public IRubyObject allocate(Ruby runtime, RubyClass klass) { return new X509CRL(runtime, klass); } }; public static void createX509CRL(final Ruby runtime, final RubyModule _X509) { RubyClass _CRL = _X509.defineClassUnder("CRL", runtime.getObject(), X509CRL_ALLOCATOR); RubyClass _OpenSSLError = runtime.getModule("OpenSSL").getClass("OpenSSLError"); _X509.defineClassUnder("CRLError", _OpenSSLError, _OpenSSLError.getAllocator()); _CRL.defineAnnotatedMethods(X509CRL.class); } private RubyInteger version; private IRubyObject issuer; private RubyTime last_update; private RubyTime next_update; private RubyArray revoked; private RubyArray extensions; private IRubyObject signature_algorithm; private boolean changed = true; private java.security.cert.X509CRL crl = null; private transient X509CRLHolder crlHolder; private transient ASN1Primitive crlValue; static RubyClass _CRL(final Ruby runtime) { return _X509(runtime).getClass("CRL"); } public X509CRL(Ruby runtime, RubyClass type) { super(runtime, type); } private X509CRL(Ruby runtime) { super(runtime, _CRL(runtime)); } java.security.cert.X509CRL getCRL() { if ( crl != null ) return crl; try { if ( crlHolder == null ) { throw new IllegalStateException("no crl holder"); } final byte[] encoded = crlHolder.getEncoded(); return crl = generateCRL(encoded, 0, encoded.length); } catch (IOException ex) { throw newCRLError(getRuntime(), ex); } catch (GeneralSecurityException ex) { throw newCRLError(getRuntime(), ex); } } private X509CRLHolder getCRLHolder(boolean allowNull) { if ( crlHolder != null ) return crlHolder; try { if ( crl == null ) { if ( allowNull ) return null; throw new IllegalStateException("no crl"); } return crlHolder = new X509CRLHolder(crl.getEncoded()); } catch (IOException ex) { throw newCRLError(getRuntime(), ex); } catch (CRLException ex) { throw newCRLError(getRuntime(), ex); } } final byte[] getEncoded() throws IOException, CRLException { if ( crlHolder != null ) return crlHolder.getEncoded(); return getCRL().getEncoded(); } private byte[] getSignature() { return getCRL().getSignature(); } private static boolean avoidJavaSecurity = false; private static java.security.cert.X509CRL generateCRL( final byte[] bytes, final int offset, final int length) throws GeneralSecurityException { CertificateFactory factory = SecurityHelper.getCertificateFactory("X.509"); return (java.security.cert.X509CRL) factory.generateCRL( new ByteArrayInputStream(bytes, offset, length) ); } private static X509CRLHolder parseCRLHolder( final byte[] bytes, final int offset, final int length) throws IOException { return new X509CRLHolder(new ByteArrayInputStream(bytes, offset, length)); } @JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args, final Block block) { final Ruby runtime = context.runtime; this.extensions = runtime.newArray(8); if ( Arity.checkArgumentCount(runtime, args, 0, 1) == 0 ) return this; final ByteList strList = args[0].asString().getByteList(); final byte[] bytes = strList.unsafeBytes(); final int offset = strList.getBegin(); final int length = strList.getRealSize(); try { if ( avoidJavaSecurity ) { this.crlHolder = parseCRLHolder(bytes, offset, length); } else { this.crl = generateCRL(bytes, offset, length); } } catch (IOException e) { debugStackTrace(runtime, e); throw newCRLError(runtime, e); } catch (GeneralSecurityException e) { debugStackTrace(runtime, e); throw newCRLError(runtime, e); } set_last_update( context, RubyTime.newTime(runtime, crl.getThisUpdate().getTime()) ); set_next_update( context, RubyTime.newTime(runtime, crl.getNextUpdate().getTime()) ); set_issuer( X509Name.newName(runtime, crl.getIssuerX500Principal()) ); final int version = crl.getVersion(); this.version = runtime.newFixnum( version > 0 ? version - 1 : 2 ); extractExtensions(context); Set<? extends X509CRLEntry> revokedCRLs = crl.getRevokedCertificates(); if ( revokedCRLs != null && ! revokedCRLs.isEmpty() ) { final X509CRLEntry[] revokedSorted = revokedCRLs.toArray(new X509CRLEntry[ revokedCRLs.size() ]); Arrays.sort(revokedSorted, 0, revokedSorted.length, new Comparator<X509CRLEntry>() { public int compare(X509CRLEntry o1, X509CRLEntry o2) { return o1.getRevocationDate().compareTo( o2.getRevocationDate() ); } }); for ( X509CRLEntry entry : revokedSorted ) { revoked().append( X509Revoked.newInstance(context, entry) ); } } this.changed = false; return this; } private void extractExtensions(final ThreadContext context) { if ( crlHolder != null ) extractExtensions(context, crlHolder); else extractExtensionsCRL(context, getCRL()); } @SuppressWarnings("unchecked") private void extractExtensions(final ThreadContext context, final X509CRLHolder crl) { if ( ! crlHolder.hasExtensions() ) return; for ( ASN1ObjectIdentifier oid : (Collection<ASN1ObjectIdentifier>) crl.getExtensionOIDs() ) { addExtension(context, oid, crl); } } private void addExtension(final ThreadContext context, final ASN1ObjectIdentifier extOID, final X509CRLHolder crl) { final Extension ext = crl.getExtension(extOID); final IRubyObject extension = newExtension(context.runtime, extOID, ext); this.extensions.append(extension); } private void extractExtensionsCRL(final ThreadContext context, final java.security.cert.X509Extension crl) { //final RubyClass _Extension = _Extension(context.runtime); final Set<String> criticalExtOIDs = crl.getCriticalExtensionOIDs(); if ( criticalExtOIDs != null ) { for ( final String extOID : criticalExtOIDs ) { addExtensionCRL(context, extOID, crl, true); } } final Set<String> nonCriticalExtOIDs = crl.getNonCriticalExtensionOIDs(); if ( nonCriticalExtOIDs != null ) { for ( final String extOID : nonCriticalExtOIDs ) { addExtensionCRL(context, extOID, crl, false); } } } private void addExtensionCRL(final ThreadContext context, final String extOID, final java.security.cert.X509Extension crl, final boolean critical) { try { final IRubyObject extension = newExtension(context, extOID, crl, critical); if ( extension != null ) this.extensions.append(extension); } catch (IOException e) { throw newCRLError(context.runtime, e); } } @Override @JRubyMethod(visibility = Visibility.PRIVATE) public IRubyObject initialize_copy(final IRubyObject obj) { if ( this == obj ) return this; return super.initialize_copy(obj); } @JRubyMethod(name = {"to_pem", "to_s"}) public IRubyObject to_pem(final ThreadContext context) { StringWriter writer = new StringWriter(); try { PEMInputOutput.writeX509CRL(writer, crl); return RubyString.newString(context.runtime, writer.getBuffer()); } catch (IOException e) { throw newCRLError(context.runtime, e); } } @JRubyMethod public IRubyObject to_der(final ThreadContext context) { try { return StringHelper.newString(context.runtime, getEncoded()); } catch (IOException e) { throw newCRLError(context.runtime, e); } catch (CRLException e) { throw newCRLError(context.runtime, e); } } @JRubyMethod @SuppressWarnings("unchecked") public IRubyObject to_text(final ThreadContext context) { final Ruby runtime = context.runtime; final char[] S16 = StringHelper.S20; final StringBuilder text = new StringBuilder(160); text.append("Certificate Revocation List (CRL):\n"); final int version = RubyNumeric.fix2int(this.version); text.append(S16,0,8).append("Version ").append( version + 1 ). append(" (0x").append( Integer.toString( version, 16 ) ).append(")\n"); text.append(S16,0,4).append("Signature Algorithm: ").append( signature_algorithm() ).append('\n'); text.append(S16,0,8).append("Issuer: ").append( issuer() ).append('\n'); text.append(S16,0,8).append("Last Update: "); appendGMTDateTime( text, getLastUpdate() ).append('\n'); if ( ! next_update().isNil() ) { text.append(S16,0,8).append("Next Update: "); appendGMTDateTime( text, getNextUpdate() ).append('\n'); } else { text.append(S16,0,8).append("Next Update: NONE\n"); } if ( extensions != null && extensions.size() > 0 ) { text.append(S16,0,8).append("CRL extensions:\n"); extensions_to_text(context, extensions, text, 12); } if ( revoked != null && revoked.size() > 0 ) { text.append("\nRevoked Certificates:\n"); for ( int i = 0; i < revoked.size(); i++ ) { final X509Revoked rev = (X509Revoked) revoked.entry(i); final String serial = rev.serial.toString(16); text.append(S16,0,4).append("Serial Number: "); if ( serial.length() % 2 == 0 ) text.append(serial).append('\n'); else text.append('0').append(serial).append('\n'); text.append(S16,0,8).append("Revocation Date: "); appendGMTDateTime( text, rev.getTime() ).append('\n'); if ( rev.hasExtensions() ) { text.append(S16,0,8).append("CRL entry extensions:\n"); extensions_to_text(context, extensions, text, 12); } } } else { text.append("No Revoked Certificates.\n"); } // TODO we shall parse / use crlValue when != null : text.append(S16,0,4).append("Signature Algorithm: ").append( signature_algorithm() ).append('\n'); appendLowerHexValue(text, getSignature(), 9, 54); return RubyString.newString( runtime, text ); } static void extensions_to_text(final ThreadContext context, final List<X509Extension> exts, final StringBuilder text, final int indent) { final char[] S20 = StringHelper.S20; for ( int i = 0; i < exts.size(); i++ ) { final X509Extension ext = exts.get(i); final ASN1ObjectIdentifier oid = ext.getRealObjectID(); final String no = ASN1.o2a(context.runtime, oid); text.append(S20,0,indent).append( no ).append(": "); if ( ext.isRealCritical() ) text.append("critical"); text.append('\n'); final String value = ext.value(context).toString(); for ( String val : value.split("\n") ) { text.append(S20,0,16).append( val ).append('\n'); } } } @Override @JRubyMethod @SuppressWarnings("unchecked") public IRubyObject inspect() { return ObjectSupport.inspect(this, (List) getInstanceVariableList()); } @Override // FAKE'em to include "instance" variables in inspect public List<Variable<IRubyObject>> getInstanceVariableList() { final ArrayList<Variable<IRubyObject>> list = new ArrayList<Variable<IRubyObject>>(6); return list; } @JRubyMethod public IRubyObject version() { return version == null ? version = getRuntime().newFixnum(0) : version; } @JRubyMethod(name="version=") public IRubyObject set_version(IRubyObject version) { if ( ! version.equals(this.version) ) this.changed = true; return this.version = version.convertToInteger("to_i"); } @JRubyMethod public IRubyObject signature_algorithm() { return signature_algorithm == null ? signature_algorithm = signature_algorithm(getRuntime()) : signature_algorithm; } private RubyString signature_algorithm(final Ruby runtime) { return RubyString.newString(runtime, getSignatureAlgorithm(runtime, "itu-t")); } private String getSignatureAlgorithm(final Ruby runtime, final String def) { final X509CRLHolder crlHolder = getCRLHolder(true); if ( crlHolder == null ) return def; ASN1ObjectIdentifier algId = crlHolder.toASN1Structure().getSignatureAlgorithm().getAlgorithm(); //ASN1ObjectIdentifier algId = ASN1.toObjectID( getCRL().getSigAlgOID(), true ); String algName; if ( algId != null ) { algName = ASN1.o2a(runtime, algId, true); } else algName = null; //else { // algName = getCRL().getSigAlgName(); // algId = ASN1.toObjectID( algName, true ); // if ( algId != null ) { // algName = ASN1.o2a(runtime, algId, true); // } //} return algName == null ? def : algName; } @JRubyMethod public IRubyObject issuer() { return this.issuer == null ? this.issuer = X509Name.newName(getRuntime()) : this.issuer; } @JRubyMethod(name="issuer=") public IRubyObject set_issuer(final IRubyObject issuer) { if ( ! issuer.equals(this.issuer) ) this.changed = true; return this.issuer = issuer; } DateTime getLastUpdate() { if ( last_update == null ) return null; return last_update.getDateTime(); } @JRubyMethod public IRubyObject last_update() { return last_update == null ? getRuntime().getNil() : last_update; } @JRubyMethod(name="last_update=") public IRubyObject set_last_update(final ThreadContext context, IRubyObject val) { this.changed = true; final RubyTime value = (RubyTime) val.callMethod(context, "getutc"); value.setMicroseconds(0); return this.last_update = value; } DateTime getNextUpdate() { if ( next_update == null ) return null; return next_update.getDateTime(); } @JRubyMethod public IRubyObject next_update() { return next_update == null ? getRuntime().getNil() : next_update; } @JRubyMethod(name="next_update=") public IRubyObject set_next_update(final ThreadContext context, IRubyObject val) { this.changed = true; final RubyTime value = (RubyTime) val.callMethod(context, "getutc"); value.setMicroseconds(0); return this.next_update = value; } @JRubyMethod public RubyArray revoked() { return revoked == null ? revoked = getRuntime().newArray(4) : revoked; } @JRubyMethod(name="revoked=") public IRubyObject set_revoked(final IRubyObject revoked) { this.changed = true; return this.revoked = (RubyArray) revoked; } @JRubyMethod public IRubyObject add_revoked(final ThreadContext context, IRubyObject val) { this.changed = true; revoked().callMethod(context, "<<", val); return val; } @JRubyMethod public RubyArray extensions() { return this.extensions; } @SuppressWarnings("unchecked") @JRubyMethod(name="extensions=") public IRubyObject set_extensions(final IRubyObject extensions) { return this.extensions = (RubyArray) extensions; } @JRubyMethod public IRubyObject add_extension(final IRubyObject extension) { extensions().append(extension); return extension; } @JRubyMethod public IRubyObject sign(final ThreadContext context, final IRubyObject key, IRubyObject digest) { final Ruby runtime = context.runtime; final String signatureAlgorithm = getSignatureAlgorithm(runtime, (PKey) key, (Digest) digest); final X500Name issuerName = ((X509Name) issuer).getX500Name(); final java.util.Date thisUpdate = getLastUpdate().toDate(); final X509v2CRLBuilder generator = new X509v2CRLBuilder(issuerName, thisUpdate); final java.util.Date nextUpdate = getNextUpdate().toDate(); generator.setNextUpdate(nextUpdate); //signature_algorithm = RubyString.newString(runtime, digAlg); //generator.setSignatureAlgorithm( signatureAlgorithm ); if ( revoked != null ) { for ( int i = 0; i < revoked.size(); i++ ) { final X509Revoked rev = (X509Revoked) revoked.entry(i); BigInteger serial = new BigInteger( rev.callMethod(context, "serial").toString() ); RubyTime t1 = (RubyTime) rev.callMethod(context, "time").callMethod(context, "getutc"); t1.setMicroseconds(0); final Extensions revExts; if ( rev.hasExtensions() ) { final RubyArray exts = rev.extensions(); final ASN1Encodable[] array = new ASN1Encodable[ exts.size() ]; for ( int j = 0; j < exts.size(); j++ ) { final X509Extension ext = (X509Extension) exts.entry(j); try { array[j] = ext.toASN1Sequence(); } catch (IOException e) { throw newCRLError(runtime, e); } } revExts = Extensions.getInstance( new DERSequence(array) ); } else { revExts = null; } generator.addCRLEntry( serial, t1.getJavaDate(), revExts ); } } try { for ( int i = 0; i < extensions.size(); i++ ) { X509Extension ext = (X509Extension) extensions.entry(i); ASN1Encodable value = ext.getRealValue(); generator.addExtension(ext.getRealObjectID(), ext.isRealCritical(), value); } } catch (IOException e) { throw newCRLError(runtime, e); } final PrivateKey privateKey = ((PKey) key).getPrivateKey(); try { if ( avoidJavaSecurity ) { // NOT IMPLEMENTED } else { //crl = generator.generate(((PKey) key).getPrivateKey()); } /* AlgorithmIdentifier keyAldID = new AlgorithmIdentifier(new ASN1ObjectIdentifier(keyAlg)); AlgorithmIdentifier digAldID = new AlgorithmIdentifier(new ASN1ObjectIdentifier(digAlg)); final BcContentSignerBuilder signerBuilder; final AsymmetricKeyParameter signerPrivateKey; if ( isDSA ) { signerBuilder = new BcDSAContentSignerBuilder(keyAldID, digAldID); DSAPrivateKey privateKey = (DSAPrivateKey) ((PKey) key).getPrivateKey(); DSAParameters params = new DSAParameters( privateKey.getParams().getP(), privateKey.getParams().getQ(), privateKey.getParams().getG() ); signerPrivateKey = new DSAPrivateKeyParameters(privateKey.getX(), params); } */ ContentSigner signer = new JcaContentSignerBuilder( signatureAlgorithm ).build(privateKey); this.crlHolder = generator.build( signer ); this.crl = null; } catch (IllegalStateException e) { debugStackTrace(e); throw newCRLError(runtime, e); } catch (Exception e) { debugStackTrace(e); throw newCRLError(runtime, e.getMessage()); } final ASN1Primitive crlVal = getCRLValue(runtime); ASN1Sequence v1 = (ASN1Sequence) ( ((ASN1Sequence) crlVal).getObjectAt(0) ); final ASN1EncodableVector build1 = new ASN1EncodableVector(); int copyIndex = 0; if ( v1.getObjectAt(0) instanceof ASN1Integer ) copyIndex++; build1.add( new ASN1Integer( new BigInteger(version.toString()) ) ); while ( copyIndex < v1.size() ) { build1.add( v1.getObjectAt(copyIndex++) ); } final ASN1EncodableVector build2 = new ASN1EncodableVector(); build2.add( new DLSequence(build1) ); build2.add( ((ASN1Sequence) crlVal).getObjectAt(1) ); build2.add( ((ASN1Sequence) crlVal).getObjectAt(2) ); this.crlValue = new DLSequence(build2); changed = false; return this; } private String getSignatureAlgorithm(final Ruby runtime, final PKey key, final Digest digest) { // Have to obey some artificial constraints of the OpenSSL implementation. Stupid. final String keyAlg = key.getAlgorithm(); final String digAlg = digest.getShortAlgorithm(); if ( "DSA".equalsIgnoreCase(keyAlg) ) { if ( ( "MD5".equalsIgnoreCase( digAlg ) ) ) { // || // ( "SHA1".equals( digest.name().toString() ) ) ) { throw newCRLError(runtime, "unsupported key / digest algorithm ("+ key +" / "+ digAlg +")"); } } else if ( "RSA".equalsIgnoreCase(keyAlg) ) { if ( "DSS1".equals( digest.name().toString() ) ) { throw newCRLError(runtime, "unsupported key / digest algorithm ("+ key +" / "+ digAlg +")"); } } return digAlg + "WITH" + keyAlg; } private boolean isDSA(final PKey key) { return "DSA".equalsIgnoreCase( key.getAlgorithm() ); } private ASN1Primitive getCRLValue(final Ruby runtime) { if ( this.crlValue != null ) return this.crlValue; return this.crlValue = readCRL( runtime ); } private ASN1Primitive readCRL(final Ruby runtime) { try { return ASN1.readObject( getEncoded() ); } catch (CRLException e) { throw newCRLError(runtime, e); } catch (IOException e) { throw newCRLError(runtime, e); } } @JRubyMethod public IRubyObject verify(final ThreadContext context, final IRubyObject key) { if ( changed ) return context.runtime.getFalse(); final PublicKey publicKey = ((PKey) key).getPublicKey(); try { boolean valid = SecurityHelper.verify(getCRL(), publicKey, true); return context.runtime.newBoolean(valid); } catch (GeneralSecurityException e) { debug("CRL#verify() failed:", e); return context.runtime.getFalse(); } } private static RubyClass _CRLError(final Ruby runtime) { return _X509(runtime).getClass("CRLError"); } static RaiseException newCRLError(Ruby runtime, Exception e) { return Utils.newError(runtime, _CRLError(runtime), e); } private static RaiseException newCRLError(Ruby runtime, String message) { return Utils.newError(runtime, _CRLError(runtime), message); } }// X509CRL